@claude-transcripts/cli 0.5.0 → 0.6.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.
@@ -1,5 +1,92 @@
1
1
  // @bun
2
- // packages/shared/src/model/cli-project.ts
2
+ // packages/shared/src/blueprint/actions.ts
3
+ var ACTIONS = [
4
+ {
5
+ key: "write-event-marker",
6
+ summary: "Append a light append-only event marker doc",
7
+ implemented: true
8
+ },
9
+ {
10
+ key: "update-counts",
11
+ summary: "Bump per-session counters (events/prompts/errors/tools)",
12
+ implemented: true
13
+ },
14
+ {
15
+ key: "flush-transcript-chunk",
16
+ summary: "Tail the transcript \u2192 append chunk docs",
17
+ implemented: true
18
+ },
19
+ { key: "write-summary", summary: "Write summary:<id> at session end", implemented: true },
20
+ {
21
+ key: "upload-blobs",
22
+ summary: "Upload summary.json + transcript.jsonl to S3",
23
+ implemented: true
24
+ },
25
+ {
26
+ key: "seed-session-start",
27
+ summary: "Reset counters + seed the chunk offset at start",
28
+ implemented: true
29
+ },
30
+ {
31
+ key: "announce-recording",
32
+ summary: "Tell the user, in the transcript, whether and where this session is recorded",
33
+ implemented: true
34
+ },
35
+ {
36
+ key: "inject-recall-policy",
37
+ summary: "Prime the session with the recall policy + how much history this cwd has",
38
+ implemented: true
39
+ },
40
+ { key: "enrich-metadata", summary: "Post additional session metadata", implemented: false },
41
+ {
42
+ key: "extract-feature",
43
+ summary: "Map-reduce feature extraction (urls/repos/PRs/\u2026)",
44
+ implemented: false
45
+ },
46
+ { key: "detect-memory-write", summary: "Flag Edit/Write to memory paths", implemented: false },
47
+ { key: "app-log", summary: "Emit an operational log record (app-logs DB)", implemented: false },
48
+ {
49
+ key: "index-for-search",
50
+ summary: "Push derived content to the search backend",
51
+ implemented: false
52
+ },
53
+ {
54
+ key: "reconcile-session",
55
+ summary: "Finalize a stale running session from chunks/S3",
56
+ implemented: false
57
+ }
58
+ ];
59
+ var BINDINGS = [
60
+ {
61
+ event: "SessionStart",
62
+ actions: [
63
+ "seed-session-start",
64
+ "write-event-marker",
65
+ "announce-recording",
66
+ "inject-recall-policy"
67
+ ]
68
+ },
69
+ {
70
+ event: "UserPromptSubmit",
71
+ actions: ["write-event-marker", "update-counts", "flush-transcript-chunk"]
72
+ },
73
+ {
74
+ event: "PostToolUse",
75
+ actions: ["write-event-marker", "update-counts", "flush-transcript-chunk"]
76
+ },
77
+ {
78
+ event: "PostToolUseFailure",
79
+ actions: ["write-event-marker", "update-counts", "flush-transcript-chunk"]
80
+ },
81
+ { event: "Stop", actions: ["write-event-marker", "flush-transcript-chunk"] },
82
+ { event: "SubagentStart", actions: ["write-event-marker", "update-counts"] },
83
+ { event: "SubagentStop", actions: ["write-event-marker", "update-counts"] },
84
+ { event: "StopFailure", actions: ["write-event-marker", "update-counts"] },
85
+ { event: "PreCompact", actions: ["write-event-marker"] },
86
+ { event: "PostCompact", actions: ["write-event-marker"] },
87
+ { event: "SessionEnd", actions: ["flush-transcript-chunk", "write-summary", "upload-blobs"] }
88
+ ];
89
+ // packages/shared/src/blueprint/cli-project.ts
3
90
  var isFlag = (a) => a.name.startsWith("--");
4
91
  function cliArgType(a) {
5
92
  if (a.type)
@@ -380,7 +467,7 @@ ${lines.join(`
380
467
  `;
381
468
  }
382
469
 
383
- // packages/shared/src/model/cli.ts
470
+ // packages/shared/src/blueprint/cli.ts
384
471
  var WEBAPI = {
385
472
  name: "--webapi",
386
473
  type: "string",
@@ -673,14 +760,20 @@ var CLI_SPEC = {
673
760
  args: [
674
761
  {
675
762
  name: "action",
676
- choices: ["run", "install", "uninstall", "status"],
763
+ choices: ["run", "upload", "install", "uninstall", "status"],
677
764
  default: "status",
678
- description: "`run` reads one event payload from stdin (Claude Code calls this)"
765
+ description: "`run` reads one event payload from stdin (Claude Code calls this); `upload` " + "ships one transcript to S3 (SessionEnd starts it detached)"
679
766
  },
680
767
  { name: "--dry-run", description: "with `install`: show the change without writing" },
681
768
  {
682
769
  name: "--force",
683
770
  description: "with `install`: register even if the plugin already does"
771
+ },
772
+ { name: "--session", type: "string", description: "with `upload`: the session id" },
773
+ {
774
+ name: "--transcript",
775
+ type: "string",
776
+ description: "with `upload`: the transcript file to ship"
684
777
  }
685
778
  ],
686
779
  examples: ["hook status", "hook install --dry-run"]
@@ -717,1398 +810,1415 @@ var CLI_SPEC = {
717
810
  ]
718
811
  };
719
812
 
720
- // packages/shared/src/couch-url.ts
721
- var DEFAULT_COUCHDB_HOST = "127.0.0.1";
722
- var DEFAULT_COUCHDB_PORT = "7652";
723
- function trimTrailingSlash(url) {
724
- return url.replace(/\/+$/, "");
725
- }
726
- function resolveCouchUrl(env = {}) {
727
- const explicit = env.COUCHDB_URL?.trim();
728
- if (explicit) {
729
- const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(explicit) ? explicit : `http://${explicit}`;
730
- return trimTrailingSlash(withScheme);
731
- }
732
- const host = env.COUCHDB_HOST || DEFAULT_COUCHDB_HOST;
733
- const port = env.COUCHDB_PORT || DEFAULT_COUCHDB_PORT;
734
- return `http://${host}:${port}`;
735
- }
736
- // packages/shared/src/highlight.ts
737
- var HIGHLIGHT_PRE = "\uE000";
738
- var HIGHLIGHT_POST = "\uE001";
739
- function stripHighlightMarks(marked) {
740
- return marked.replaceAll(HIGHLIGHT_PRE, "").replaceAll(HIGHLIGHT_POST, "");
741
- }
742
- // packages/shared/src/migrations/designs.ts
743
- var CHUNK_ENTRIES_MAP = `function (doc) {
744
- if (doc.type !== "chunk" || !doc.entries) return;
745
- for (var i = 0; i < doc.entries.length; i++) {
746
- var e = doc.entries[i];
747
- if (!e || !e.role) continue;
748
- emit([doc.session_id, doc.byte_start, i], {
749
- role: e.role,
750
- timestamp: e.timestamp || "",
751
- text: e.text || "",
752
- toolUses: e.toolUses || null,
753
- toolUseId: e.toolUseId || null,
754
- isError: e.isError || false,
755
- isSidechain: e.isSidechain || false,
756
- kind: e.kind || null
757
- });
758
- }
759
- }`;
760
- var CHUNKS_DESIGN = {
761
- _id: "_design/chunks",
762
- language: "javascript",
763
- views: {
764
- by_session: {
765
- map: 'function(doc){if(doc.type==="chunk"){emit([doc.session_id,doc.byte_start],{byte_start:doc.byte_start,byte_end:doc.byte_end,entry_count:doc.entry_count});}}'
766
- },
767
- entry_count_by_session: {
768
- map: 'function(doc){if(doc.type==="chunk"){emit(doc.session_id,doc.entry_count||0);}}',
769
- reduce: "_sum"
770
- },
771
- entries_by_session: { map: CHUNK_ENTRIES_MAP, reduce: "_count" }
772
- }
773
- };
774
- var INITIAL_DESIGNS = [
813
+ // packages/shared/src/blueprint/env.ts
814
+ var ENV_VARS = [
775
815
  {
776
- _id: "_design/sessions",
777
- language: "javascript",
778
- views: {
779
- by_date: {
780
- map: 'function(doc){if(doc.type==="summary"){var t=doc.timestamp.split(/[-T:]/);emit([parseInt(t[0]),parseInt(t[1]),parseInt(t[2])],{session_id:doc.session_id,event_count:doc.event_count,prompt_count:doc.prompt_count,error_count:doc.error_count,cwd:doc.cwd});}}',
781
- reduce: "_count"
782
- },
783
- by_cwd: {
784
- map: 'function(doc){if(doc.type==="summary"){emit([doc.cwd,doc.timestamp],{session_id:doc.session_id,event_count:doc.event_count,prompt_count:doc.prompt_count});}}',
785
- reduce: "_count"
786
- }
787
- }
816
+ name: "IMAGE_NS",
817
+ scope: "image",
818
+ description: "registry host/org for pulled images, e.g. ghcr.io/OWNER"
788
819
  },
820
+ { name: "COUCHDB_TAG", scope: "image", default: "3", description: "CouchDB image tag" },
821
+ { name: "GARAGE_TAG", scope: "image", default: "v2.3.0", description: "Garage image tag" },
822
+ { name: "GARAGE_UI_TAG", scope: "image", default: "1.1.0", description: "Garage UI image tag" },
823
+ { name: "FOSSIL_TAG", scope: "image", default: "2.28", description: "Fossil image tag" },
824
+ { name: "MEILI_TAG", scope: "image", default: "v1.10", description: "Meilisearch image tag" },
789
825
  {
790
- _id: "_design/events",
791
- language: "javascript",
792
- views: {
793
- by_session: {
794
- map: 'function(doc){if(doc.type==="event"){emit([doc.session_id,doc.timestamp],{event:doc.event,tool_name:doc.tool_name||null,input_preview:doc.input_preview||doc.prompt_preview||doc.error_preview||null});}}'
795
- },
796
- by_type: {
797
- map: 'function(doc){if(doc.type==="event"){var t=doc.timestamp.split(/[-T:]/);emit([doc.event,parseInt(t[0]),parseInt(t[1]),parseInt(t[2])],1);}}',
798
- reduce: "_count"
799
- }
800
- }
826
+ name: "MEILI_UI_TAG",
827
+ scope: "image",
828
+ default: "latest",
829
+ description: "Meilisearch UI image tag"
801
830
  },
831
+ { name: "APP_TAG", scope: "image", default: "latest", description: "app image tag" },
832
+ { name: "WEBAPI_PORT", scope: "port", default: "7650", description: "webapi host port" },
833
+ { name: "WEBUI_PORT", scope: "port", default: "7651", description: "webui dev-server host port" },
834
+ { name: "COUCHDB_PORT", scope: "port", default: "7652", description: "CouchDB host port" },
802
835
  {
803
- _id: "_design/tools",
804
- language: "javascript",
805
- views: {
806
- usage: {
807
- map: 'function(doc){if(doc.type==="event"&&doc.tool_name){var t=doc.timestamp.split(/[-T:]/);emit([doc.tool_name,parseInt(t[0]),parseInt(t[1]),parseInt(t[2])],1);}}',
808
- reduce: "_count"
809
- },
810
- failures: {
811
- map: 'function(doc){if(doc.event==="PostToolUseFailure"){emit([doc.tool_name,doc.timestamp],{session_id:doc.session_id,error_preview:doc.error_preview,cwd:doc.cwd});}}'
812
- },
813
- errors: {
814
- map: 'function(doc){if(doc.event==="PostToolUseFailure"||doc.error){emit([doc.tool_name||"unknown",doc.timestamp],{session_id:doc.session_id,error_preview:doc.error_preview||doc.error,cwd:doc.cwd});}}'
815
- }
816
- }
836
+ name: "GARAGE_S3_PORT",
837
+ scope: "port",
838
+ default: "7653",
839
+ description: "Garage S3 API host port"
817
840
  },
818
841
  {
819
- _id: "_design/activity",
820
- language: "javascript",
821
- views: {
822
- timeline: {
823
- map: 'function(doc){if(doc.type==="event"){var t=doc.timestamp.split(/[-T:]/);emit([parseInt(t[0]),parseInt(t[1]),parseInt(t[2]),parseInt(t[3])],1);}}',
824
- reduce: "_count"
825
- }
826
- }
842
+ name: "GARAGE_ADMIN_PORT",
843
+ scope: "port",
844
+ default: "7654",
845
+ description: "Garage admin API host port"
827
846
  },
828
- CHUNKS_DESIGN,
829
847
  {
830
- _id: "_design/session_meta",
831
- language: "javascript",
832
- views: {
833
- start_meta: {
834
- map: 'function(doc){if(doc.event==="SessionStart"){emit(doc.session_id,{timestamp:doc.timestamp,model:doc.model||"",cwd:doc.cwd||"",hostname:doc.hostname||""});}}'
835
- },
836
- tokens_by_date: {
837
- map: 'function(doc){if(doc.type==="summary"&&doc.token_usage){var t=doc.timestamp.split(/[-T:]/);var u=doc.token_usage;emit([parseInt(t[0]),parseInt(t[1]),parseInt(t[2])],{input:u.input||0,output:u.output||0,cacheCreation:u.cacheCreation||0,cacheRead:u.cacheRead||0,total:u.total||0,sessions:1});}}',
838
- reduce: "_sum"
839
- }
840
- }
841
- }
842
- ];
843
- // packages/shared/src/migrations/session-index.ts
844
- var AGGREGATE_MAP = `function (doc) {
845
- if (!doc.session_id) return;
846
- // A \`chunk\` doc contributes transcript coverage ONLY \u2014 no timestamps, no counts \u2014
847
- // so every other metric keeps the exact meaning it had before chunks were indexed.
848
- // \`chunkEntries\` counts turns the chunk view can actually serve (byte-range-only
849
- // chunks carry no \`entries[]\`), while \`chunkBytes\` tracks raw byte coverage.
850
- if (doc.type === "chunk") {
851
- emit(doc.session_id, {
852
- ended: 0, events: 0, prompts: 0, errors: 0, started: 0, tools: {},
853
- first: "", last: "", model: "", cwd: "", hostname: "",
854
- modelAt: "", cwdAt: "", hostnameAt: "", summary: null,
855
- chunkEntries: doc.entries ? doc.entries.length : 0,
856
- chunkBytes: doc.byte_end || 0
857
- });
858
- return;
859
- }
860
- if (doc.type !== "event" && doc.type !== "summary") return;
861
- var isSummary = doc.type === "summary";
862
- var ts = doc.timestamp || "";
863
- var model = (doc.event === "SessionStart" || isSummary) ? (doc.model || "") : "";
864
- var tools = {};
865
- if (doc.event === "PostToolUse" && doc.tool_name) tools[doc.tool_name] = 1;
866
- emit(doc.session_id, {
867
- ended: isSummary ? 1 : 0,
868
- events: doc.type === "event" ? 1 : 0,
869
- prompts: doc.event === "UserPromptSubmit" ? 1 : 0,
870
- errors: doc.event === "PostToolUseFailure" ? 1 : 0,
871
- started: doc.event === "SessionStart" ? 1 : 0,
872
- tools: tools,
873
- first: ts,
874
- last: ts,
875
- model: model,
876
- cwd: doc.cwd || "",
877
- hostname: doc.hostname || "",
878
- modelAt: model ? ts : "",
879
- cwdAt: doc.cwd ? ts : "",
880
- hostnameAt: doc.hostname ? ts : "",
881
- summary: isSummary ? {
882
- event_count: doc.event_count || 0,
883
- prompt_count: doc.prompt_count || 0,
884
- error_count: doc.error_count || 0,
885
- tool_counts: doc.tool_counts || {},
886
- end_reason: doc.end_reason || "",
887
- transcript_bytes: doc.transcript_bytes || 0,
888
- token_usage: doc.token_usage || null,
889
- timestamp: doc.timestamp || "",
890
- source: doc.source || ""
891
- } : null,
892
- chunkEntries: 0,
893
- chunkBytes: 0
894
- });
895
- }`;
896
- var AGGREGATE_REDUCE = `function (keys, values, rereduce) {
897
- var acc = { ended:0, events:0, prompts:0, errors:0, started:0, tools:{}, first:"", last:"", model:"", cwd:"", hostname:"", modelAt:"", cwdAt:"", hostnameAt:"", summary:null, chunkEntries:0, chunkBytes:0 };
898
- // Keep the earliest-stamped value; an unstamped one sorts last, ties go to the smaller value.
899
- function earliest(v, f) {
900
- var val = v[f];
901
- if (!val) return;
902
- var at = v[f + "At"] || "", a = at || "\\uffff", b = acc[f + "At"] || "\\uffff";
903
- if (acc[f] === "" || a < b || (a === b && val < acc[f])) { acc[f] = val; acc[f + "At"] = at; }
904
- }
905
- for (var i = 0; i < values.length; i++) {
906
- var v = values[i];
907
- if (!v) continue;
908
- acc.chunkEntries += v.chunkEntries || 0;
909
- if ((v.chunkBytes || 0) > acc.chunkBytes) acc.chunkBytes = v.chunkBytes;
910
- acc.ended += v.ended || 0;
911
- acc.events += v.events || 0;
912
- acc.prompts += v.prompts || 0;
913
- acc.errors += v.errors || 0;
914
- acc.started += v.started || 0;
915
- if (v.last && v.last > acc.last) acc.last = v.last;
916
- if (v.first && (acc.first === "" || v.first < acc.first)) acc.first = v.first;
917
- earliest(v, "model");
918
- earliest(v, "cwd");
919
- earliest(v, "hostname");
920
- // A session has one \`summary:<id>\` doc, so there is only ever one to keep.
921
- if (v.summary) acc.summary = v.summary;
922
- var t = v.tools || {};
923
- for (var k in t) { if (t.hasOwnProperty(k)) acc.tools[k] = (acc.tools[k] || 0) + t[k]; }
924
- }
925
- var names = Object.keys(acc.tools).sort(), tools = {};
926
- for (var j = 0; j < names.length; j++) tools[names[j]] = acc.tools[names[j]];
927
- acc.tools = tools;
928
- return acc;
929
- }`;
930
- var EVENT_TIMES_MAP = `function (doc) {
931
- if (!doc.session_id || !doc.timestamp) return;
932
- if (doc.type !== "event" && doc.type !== "summary") return;
933
- emit(doc.session_id, doc.timestamp);
934
- }`;
935
- var SESSION_INDEX_DESIGN = {
936
- _id: "_design/session_index",
937
- language: "javascript",
938
- views: {
939
- aggregate: { map: AGGREGATE_MAP, reduce: AGGREGATE_REDUCE },
940
- event_times: { map: EVENT_TIMES_MAP }
941
- }
942
- };
943
-
944
- // packages/shared/src/migrations/speaker-split.ts
945
- var BY_ROLE_MAP = `function (doc) {
946
- if (doc.type !== "chunk" || !doc.entries) return;
947
- for (var i = 0; i < doc.entries.length; i++) {
948
- var e = doc.entries[i];
949
- if (!e || !e.role) continue;
950
- emit([doc.session_id, e.role, doc.byte_start, i], {
951
- role: e.role,
952
- timestamp: e.timestamp || "",
953
- text: e.text || "",
954
- toolUses: e.toolUses || null,
955
- toolUseId: e.toolUseId || null,
956
- isError: e.isError || false
957
- });
958
- }
959
- }`;
960
- var BY_ROLE_TIME_MAP = `function (doc) {
961
- if (doc.type !== "chunk" || !doc.entries) return;
962
- for (var i = 0; i < doc.entries.length; i++) {
963
- var e = doc.entries[i];
964
- if (!e || !e.role) continue;
965
- emit([e.role, e.timestamp || "", doc.session_id, doc.byte_start, i], {
966
- sessionId: doc.session_id,
967
- cwd: doc.cwd || "",
968
- role: e.role,
969
- timestamp: e.timestamp || "",
970
- text: e.text || ""
971
- });
972
- }
973
- }`;
974
- var SPEAKER_SPLIT_DESIGN = {
975
- _id: "_design/speaker_split",
976
- language: "javascript",
977
- views: {
978
- by_role: { map: BY_ROLE_MAP, reduce: "_count" },
979
- by_role_time: { map: BY_ROLE_TIME_MAP, reduce: "_count" }
980
- }
981
- };
982
-
983
- // packages/shared/src/migrations/registry.ts
984
- var initialSchema = {
985
- id: 1,
986
- name: "initial-schema",
987
- async up(ctx) {
988
- for (const design of INITIAL_DESIGNS) {
989
- ctx.log(`+ ${design._id}`);
990
- const { _rev, ...body } = design;
991
- await ctx.putDoc(design._id, body);
992
- }
993
- },
994
- async down(ctx) {
995
- for (const design of INITIAL_DESIGNS) {
996
- ctx.log(`- ${design._id}`);
997
- await ctx.deleteDoc(design._id);
998
- }
999
- }
1000
- };
1001
- var sessionIndexView = {
1002
- id: 2,
1003
- name: "session-index-view",
1004
- async up(ctx) {
1005
- ctx.log(`+ ${SESSION_INDEX_DESIGN._id}`);
1006
- const { _rev, ...body } = SESSION_INDEX_DESIGN;
1007
- await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
1008
- },
1009
- async down(ctx) {
1010
- ctx.log(`- ${SESSION_INDEX_DESIGN._id}`);
1011
- await ctx.deleteDoc(SESSION_INDEX_DESIGN._id);
1012
- }
1013
- };
1014
- var sessionIndexSource = {
1015
- id: 3,
1016
- name: "session-index-source",
1017
- async up(ctx) {
1018
- ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (emit summary.source)`);
1019
- const { _rev, ...body } = SESSION_INDEX_DESIGN;
1020
- await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
1021
- },
1022
- async down(ctx) {
1023
- ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (source field left in place \u2014 additive)`);
1024
- }
1025
- };
1026
- var speakerSplitView = {
1027
- id: 4,
1028
- name: "speaker-split-view",
1029
- async up(ctx) {
1030
- ctx.log(`+ ${SPEAKER_SPLIT_DESIGN._id}`);
1031
- const { _rev, ...body } = SPEAKER_SPLIT_DESIGN;
1032
- await ctx.putDoc(SPEAKER_SPLIT_DESIGN._id, body);
1033
- },
1034
- async down(ctx) {
1035
- ctx.log(`- ${SPEAKER_SPLIT_DESIGN._id}`);
1036
- await ctx.deleteDoc(SPEAKER_SPLIT_DESIGN._id);
1037
- }
1038
- };
1039
- var speakerSplitTimeView = {
1040
- id: 5,
1041
- name: "speaker-split-time-view",
1042
- async up(ctx) {
1043
- ctx.log(`~ ${SPEAKER_SPLIT_DESIGN._id} (add by_role_time)`);
1044
- const { _rev, ...body } = SPEAKER_SPLIT_DESIGN;
1045
- await ctx.putDoc(SPEAKER_SPLIT_DESIGN._id, body);
1046
- },
1047
- async down(ctx) {
1048
- ctx.log(`~ ${SPEAKER_SPLIT_DESIGN._id} (by_role_time left in place \u2014 additive)`);
1049
- }
1050
- };
1051
- var chunkEntriesView = {
1052
- id: 6,
1053
- name: "chunk-entries-view",
1054
- async up(ctx) {
1055
- ctx.log(`~ ${CHUNKS_DESIGN._id} (add entries_by_session)`);
1056
- const { _rev, ...body } = CHUNKS_DESIGN;
1057
- await ctx.putDoc(CHUNKS_DESIGN._id, body);
1058
- },
1059
- async down(ctx) {
1060
- ctx.log(`~ ${CHUNKS_DESIGN._id} (entries_by_session left in place \u2014 additive)`);
1061
- }
1062
- };
1063
- var sessionIndexChunkCoverage = {
1064
- id: 7,
1065
- name: "session-index-chunk-coverage",
1066
- async up(ctx) {
1067
- ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (emit chunk coverage)`);
1068
- const { _rev, ...body } = SESSION_INDEX_DESIGN;
1069
- await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
1070
- },
1071
- async down(ctx) {
1072
- ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (chunk coverage left in place \u2014 additive)`);
1073
- }
1074
- };
1075
- var chunkEntryKind = {
1076
- id: 8,
1077
- name: "chunk-entry-kind",
1078
- async up(ctx) {
1079
- ctx.log(`~ ${CHUNKS_DESIGN._id} (emit entry kind)`);
1080
- const { _rev, ...body } = CHUNKS_DESIGN;
1081
- await ctx.putDoc(CHUNKS_DESIGN._id, body);
848
+ name: "GARAGE_WEBUI_PORT",
849
+ scope: "port",
850
+ default: "7655",
851
+ description: "Garage web UI host port"
1082
852
  },
1083
- async down(ctx) {
1084
- ctx.log(`~ ${CHUNKS_DESIGN._id} (kind left in place \u2014 additive)`);
1085
- }
1086
- };
1087
- var sessionIndexEventTimes = {
1088
- id: 9,
1089
- name: "session-index-event-times",
1090
- async up(ctx) {
1091
- ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (add event_times view)`);
1092
- const { _rev, ...body } = SESSION_INDEX_DESIGN;
1093
- await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
853
+ { name: "MEILI_PORT", scope: "port", default: "7656", description: "Meilisearch host port" },
854
+ {
855
+ name: "MEILI_UI_PORT",
856
+ scope: "port",
857
+ default: "7657",
858
+ description: "Meilisearch UI host port"
1094
859
  },
1095
- async down(ctx) {
1096
- ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (event_times left in place \u2014 additive)`);
1097
- }
1098
- };
1099
- var sessionIndexDeterministic = {
1100
- id: 10,
1101
- name: "session-index-deterministic",
1102
- async up(ctx) {
1103
- ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (earliest model/cwd/hostname)`);
1104
- const { _rev, ...body } = SESSION_INDEX_DESIGN;
1105
- await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
860
+ {
861
+ name: "FOSSIL_PORT",
862
+ scope: "port",
863
+ default: "7658",
864
+ description: "Fossil web UI + sync host port"
1106
865
  },
1107
- async down(ctx) {
1108
- ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (deterministic pick left in place)`);
1109
- }
1110
- };
1111
- var MIGRATIONS = [
1112
- initialSchema,
1113
- sessionIndexView,
1114
- sessionIndexSource,
1115
- speakerSplitView,
1116
- speakerSplitTimeView,
1117
- chunkEntriesView,
1118
- sessionIndexChunkCoverage,
1119
- chunkEntryKind,
1120
- sessionIndexEventTimes,
1121
- sessionIndexDeterministic
1122
- ];
1123
- function latestVersion() {
1124
- return MIGRATIONS.reduce((max, m) => Math.max(max, m.id), 0);
1125
- }
1126
- function docTransformsBetween(from, to, migrations = MIGRATIONS) {
1127
- return migrations.filter((m) => m.id > from && m.id <= to && m.transformsDocs === true).sort((a, b) => a.id - b.id).map((m) => ({ id: m.id, name: m.name }));
1128
- }
1129
- // packages/shared/src/model/actions.ts
1130
- var ACTIONS = [
866
+ { name: "WEBAPI_HOST", scope: "host", default: "127.0.0.1", description: "webapi bind host" },
867
+ { name: "WEBUI_HOST", scope: "host", default: "127.0.0.1", description: "webui bind host" },
868
+ { name: "CT_STATIC_DIR", scope: "host", description: "prebuilt SPA dir (prod image only)" },
869
+ { name: "CT_VERSION", scope: "host", description: "baked release version" },
870
+ {
871
+ name: "COUCHDB_URL",
872
+ scope: "endpoint",
873
+ description: "full CouchDB base URL; wins over COUCHDB_HOST/PORT (https + path prefix OK)"
874
+ },
875
+ { name: "COUCHDB_HOST", scope: "endpoint", default: "127.0.0.1", description: "CouchDB host" },
876
+ { name: "COUCHDB_USER", scope: "secret", description: "CouchDB admin (blank in bundled dev)" },
1131
877
  {
1132
- key: "write-event-marker",
1133
- summary: "Append a light append-only event marker doc",
1134
- implemented: true
878
+ name: "COUCHDB_PASSWORD",
879
+ scope: "secret",
880
+ description: "CouchDB password (blank in bundled dev)"
1135
881
  },
1136
882
  {
1137
- key: "update-counts",
1138
- summary: "Bump per-session counters (events/prompts/errors/tools)",
1139
- implemented: true
883
+ name: "S3_ENDPOINT",
884
+ scope: "endpoint",
885
+ default: "http://127.0.0.1:7653",
886
+ description: "S3 endpoint URL"
1140
887
  },
888
+ { name: "S3_REGION", scope: "endpoint", default: "garage", description: "S3 region" },
889
+ { name: "S3_ACCESS_KEY", scope: "secret", description: "app-facing S3 access key" },
890
+ { name: "S3_SECRET_KEY", scope: "secret", description: "app-facing S3 secret key" },
891
+ { name: "GARAGE_RPC_SECRET", scope: "secret", description: "Garage internal RPC secret" },
892
+ { name: "GARAGE_ADMIN_TOKEN", scope: "secret", description: "Garage admin token" },
893
+ { name: "GARAGE_METRICS_TOKEN", scope: "secret", description: "Garage metrics token" },
1141
894
  {
1142
- key: "flush-transcript-chunk",
1143
- summary: "Tail the transcript \u2192 append chunk docs",
1144
- implemented: true
895
+ name: "MEILI_HOST",
896
+ scope: "endpoint",
897
+ default: "http://127.0.0.1:7656",
898
+ description: "Meilisearch endpoint URL"
1145
899
  },
1146
- { key: "write-summary", summary: "Write summary:<id> at session end", implemented: true },
1147
900
  {
1148
- key: "upload-blobs",
1149
- summary: "Upload summary.json + transcript.jsonl to S3",
1150
- implemented: true
901
+ name: "FOSSIL_URL",
902
+ scope: "endpoint",
903
+ default: "http://127.0.0.1:7658",
904
+ description: "Fossil server base URL (serves each repository at /<name>/)"
1151
905
  },
1152
906
  {
1153
- key: "seed-session-start",
1154
- summary: "Reset counters + seed the chunk offset at start",
1155
- implemented: true
907
+ name: "MEILI_API_KEY",
908
+ scope: "secret",
909
+ description: "Meilisearch master key (blank in bundled dev)"
910
+ }
911
+ ];
912
+
913
+ // packages/shared/src/blueprint/hooks.ts
914
+ var HOOK_TYPES = [
915
+ {
916
+ event: "SessionStart",
917
+ category: "session-start",
918
+ canBlock: false,
919
+ wired: true,
920
+ summary: "A session begins, resumes, is cleared, or restarts after compaction \u2014 `source` \u2208 `startup`/`resume`/`clear`/`compact`. The per-session entry point."
1156
921
  },
1157
922
  {
1158
- key: "announce-recording",
1159
- summary: "Tell the user, in the transcript, whether and where this session is recorded",
1160
- implemented: true
923
+ event: "Setup",
924
+ category: "session-start",
925
+ canBlock: false,
926
+ wired: false,
927
+ ignoreReason: "Env/setup signal, not session activity.",
928
+ summary: "The CLI runs in init/maintenance mode (e.g. `--init-only`) \u2014 one-time environment setup."
1161
929
  },
1162
930
  {
1163
- key: "inject-recall-policy",
1164
- summary: "Prime the session with the recall policy + how much history this cwd has",
1165
- implemented: true
931
+ event: "InstructionsLoaded",
932
+ category: "session-start",
933
+ canBlock: false,
934
+ wired: false,
935
+ ignoreReason: "Instruction-load audit \u2014 out of Tier-1 scope.",
936
+ summary: "`CLAUDE.md` / `.claude/rules/*.md` instruction files are loaded \u2014 `load_reason` records why. Observability of which instructions applied."
1166
937
  },
1167
- { key: "enrich-metadata", summary: "Post additional session metadata", implemented: false },
1168
938
  {
1169
- key: "extract-feature",
1170
- summary: "Map-reduce feature extraction (urls/repos/PRs/\u2026)",
1171
- implemented: false
939
+ event: "UserPromptSubmit",
940
+ category: "turn-input",
941
+ canBlock: true,
942
+ wired: true,
943
+ summary: "The user submits a prompt, before Claude sees it. Can block or inject context."
1172
944
  },
1173
- { key: "detect-memory-write", summary: "Flag Edit/Write to memory paths", implemented: false },
1174
- { key: "app-log", summary: "Emit an operational log record (app-logs DB)", implemented: false },
1175
945
  {
1176
- key: "index-for-search",
1177
- summary: "Push derived content to the search backend",
1178
- implemented: false
946
+ event: "UserPromptExpansion",
947
+ category: "turn-input",
948
+ canBlock: true,
949
+ wired: false,
950
+ ignoreReason: "Slash-command expansion \u2014 redundant with `UserPromptSubmit`.",
951
+ summary: "A slash command / skill prompt is expanded/templated before use."
1179
952
  },
1180
953
  {
1181
- key: "reconcile-session",
1182
- summary: "Finalize a stale running session from chunks/S3",
1183
- implemented: false
1184
- }
1185
- ];
1186
- var BINDINGS = [
954
+ event: "PreToolUse",
955
+ category: "tool",
956
+ canBlock: true,
957
+ wired: false,
958
+ ignoreReason: "Blocking pre-hook; an observe-only writer gains nothing \u2014 `PostToolUse` captures the outcome.",
959
+ summary: "Before a tool executes \u2014 `tool_name` + `tool_input` available. Can block or modify the input."
960
+ },
1187
961
  {
1188
- event: "SessionStart",
1189
- actions: [
1190
- "seed-session-start",
1191
- "write-event-marker",
1192
- "announce-recording",
1193
- "inject-recall-policy"
1194
- ]
962
+ event: "PermissionRequest",
963
+ category: "tool",
964
+ canBlock: true,
965
+ wired: false,
966
+ ignoreReason: "Permission-dialog control hook, not session history.",
967
+ summary: "A permission dialog is about to be shown for a tool call."
1195
968
  },
1196
969
  {
1197
- event: "UserPromptSubmit",
1198
- actions: ["write-event-marker", "update-counts", "flush-transcript-chunk"]
970
+ event: "PermissionDenied",
971
+ category: "tool",
972
+ canBlock: false,
973
+ wired: false,
974
+ ignoreReason: "Permission control event, not session history.",
975
+ summary: "A tool call was denied (auto-classifier or user)."
1199
976
  },
1200
977
  {
1201
978
  event: "PostToolUse",
1202
- actions: ["write-event-marker", "update-counts", "flush-transcript-chunk"]
979
+ category: "tool",
980
+ canBlock: false,
981
+ wired: true,
982
+ summary: "A tool succeeds \u2014 `tool_output` available. Can post-process the result."
1203
983
  },
1204
984
  {
1205
985
  event: "PostToolUseFailure",
1206
- actions: ["write-event-marker", "update-counts", "flush-transcript-chunk"]
1207
- },
1208
- { event: "Stop", actions: ["write-event-marker", "flush-transcript-chunk"] },
1209
- { event: "SubagentStart", actions: ["write-event-marker", "update-counts"] },
1210
- { event: "SubagentStop", actions: ["write-event-marker", "update-counts"] },
1211
- { event: "StopFailure", actions: ["write-event-marker", "update-counts"] },
1212
- { event: "PreCompact", actions: ["write-event-marker"] },
1213
- { event: "PostCompact", actions: ["write-event-marker"] },
1214
- { event: "SessionEnd", actions: ["flush-transcript-chunk", "write-summary", "upload-blobs"] }
1215
- ];
1216
- // packages/shared/src/model/env.ts
1217
- var ENV_VARS = [
1218
- {
1219
- name: "IMAGE_NS",
1220
- scope: "image",
1221
- description: "registry host/org for pulled images, e.g. ghcr.io/OWNER"
986
+ category: "tool",
987
+ canBlock: false,
988
+ wired: true,
989
+ summary: "A tool fails \u2014 `error_message` available."
1222
990
  },
1223
- { name: "COUCHDB_TAG", scope: "image", default: "3", description: "CouchDB image tag" },
1224
- { name: "GARAGE_TAG", scope: "image", default: "v2.3.0", description: "Garage image tag" },
1225
- { name: "GARAGE_UI_TAG", scope: "image", default: "1.1.0", description: "Garage UI image tag" },
1226
- { name: "MEILI_TAG", scope: "image", default: "v1.10", description: "Meilisearch image tag" },
1227
991
  {
1228
- name: "MEILI_UI_TAG",
1229
- scope: "image",
1230
- default: "latest",
1231
- description: "Meilisearch UI image tag"
992
+ event: "PostToolBatch",
993
+ category: "tool",
994
+ canBlock: true,
995
+ wired: false,
996
+ ignoreReason: "Batch boundary; the individual `PostToolUse` events are already captured.",
997
+ summary: "A batch of parallel tool calls resolves."
1232
998
  },
1233
- { name: "APP_TAG", scope: "image", default: "latest", description: "app image tag" },
1234
- { name: "WEBAPI_PORT", scope: "port", default: "7650", description: "webapi host port" },
1235
- { name: "WEBUI_PORT", scope: "port", default: "7651", description: "webui dev-server host port" },
1236
- { name: "COUCHDB_PORT", scope: "port", default: "7652", description: "CouchDB host port" },
1237
999
  {
1238
- name: "GARAGE_S3_PORT",
1239
- scope: "port",
1240
- default: "7653",
1241
- description: "Garage S3 API host port"
1000
+ event: "SubagentStart",
1001
+ category: "subagent",
1002
+ canBlock: false,
1003
+ wired: true,
1004
+ summary: "A subagent is spawned \u2014 `agent_type`, `agent_id`, `task_description`."
1242
1005
  },
1243
1006
  {
1244
- name: "GARAGE_ADMIN_PORT",
1245
- scope: "port",
1246
- default: "7654",
1247
- description: "Garage admin API host port"
1007
+ event: "SubagentStop",
1008
+ category: "subagent",
1009
+ canBlock: true,
1010
+ wired: true,
1011
+ summary: "A subagent finishes \u2014 `summary` of its work."
1248
1012
  },
1249
1013
  {
1250
- name: "GARAGE_WEBUI_PORT",
1251
- scope: "port",
1252
- default: "7655",
1253
- description: "Garage web UI host port"
1014
+ event: "TeammateIdle",
1015
+ category: "subagent",
1016
+ canBlock: true,
1017
+ wired: false,
1018
+ ignoreReason: "Team orchestration, not session history.",
1019
+ summary: "A team teammate goes idle."
1254
1020
  },
1255
- { name: "MEILI_PORT", scope: "port", default: "7656", description: "Meilisearch host port" },
1256
1021
  {
1257
- name: "MEILI_UI_PORT",
1258
- scope: "port",
1259
- default: "7657",
1260
- description: "Meilisearch UI host port"
1022
+ event: "TaskCreated",
1023
+ category: "subagent",
1024
+ canBlock: true,
1025
+ wired: false,
1026
+ ignoreReason: "Task orchestration, not session history.",
1027
+ summary: "A task is created via `TaskCreate`."
1261
1028
  },
1262
- { name: "WEBAPI_HOST", scope: "host", default: "127.0.0.1", description: "webapi bind host" },
1263
- { name: "WEBUI_HOST", scope: "host", default: "127.0.0.1", description: "webui bind host" },
1264
- { name: "CT_STATIC_DIR", scope: "host", description: "prebuilt SPA dir (prod image only)" },
1265
- { name: "CT_VERSION", scope: "host", description: "baked release version" },
1266
1029
  {
1267
- name: "COUCHDB_URL",
1268
- scope: "endpoint",
1269
- description: "full CouchDB base URL; wins over COUCHDB_HOST/PORT (https + path prefix OK)"
1030
+ event: "TaskCompleted",
1031
+ category: "subagent",
1032
+ canBlock: true,
1033
+ wired: false,
1034
+ ignoreReason: "Task orchestration, not session history.",
1035
+ summary: "A task is marked complete."
1270
1036
  },
1271
- { name: "COUCHDB_HOST", scope: "endpoint", default: "127.0.0.1", description: "CouchDB host" },
1272
- { name: "COUCHDB_USER", scope: "secret", description: "CouchDB admin (blank in bundled dev)" },
1273
1037
  {
1274
- name: "COUCHDB_PASSWORD",
1275
- scope: "secret",
1276
- description: "CouchDB password (blank in bundled dev)"
1038
+ event: "MessageDisplay",
1039
+ category: "display",
1040
+ canBlock: false,
1041
+ wired: false,
1042
+ ignoreReason: "UX event; the message content is already in the transcript.",
1043
+ summary: "An assistant message is streamed/displayed to the user."
1277
1044
  },
1278
1045
  {
1279
- name: "S3_ENDPOINT",
1280
- scope: "endpoint",
1281
- default: "http://127.0.0.1:7653",
1282
- description: "S3 endpoint URL"
1046
+ event: "Elicitation",
1047
+ category: "display",
1048
+ canBlock: true,
1049
+ wired: false,
1050
+ ignoreReason: "MCP interaction control hook; an observe-only writer gains nothing.",
1051
+ summary: "An MCP server requests user input."
1283
1052
  },
1284
- { name: "S3_REGION", scope: "endpoint", default: "garage", description: "S3 region" },
1285
- { name: "S3_ACCESS_KEY", scope: "secret", description: "app-facing S3 access key" },
1286
- { name: "S3_SECRET_KEY", scope: "secret", description: "app-facing S3 secret key" },
1287
- { name: "GARAGE_RPC_SECRET", scope: "secret", description: "Garage internal RPC secret" },
1288
- { name: "GARAGE_ADMIN_TOKEN", scope: "secret", description: "Garage admin token" },
1289
- { name: "GARAGE_METRICS_TOKEN", scope: "secret", description: "Garage metrics token" },
1290
1053
  {
1291
- name: "MEILI_HOST",
1292
- scope: "endpoint",
1293
- default: "http://127.0.0.1:7656",
1294
- description: "Meilisearch endpoint URL"
1054
+ event: "ElicitationResult",
1055
+ category: "display",
1056
+ canBlock: true,
1057
+ wired: false,
1058
+ ignoreReason: "MCP interaction event; the content is in the transcript.",
1059
+ summary: "The user responds to an elicitation."
1295
1060
  },
1296
1061
  {
1297
- name: "MEILI_API_KEY",
1298
- scope: "secret",
1299
- description: "Meilisearch master key (blank in bundled dev)"
1300
- }
1301
- ];
1302
-
1303
- // packages/shared/src/model/hooks.ts
1304
- var HOOK_TYPES = [
1305
- {
1306
- event: "SessionStart",
1307
- category: "session-start",
1062
+ event: "Notification",
1063
+ category: "display",
1308
1064
  canBlock: false,
1309
- wired: true,
1310
- summary: "A session begins, resumes, is cleared, or restarts after compaction \u2014 `source` \u2208 `startup`/`resume`/`clear`/`compact`. The per-session entry point."
1065
+ wired: false,
1066
+ ignoreReason: "UX notification, not session history.",
1067
+ summary: "Claude Code emits a notification (`permission_prompt`, `idle_prompt`, `auth_success`, \u2026)."
1311
1068
  },
1312
1069
  {
1313
- event: "Setup",
1314
- category: "session-start",
1070
+ event: "CwdChanged",
1071
+ category: "environment",
1315
1072
  canBlock: false,
1316
1073
  wired: false,
1317
- ignoreReason: "Env/setup signal, not session activity.",
1318
- summary: "The CLI runs in init/maintenance mode (e.g. `--init-only`) \u2014 one-time environment setup."
1074
+ ignoreReason: "Host signal, out of session scope.",
1075
+ summary: "The working directory changes during a session."
1319
1076
  },
1320
1077
  {
1321
- event: "InstructionsLoaded",
1322
- category: "session-start",
1078
+ event: "FileChanged",
1079
+ category: "environment",
1323
1080
  canBlock: false,
1324
1081
  wired: false,
1325
- ignoreReason: "Instruction-load audit \u2014 out of Tier-1 scope.",
1326
- summary: "`CLAUDE.md` / `.claude/rules/*.md` instruction files are loaded \u2014 `load_reason` records why. Observability of which instructions applied."
1082
+ ignoreReason: "Watcher signal, out of session scope.",
1083
+ summary: "A watched file changes on disk."
1327
1084
  },
1328
1085
  {
1329
- event: "UserPromptSubmit",
1330
- category: "turn-input",
1086
+ event: "ConfigChange",
1087
+ category: "environment",
1331
1088
  canBlock: true,
1332
- wired: true,
1333
- summary: "The user submits a prompt, before Claude sees it. Can block or inject context."
1089
+ wired: false,
1090
+ ignoreReason: "Config-change signal, out of session scope.",
1091
+ summary: "A settings/config/skills file changes during the session (`user_settings`, `project_settings`, \u2026)."
1334
1092
  },
1335
1093
  {
1336
- event: "UserPromptExpansion",
1337
- category: "turn-input",
1094
+ event: "WorktreeCreate",
1095
+ category: "worktree",
1338
1096
  canBlock: true,
1339
1097
  wired: false,
1340
- ignoreReason: "Slash-command expansion \u2014 redundant with `UserPromptSubmit`.",
1341
- summary: "A slash command / skill prompt is expanded/templated before use."
1098
+ ignoreReason: "Git-worktree lifecycle, irrelevant to session logging.",
1099
+ summary: "A git worktree is created (`--worktree`)."
1342
1100
  },
1343
1101
  {
1344
- event: "PreToolUse",
1345
- category: "tool",
1346
- canBlock: true,
1102
+ event: "WorktreeRemove",
1103
+ category: "worktree",
1104
+ canBlock: false,
1347
1105
  wired: false,
1348
- ignoreReason: "Blocking pre-hook; an observe-only writer gains nothing \u2014 `PostToolUse` captures the outcome.",
1349
- summary: "Before a tool executes \u2014 `tool_name` + `tool_input` available. Can block or modify the input."
1106
+ ignoreReason: "Git-worktree lifecycle, irrelevant to session logging.",
1107
+ summary: "A git worktree is removed."
1350
1108
  },
1351
1109
  {
1352
- event: "PermissionRequest",
1353
- category: "tool",
1110
+ event: "PreCompact",
1111
+ category: "compaction",
1354
1112
  canBlock: true,
1355
- wired: false,
1356
- ignoreReason: "Permission-dialog control hook, not session history.",
1357
- summary: "A permission dialog is about to be shown for a tool call."
1113
+ wired: true,
1114
+ summary: "Before context compaction \u2014 `trigger` \u2208 `manual`/`auto`."
1358
1115
  },
1359
1116
  {
1360
- event: "PermissionDenied",
1361
- category: "tool",
1117
+ event: "PostCompact",
1118
+ category: "compaction",
1362
1119
  canBlock: false,
1363
- wired: false,
1364
- ignoreReason: "Permission control event, not session history.",
1365
- summary: "A tool call was denied (auto-classifier or user)."
1120
+ wired: true,
1121
+ summary: "After compaction completes \u2014 `trigger` \u2208 `manual`/`auto`."
1366
1122
  },
1367
1123
  {
1368
- event: "PostToolUse",
1369
- category: "tool",
1124
+ event: "Stop",
1125
+ category: "turn-end",
1126
+ canBlock: true,
1127
+ wired: true,
1128
+ summary: "Claude finishes responding to a turn \u2014 `turn_number`, `assistant_message`."
1129
+ },
1130
+ {
1131
+ event: "StopFailure",
1132
+ category: "turn-end",
1370
1133
  canBlock: false,
1371
1134
  wired: true,
1372
- summary: "A tool succeeds \u2014 `tool_output` available. Can post-process the result."
1135
+ summary: "A turn ends with an API error \u2014 `rate_limit`, `overloaded`, `max_output_tokens`, \u2026 A turn-level failure (see the crash note in the doc)."
1373
1136
  },
1374
1137
  {
1375
- event: "PostToolUseFailure",
1376
- category: "tool",
1138
+ event: "SessionEnd",
1139
+ category: "session-end",
1377
1140
  canBlock: false,
1378
1141
  wired: true,
1379
- summary: "A tool fails \u2014 `error_message` available."
1142
+ summary: "The session terminates \u2014 `reason` \u2208 `clear`/`resume`/`logout`/`prompt_input_exit`/`bypass_permissions_disabled`/`other`."
1143
+ }
1144
+ ];
1145
+ function hookLaunch(command) {
1146
+ return `cd / && ${command}`;
1147
+ }
1148
+ var PLUGIN_HOOK_COMMAND = hookLaunch('bun run "${CLAUDE_PLUGIN_ROOT}/scripts/dispatch.ts"');
1149
+
1150
+ // packages/shared/src/blueprint/recall.ts
1151
+ var RECALL_MODES = ["off", "suggest", "auto"];
1152
+ var RECALL_SCOPES = ["project", "host", "all"];
1153
+ var DEFAULT_RECALL = {
1154
+ mode: "auto",
1155
+ scope: "project",
1156
+ maxResults: 5,
1157
+ maxSnippetChars: 400,
1158
+ triggers: { priorWorkQuestion: true, repeatedError: true, beforeRederiving: true },
1159
+ excludeCwdGlobs: [],
1160
+ primer: { onSessionStart: true, maxTokens: 200 }
1161
+ };
1162
+ var RECALL_ENV = {
1163
+ mode: "CLAUDE_PLUGIN_OPTION_RECALL_MODE",
1164
+ scope: "CLAUDE_PLUGIN_OPTION_RECALL_SCOPE",
1165
+ maxResults: "CLAUDE_PLUGIN_OPTION_MAX_RESULTS"
1166
+ };
1167
+ function resolveRecall(fromConfig, env = {}) {
1168
+ const c = fromConfig ?? {};
1169
+ const merged = {
1170
+ ...DEFAULT_RECALL,
1171
+ ...stripUndefined(c),
1172
+ triggers: { ...DEFAULT_RECALL.triggers, ...stripUndefined(c.triggers ?? {}) },
1173
+ primer: { ...DEFAULT_RECALL.primer, ...stripUndefined(c.primer ?? {}) },
1174
+ excludeCwdGlobs: c.excludeCwdGlobs ?? DEFAULT_RECALL.excludeCwdGlobs
1175
+ };
1176
+ const mode = env[RECALL_ENV.mode];
1177
+ if (mode && RECALL_MODES.includes(mode))
1178
+ merged.mode = mode;
1179
+ const scope = env[RECALL_ENV.scope];
1180
+ if (scope && RECALL_SCOPES.includes(scope))
1181
+ merged.scope = scope;
1182
+ const max = Number(env[RECALL_ENV.maxResults]);
1183
+ if (Number.isInteger(max) && max > 0)
1184
+ merged.maxResults = max;
1185
+ return merged;
1186
+ }
1187
+ function stripUndefined(o) {
1188
+ const out = {};
1189
+ for (const [k, v] of Object.entries(o))
1190
+ if (v !== undefined)
1191
+ out[k] = v;
1192
+ return out;
1193
+ }
1194
+ function scopeFlag(scope, cwd, hostname) {
1195
+ switch (scope) {
1196
+ case "project":
1197
+ return `--cwd "${cwd}"`;
1198
+ case "host":
1199
+ return `--hostname "${hostname}"`;
1200
+ default:
1201
+ return "";
1202
+ }
1203
+ }
1204
+ function daysAgo(iso, now) {
1205
+ if (!iso)
1206
+ return "";
1207
+ const d = Math.floor((now - Date.parse(iso)) / 86400000);
1208
+ if (!Number.isFinite(d))
1209
+ return "";
1210
+ return d <= 0 ? "today" : d === 1 ? "yesterday" : `${d} days ago`;
1211
+ }
1212
+ function recallPrimer(policy, facts, where, now = Date.now()) {
1213
+ if (policy.mode === "off" || !policy.primer.onSessionStart)
1214
+ return null;
1215
+ if (facts.sessionCount <= 0)
1216
+ return null;
1217
+ const scopeWord = { project: "this project", host: "this host", all: "all projects" }[policy.scope];
1218
+ const recent = daysAgo(facts.mostRecent, now);
1219
+ const flag = scopeFlag(policy.scope, where.cwd, where.hostname);
1220
+ const when = [];
1221
+ if (policy.triggers.priorWorkQuestion)
1222
+ when.push("a question about why existing code or a decision is the way it is");
1223
+ if (policy.triggers.repeatedError)
1224
+ when.push("an error that looks familiar");
1225
+ if (policy.triggers.beforeRederiving)
1226
+ when.push("re-deriving something that looks like prior work here");
1227
+ const action = policy.mode === "auto" ? "search history first" : "offer to search history, and search if the user agrees";
1228
+ return [
1229
+ `Session history for ${scopeWord} is available via \`claude-transcripts search "<query>" ${flag} --json --limit ${policy.maxResults}\`.`,
1230
+ `${facts.sessionCount} recorded session${facts.sessionCount === 1 ? "" : "s"} in scope${recent ? `, most recent ${recent}` : ""}.`,
1231
+ when.length ? `Before answering ${joinOr(when)}, ${action}.` : "",
1232
+ `Cite the session id and date. Snippets only, never a whole transcript; max ${policy.maxResults} results. See the \`recall\` skill.`
1233
+ ].filter(Boolean).join(" ");
1234
+ }
1235
+ function joinOr(items) {
1236
+ if (items.length <= 1)
1237
+ return items[0] ?? "";
1238
+ return `${items.slice(0, -1).join(", ")}, or ${items[items.length - 1]}`;
1239
+ }
1240
+
1241
+ // packages/shared/src/blueprint/routes.ts
1242
+ var ROUTES = [
1243
+ {
1244
+ path: "/",
1245
+ serves: "machine-readable app manifest (agent entrypoint)",
1246
+ nav: { label: "App manifest", section: "Reference" }
1380
1247
  },
1381
1248
  {
1382
- event: "PostToolBatch",
1383
- category: "tool",
1384
- canBlock: true,
1385
- wired: false,
1386
- ignoreReason: "Batch boundary; the individual `PostToolUse` events are already captured.",
1387
- summary: "A batch of parallel tool calls resolves."
1249
+ path: "/health",
1250
+ serves: "liveness + store reachability",
1251
+ nav: { label: "Health", section: "Reference" }
1252
+ },
1253
+ { path: "/api", serves: "app JSON API (the gateway)" },
1254
+ {
1255
+ path: "/api/docs",
1256
+ serves: "Scalar API reference (renders the OpenAPI spec)",
1257
+ nav: { label: "API reference", section: "Reference" }
1258
+ },
1259
+ {
1260
+ path: "/api/openapi.json",
1261
+ serves: "OpenAPI spec (contract source of truth)",
1262
+ nav: { label: "OpenAPI spec", section: "Reference" }
1263
+ },
1264
+ { path: "/api/couch/*", serves: "read-only CouchDB proxy" },
1265
+ { path: "/api/s3/*", serves: "read-only S3 proxy" },
1266
+ { path: "/api/fossil/*", serves: "read-only Fossil JSON API proxy" },
1267
+ { path: "/api/sessions", serves: "session list / detail / transcript / turns" },
1268
+ { path: "/api/turns", serves: "turns across all sessions, time-ordered by speaker" },
1269
+ { path: "/api/search", serves: "full-text search over sessions + conversation content" },
1270
+ {
1271
+ path: "/api/ingest",
1272
+ serves: "curated write surface (summary/events/chunks/transcript, reset)"
1273
+ },
1274
+ { path: "/api/migrate", serves: "schema migration status / up / down" },
1275
+ {
1276
+ path: "/api/blueprint",
1277
+ serves: "app blueprint introspection (services/hooks/actions/env)",
1278
+ nav: { label: "App blueprint", section: "Reference" }
1279
+ },
1280
+ { path: "/app", serves: "webui SPA" },
1281
+ {
1282
+ path: "/docs",
1283
+ serves: "rendered technical docs",
1284
+ nav: { label: "Technical docs", section: "Reference" }
1285
+ },
1286
+ {
1287
+ path: "/cli/download",
1288
+ serves: "the bundled CLI binary",
1289
+ nav: { label: "Download CLI", section: "Install" }
1290
+ }
1291
+ ];
1292
+
1293
+ // packages/shared/src/blueprint/services.ts
1294
+ var RELEASE_IMAGE_NS = "ghcr.io/vredchenko";
1295
+ var SERVICES = [
1296
+ {
1297
+ key: "webapi",
1298
+ name: "webapi",
1299
+ role: "gateway",
1300
+ ports: [{ internal: 7650, hostEnv: "WEBAPI_PORT", defaultHost: 7650 }],
1301
+ runsOnHostInDev: true,
1302
+ notes: "The I/O gateway + stability column. In deploy it runs as the `app` image."
1303
+ },
1304
+ {
1305
+ key: "webui",
1306
+ name: "webui",
1307
+ role: "webui",
1308
+ ports: [{ internal: 7651, hostEnv: "WEBUI_PORT", defaultHost: 7651 }],
1309
+ runsOnHostInDev: true,
1310
+ notes: "Vite dev server in dev; built SPA served by the app at /app in prod."
1311
+ },
1312
+ {
1313
+ key: "cli",
1314
+ name: "cli",
1315
+ role: "cli",
1316
+ runsOnHostInDev: true,
1317
+ notes: "User-facing CLI; no port."
1318
+ },
1319
+ {
1320
+ key: "couchdb",
1321
+ name: "CouchDB",
1322
+ role: "backing",
1323
+ image: { name: "couchdb", tagEnv: "COUCHDB_TAG", defaultTag: "3", upstream: "couchdb" },
1324
+ ports: [
1325
+ { internal: 5984, hostEnv: "COUCHDB_PORT", defaultHost: 7652, label: "HTTP API + Fauxton" }
1326
+ ],
1327
+ adminUiServiceKey: "couchdbFauxton",
1328
+ adminUiPath: "/_utils/",
1329
+ holds: "databases",
1330
+ volumes: [{ host: "./data/couchdb", container: "/opt/couchdb/data" }],
1331
+ containerEnv: {
1332
+ COUCHDB_USER: "${COUCHDB_USER:-admin}",
1333
+ COUCHDB_PASSWORD: "${COUCHDB_PASSWORD:-admin}"
1334
+ },
1335
+ healthcheck: { test: ["CMD", "curl", "-f", "http://localhost:5984/_up"] },
1336
+ notes: "Source of truth. Bundled dev = fixed default admin (CouchDB 3 requires one)."
1388
1337
  },
1389
1338
  {
1390
- event: "SubagentStart",
1391
- category: "subagent",
1392
- canBlock: false,
1393
- wired: true,
1394
- summary: "A subagent is spawned \u2014 `agent_type`, `agent_id`, `task_description`."
1339
+ key: "garage",
1340
+ name: "Garage (S3)",
1341
+ role: "backing",
1342
+ image: {
1343
+ name: "garage",
1344
+ tagEnv: "GARAGE_TAG",
1345
+ defaultTag: "v2.3.0",
1346
+ upstream: "dxflrs/garage"
1347
+ },
1348
+ ports: [
1349
+ { internal: 3900, hostEnv: "GARAGE_S3_PORT", defaultHost: 7653, label: "S3 API" },
1350
+ { internal: 3903, hostEnv: "GARAGE_ADMIN_PORT", defaultHost: 7654, label: "admin API" }
1351
+ ],
1352
+ holds: "buckets",
1353
+ volumes: [
1354
+ { host: "./data/garage/meta", container: "/var/lib/garage/meta" },
1355
+ { host: "./data/garage/data", container: "/var/lib/garage/data" },
1356
+ { host: "./garage.toml", container: "/etc/garage.toml", readonly: true }
1357
+ ],
1358
+ containerEnv: {
1359
+ GARAGE_RPC_SECRET: "${GARAGE_RPC_SECRET}",
1360
+ GARAGE_ADMIN_TOKEN: "${GARAGE_ADMIN_TOKEN}",
1361
+ GARAGE_METRICS_TOKEN: "${GARAGE_METRICS_TOKEN}"
1362
+ },
1363
+ healthcheck: { test: ["CMD", "/garage", "status"] },
1364
+ notes: "Distroless image \u2014 healthcheck uses the bundled CLI."
1395
1365
  },
1396
1366
  {
1397
- event: "SubagentStop",
1398
- category: "subagent",
1399
- canBlock: true,
1400
- wired: true,
1401
- summary: "A subagent finishes \u2014 `summary` of its work."
1367
+ key: "garage-ui",
1368
+ name: "Garage Web UI",
1369
+ role: "admin-ui",
1370
+ image: {
1371
+ name: "garage-ui",
1372
+ tagEnv: "GARAGE_UI_TAG",
1373
+ defaultTag: "1.1.0",
1374
+ upstream: "khairul169/garage-webui"
1375
+ },
1376
+ ports: [{ internal: 3909, hostEnv: "GARAGE_WEBUI_PORT", defaultHost: 7655 }],
1377
+ adminUiServiceKey: "garageWebui",
1378
+ dependsOn: ["garage"],
1379
+ containerEnv: {
1380
+ API_BASE_URL: "http://garage:3903",
1381
+ API_ADMIN_KEY: "${GARAGE_ADMIN_TOKEN}",
1382
+ S3_ENDPOINT_URL: "http://garage:3900",
1383
+ S3_REGION: "garage"
1384
+ }
1402
1385
  },
1403
1386
  {
1404
- event: "TeammateIdle",
1405
- category: "subagent",
1406
- canBlock: true,
1407
- wired: false,
1408
- ignoreReason: "Team orchestration, not session history.",
1409
- summary: "A team teammate goes idle."
1387
+ key: "fossil",
1388
+ name: "Fossil",
1389
+ role: "backing",
1390
+ image: {
1391
+ name: "fossil",
1392
+ tagEnv: "FOSSIL_TAG",
1393
+ defaultTag: "2.28",
1394
+ build: { context: "./fossil" }
1395
+ },
1396
+ ports: [{ internal: 8080, hostEnv: "FOSSIL_PORT", defaultHost: 7658, label: "web UI + sync" }],
1397
+ adminUiServiceKey: "fossil",
1398
+ holds: "repositories",
1399
+ volumes: [{ host: "./data/fossil", container: "/museum" }],
1400
+ containerEnv: { FOSSIL_REPOSITORIES: "${FOSSIL_REPOSITORIES:-claude-transcripts-sessions}" },
1401
+ tcpHealth: { port: 8080 },
1402
+ notes: "Version control (repos + web UI) from one static binary. Read by the webapi (read-only JSON API proxy); nothing writes it yet. Seeds the repositories named in config on start (anonymous read-only); serves every <name>.fossil under /museum."
1410
1403
  },
1411
1404
  {
1412
- event: "TaskCreated",
1413
- category: "subagent",
1414
- canBlock: true,
1415
- wired: false,
1416
- ignoreReason: "Task orchestration, not session history.",
1417
- summary: "A task is created via `TaskCreate`."
1405
+ key: "meilisearch",
1406
+ name: "Meilisearch",
1407
+ role: "backing",
1408
+ image: {
1409
+ name: "meilisearch",
1410
+ tagEnv: "MEILI_TAG",
1411
+ defaultTag: "v1.10",
1412
+ upstream: "getmeili/meilisearch"
1413
+ },
1414
+ ports: [{ internal: 7700, hostEnv: "MEILI_PORT", defaultHost: 7656 }],
1415
+ adminUiServiceKey: "meilisearch",
1416
+ holds: "indexes",
1417
+ volumes: [{ host: "./data/meilisearch", container: "/meili_data" }],
1418
+ containerEnv: { MEILI_ENV: "development" },
1419
+ notes: "Derived search index (optional/removable). Built-in UI on /."
1418
1420
  },
1419
1421
  {
1420
- event: "TaskCompleted",
1421
- category: "subagent",
1422
- canBlock: true,
1423
- wired: false,
1424
- ignoreReason: "Task orchestration, not session history.",
1425
- summary: "A task is marked complete."
1422
+ key: "meilisearch-ui",
1423
+ name: "Meilisearch UI",
1424
+ role: "admin-ui",
1425
+ image: {
1426
+ name: "meilisearch-ui",
1427
+ tagEnv: "MEILI_UI_TAG",
1428
+ defaultTag: "v0.14.1",
1429
+ upstream: "riccoxie/meilisearch-ui"
1430
+ },
1431
+ ports: [{ internal: 24900, hostEnv: "MEILI_UI_PORT", defaultHost: 7657 }],
1432
+ adminUiServiceKey: "meilisearchUi",
1433
+ dependsOn: ["meilisearch"]
1426
1434
  },
1427
1435
  {
1428
- event: "MessageDisplay",
1429
- category: "display",
1430
- canBlock: false,
1431
- wired: false,
1432
- ignoreReason: "UX event; the message content is already in the transcript.",
1433
- summary: "An assistant message is streamed/displayed to the user."
1434
- },
1436
+ key: "app",
1437
+ name: "app (combined image)",
1438
+ role: "app",
1439
+ image: { name: "app", tagEnv: "APP_TAG", defaultTag: "latest" },
1440
+ ports: [{ internal: 7650, hostEnv: "WEBAPI_PORT", defaultHost: 7650 }],
1441
+ profiles: ["app"],
1442
+ dependsOn: ["couchdb", "garage", "meilisearch", "fossil"],
1443
+ envFile: "../.env",
1444
+ containerEnv: {
1445
+ WEBAPI_HOST: "0.0.0.0",
1446
+ WEBAPI_PORT: "7650",
1447
+ COUCHDB_URL: "http://couchdb:5984",
1448
+ S3_ENDPOINT: "http://garage:3900",
1449
+ MEILI_HOST: "http://meilisearch:7700",
1450
+ FOSSIL_URL: "http://fossil:8080"
1451
+ },
1452
+ httpHealth: { path: "/health", port: 7650 },
1453
+ notes: "webapi + webui SPA + Scalar + bundled CLI; serves /app + /api."
1454
+ }
1455
+ ];
1456
+
1457
+ // packages/shared/src/blueprint/topology.ts
1458
+ var TOPOLOGY_NODES = [
1435
1459
  {
1436
- event: "Elicitation",
1437
- category: "display",
1438
- canBlock: true,
1439
- wired: false,
1440
- ignoreReason: "MCP interaction control hook; an observe-only writer gains nothing.",
1441
- summary: "An MCP server requests user input."
1460
+ key: "claude-code",
1461
+ label: "Claude Code",
1462
+ caption: "your sessions",
1463
+ role: "producer",
1464
+ icon: "claude",
1465
+ rank: 0,
1466
+ lane: "write",
1467
+ level: "compact",
1468
+ summary: "Fires a hook event at each session lifecycle point."
1442
1469
  },
1443
1470
  {
1444
- event: "ElicitationResult",
1445
- category: "display",
1446
- canBlock: true,
1447
- wired: false,
1448
- ignoreReason: "MCP interaction event; the content is in the transcript.",
1449
- summary: "The user responds to an elicitation."
1471
+ key: "webui",
1472
+ serviceKey: "webui",
1473
+ caption: "browse",
1474
+ role: "client",
1475
+ rank: 0,
1476
+ lane: "read",
1477
+ level: "compact",
1478
+ summary: "React SPA. Optional \u2014 losing it costs the UI, not the data."
1450
1479
  },
1451
1480
  {
1452
- event: "Notification",
1453
- category: "display",
1454
- canBlock: false,
1455
- wired: false,
1456
- ignoreReason: "UX notification, not session history.",
1457
- summary: "Claude Code emits a notification (`permission_prompt`, `idle_prompt`, `auth_success`, \u2026)."
1481
+ key: "cli",
1482
+ serviceKey: "cli",
1483
+ caption: "script",
1484
+ role: "client",
1485
+ rank: 0,
1486
+ lane: "read",
1487
+ level: "compact",
1488
+ summary: "User-facing CLI and admin utility."
1458
1489
  },
1459
1490
  {
1460
- event: "CwdChanged",
1461
- category: "environment",
1462
- canBlock: false,
1463
- wired: false,
1464
- ignoreReason: "Host signal, out of session scope.",
1465
- summary: "The working directory changes during a session."
1491
+ key: "agents",
1492
+ label: "agents",
1493
+ caption: "query",
1494
+ role: "client",
1495
+ rank: 0,
1496
+ lane: "read",
1497
+ level: "compact",
1498
+ summary: "Anything that speaks the OpenAPI contract."
1466
1499
  },
1467
1500
  {
1468
- event: "FileChanged",
1469
- category: "environment",
1470
- canBlock: false,
1471
- wired: false,
1472
- ignoreReason: "Watcher signal, out of session scope.",
1473
- summary: "A watched file changes on disk."
1501
+ key: "hook",
1502
+ label: "hook",
1503
+ caption: "the writer",
1504
+ role: "writer",
1505
+ rank: 1,
1506
+ lane: "write",
1507
+ level: "compact",
1508
+ ref: "design/decisions/0016-webapi-is-the-io-gateway.md",
1509
+ summary: "Writes each event, the end-of-session summary and the transcript blob directly, so recording never depends on the webapi being up."
1474
1510
  },
1475
1511
  {
1476
- event: "ConfigChange",
1477
- category: "environment",
1478
- canBlock: true,
1479
- wired: false,
1480
- ignoreReason: "Config-change signal, out of session scope.",
1481
- summary: "A settings/config/skills file changes during the session (`user_settings`, `project_settings`, \u2026)."
1512
+ key: "webapi",
1513
+ serviceKey: "webapi",
1514
+ caption: "the I/O gateway",
1515
+ role: "gateway",
1516
+ icon: "mark",
1517
+ rank: 2,
1518
+ lane: "read",
1519
+ level: "compact",
1520
+ ref: "design/decisions/0016-webapi-is-the-io-gateway.md",
1521
+ summary: "Every read goes through it; writes are never proxied."
1482
1522
  },
1483
1523
  {
1484
- event: "WorktreeCreate",
1485
- category: "worktree",
1486
- canBlock: true,
1487
- wired: false,
1488
- ignoreReason: "Git-worktree lifecycle, irrelevant to session logging.",
1489
- summary: "A git worktree is created (`--worktree`)."
1524
+ key: "couchdb",
1525
+ serviceKey: "couchdb",
1526
+ label: "CouchDB",
1527
+ caption: "source of truth",
1528
+ role: "store",
1529
+ icon: "couchdb",
1530
+ rank: 3,
1531
+ lane: "store",
1532
+ level: "compact",
1533
+ summary: "Events, summaries and chunks. Append-only."
1490
1534
  },
1491
1535
  {
1492
- event: "WorktreeRemove",
1493
- category: "worktree",
1494
- canBlock: false,
1495
- wired: false,
1496
- ignoreReason: "Git-worktree lifecycle, irrelevant to session logging.",
1497
- summary: "A git worktree is removed."
1536
+ key: "garage",
1537
+ serviceKey: "garage",
1538
+ label: "Garage",
1539
+ caption: "transcript blobs",
1540
+ role: "store",
1541
+ icon: "garage",
1542
+ rank: 3,
1543
+ lane: "store",
1544
+ level: "compact",
1545
+ requiresFeature: "s3Blobs",
1546
+ ref: "design/decisions/0014-transcripts-live-in-s3-only.md",
1547
+ summary: "S3-compatible object storage \u2014 the transcript's sole durable home."
1498
1548
  },
1499
1549
  {
1500
- event: "PreCompact",
1501
- category: "compaction",
1502
- canBlock: true,
1503
- wired: true,
1504
- summary: "Before context compaction \u2014 `trigger` \u2208 `manual`/`auto`."
1550
+ key: "meilisearch",
1551
+ serviceKey: "meilisearch",
1552
+ label: "Meilisearch",
1553
+ caption: "derived index",
1554
+ role: "index",
1555
+ icon: "meilisearch",
1556
+ rank: 3,
1557
+ lane: "store",
1558
+ level: "compact",
1559
+ requiresFeature: "meilisearch",
1560
+ summary: "Rebuildable full-text index. Optional \u2014 losing it costs search, not history."
1505
1561
  },
1506
1562
  {
1507
- event: "PostCompact",
1508
- category: "compaction",
1509
- canBlock: false,
1510
- wired: true,
1511
- summary: "After compaction completes \u2014 `trigger` \u2208 `manual`/`auto`."
1563
+ key: "fossil",
1564
+ serviceKey: "fossil",
1565
+ label: "Fossil",
1566
+ caption: "version control",
1567
+ role: "store",
1568
+ icon: "fossil",
1569
+ rank: 3,
1570
+ lane: "store",
1571
+ level: "compact",
1572
+ summary: "Repositories, wiki and tickets with a web UI, from one binary. Read through the gateway; nothing writes it yet."
1512
1573
  },
1513
1574
  {
1514
- event: "Stop",
1515
- category: "turn-end",
1516
- canBlock: true,
1517
- wired: true,
1518
- summary: "Claude finishes responding to a turn \u2014 `turn_number`, `assistant_message`."
1575
+ key: "couchdb-fauxton",
1576
+ serviceKey: "couchdb",
1577
+ label: "Fauxton",
1578
+ role: "admin-ui",
1579
+ rank: 4,
1580
+ lane: "store",
1581
+ level: "expanded",
1582
+ summary: "CouchDB's built-in admin UI, served at /_utils/."
1519
1583
  },
1520
1584
  {
1521
- event: "StopFailure",
1522
- category: "turn-end",
1523
- canBlock: false,
1524
- wired: true,
1525
- summary: "A turn ends with an API error \u2014 `rate_limit`, `overloaded`, `max_output_tokens`, \u2026 A turn-level failure (see the crash note in the doc)."
1585
+ key: "garage-ui",
1586
+ serviceKey: "garage-ui",
1587
+ role: "admin-ui",
1588
+ rank: 4,
1589
+ lane: "store",
1590
+ level: "expanded",
1591
+ requiresFeature: "s3Blobs",
1592
+ summary: "Bucket and access-key admin for Garage."
1526
1593
  },
1527
1594
  {
1528
- event: "SessionEnd",
1529
- category: "session-end",
1530
- canBlock: false,
1531
- wired: true,
1532
- summary: "The session terminates \u2014 `reason` \u2208 `clear`/`resume`/`logout`/`prompt_input_exit`/`bypass_permissions_disabled`/`other`."
1595
+ key: "meilisearch-ui",
1596
+ serviceKey: "meilisearch-ui",
1597
+ role: "admin-ui",
1598
+ rank: 4,
1599
+ lane: "store",
1600
+ level: "expanded",
1601
+ requiresFeature: "meilisearch",
1602
+ summary: "Index browser for Meilisearch."
1533
1603
  }
1534
1604
  ];
1535
- function hookLaunch(command) {
1536
- return `cd / && ${command}`;
1537
- }
1538
- var PLUGIN_HOOK_COMMAND = hookLaunch('bun run "${CLAUDE_PLUGIN_ROOT}/scripts/dispatch.ts"');
1539
-
1540
- // packages/shared/src/model/recall.ts
1541
- var RECALL_MODES = ["off", "suggest", "auto"];
1542
- var RECALL_SCOPES = ["project", "host", "all"];
1543
- var DEFAULT_RECALL = {
1544
- mode: "auto",
1545
- scope: "project",
1546
- maxResults: 5,
1547
- maxSnippetChars: 400,
1548
- triggers: { priorWorkQuestion: true, repeatedError: true, beforeRederiving: true },
1549
- excludeCwdGlobs: [],
1550
- primer: { onSessionStart: true, maxTokens: 200 }
1551
- };
1552
- var RECALL_ENV = {
1553
- mode: "CLAUDE_PLUGIN_OPTION_RECALL_MODE",
1554
- scope: "CLAUDE_PLUGIN_OPTION_RECALL_SCOPE",
1555
- maxResults: "CLAUDE_PLUGIN_OPTION_MAX_RESULTS"
1556
- };
1557
- function resolveRecall(fromConfig, env = {}) {
1558
- const c = fromConfig ?? {};
1559
- const merged = {
1560
- ...DEFAULT_RECALL,
1561
- ...stripUndefined(c),
1562
- triggers: { ...DEFAULT_RECALL.triggers, ...stripUndefined(c.triggers ?? {}) },
1563
- primer: { ...DEFAULT_RECALL.primer, ...stripUndefined(c.primer ?? {}) },
1564
- excludeCwdGlobs: c.excludeCwdGlobs ?? DEFAULT_RECALL.excludeCwdGlobs
1565
- };
1566
- const mode = env[RECALL_ENV.mode];
1567
- if (mode && RECALL_MODES.includes(mode))
1568
- merged.mode = mode;
1569
- const scope = env[RECALL_ENV.scope];
1570
- if (scope && RECALL_SCOPES.includes(scope))
1571
- merged.scope = scope;
1572
- const max = Number(env[RECALL_ENV.maxResults]);
1573
- if (Number.isInteger(max) && max > 0)
1574
- merged.maxResults = max;
1575
- return merged;
1576
- }
1577
- function stripUndefined(o) {
1578
- const out = {};
1579
- for (const [k, v] of Object.entries(o))
1580
- if (v !== undefined)
1581
- out[k] = v;
1582
- return out;
1583
- }
1584
- function scopeFlag(scope, cwd, hostname) {
1585
- switch (scope) {
1586
- case "project":
1587
- return `--cwd "${cwd}"`;
1588
- case "host":
1589
- return `--hostname "${hostname}"`;
1590
- default:
1591
- return "";
1592
- }
1593
- }
1594
- function daysAgo(iso, now) {
1595
- if (!iso)
1596
- return "";
1597
- const d = Math.floor((now - Date.parse(iso)) / 86400000);
1598
- if (!Number.isFinite(d))
1599
- return "";
1600
- return d <= 0 ? "today" : d === 1 ? "yesterday" : `${d} days ago`;
1601
- }
1602
- function recallPrimer(policy, facts, where, now = Date.now()) {
1603
- if (policy.mode === "off" || !policy.primer.onSessionStart)
1604
- return null;
1605
- if (facts.sessionCount <= 0)
1606
- return null;
1607
- const scopeWord = { project: "this project", host: "this host", all: "all projects" }[policy.scope];
1608
- const recent = daysAgo(facts.mostRecent, now);
1609
- const flag = scopeFlag(policy.scope, where.cwd, where.hostname);
1610
- const when = [];
1611
- if (policy.triggers.priorWorkQuestion)
1612
- when.push("a question about why existing code or a decision is the way it is");
1613
- if (policy.triggers.repeatedError)
1614
- when.push("an error that looks familiar");
1615
- if (policy.triggers.beforeRederiving)
1616
- when.push("re-deriving something that looks like prior work here");
1617
- const action = policy.mode === "auto" ? "search history first" : "offer to search history, and search if the user agrees";
1618
- return [
1619
- `Session history for ${scopeWord} is available via \`claude-transcripts search "<query>" ${flag} --json --limit ${policy.maxResults}\`.`,
1620
- `${facts.sessionCount} recorded session${facts.sessionCount === 1 ? "" : "s"} in scope${recent ? `, most recent ${recent}` : ""}.`,
1621
- when.length ? `Before answering ${joinOr(when)}, ${action}.` : "",
1622
- `Cite the session id and date. Snippets only, never a whole transcript; max ${policy.maxResults} results. See the \`recall\` skill.`
1623
- ].filter(Boolean).join(" ");
1624
- }
1625
- function joinOr(items) {
1626
- if (items.length <= 1)
1627
- return items[0] ?? "";
1628
- return `${items.slice(0, -1).join(", ")}, or ${items[items.length - 1]}`;
1629
- }
1630
-
1631
- // packages/shared/src/model/routes.ts
1632
- var ROUTES = [
1633
- { path: "/", serves: "machine-readable app manifest (agent entrypoint)" },
1634
- { path: "/health", serves: "liveness + store reachability" },
1635
- { path: "/api", serves: "app JSON API (the gateway)" },
1636
- { path: "/api/docs", serves: "Scalar API reference (renders the OpenAPI spec)" },
1637
- { path: "/api/openapi.json", serves: "OpenAPI spec (contract source of truth)" },
1638
- { path: "/api/couch/*", serves: "read-only CouchDB proxy" },
1639
- { path: "/api/s3/*", serves: "read-only S3 proxy" },
1640
- { path: "/api/sessions", serves: "session list / detail / transcript / turns" },
1641
- { path: "/api/turns", serves: "turns across all sessions, time-ordered by speaker" },
1642
- { path: "/api/search", serves: "full-text search over sessions + conversation content" },
1605
+ var TOPOLOGY_EDGES = [
1643
1606
  {
1644
- path: "/api/ingest",
1645
- serves: "curated write surface (summary/events/chunks/transcript, reset)"
1607
+ from: "claude-code",
1608
+ to: "hook",
1609
+ kind: "emits",
1610
+ label: "each event",
1611
+ level: "compact",
1612
+ note: "hooks/scripts/dispatch.ts pipes the payload to `claude-transcripts hook run` and always exits 0."
1646
1613
  },
1647
- { path: "/api/migrate", serves: "schema migration status / up / down" },
1648
- { path: "/api/model", serves: "app model introspection (services/hooks/actions/env)" },
1649
- { path: "/app", serves: "webui SPA" },
1650
- { path: "/docs", serves: "rendered technical docs" },
1651
- { path: "/cli/download", serves: "the bundled CLI binary" }
1652
- ];
1653
-
1654
- // packages/shared/src/model/services.ts
1655
- var RELEASE_IMAGE_NS = "ghcr.io/vredchenko";
1656
- var SERVICES = [
1657
1614
  {
1658
- key: "webapi",
1659
- name: "webapi",
1660
- role: "gateway",
1661
- ports: [{ internal: 7650, hostEnv: "WEBAPI_PORT", defaultHost: 7650 }],
1662
- runsOnHostInDev: true,
1663
- notes: "The I/O gateway + stability column. In deploy it runs as the `app` image."
1615
+ from: "hook",
1616
+ to: "couchdb",
1617
+ kind: "direct-write",
1618
+ label: "events + summary",
1619
+ level: "compact",
1620
+ ref: "design/decisions/0016-webapi-is-the-io-gateway.md",
1621
+ note: "Written directly, not through the gateway, so a webapi outage never loses a session."
1664
1622
  },
1665
1623
  {
1666
- key: "webui",
1667
- name: "webui",
1668
- role: "webui",
1669
- ports: [{ internal: 7651, hostEnv: "WEBUI_PORT", defaultHost: 7651 }],
1670
- runsOnHostInDev: true,
1671
- notes: "Vite dev server in dev; built SPA served by the app at /app in prod."
1624
+ from: "hook",
1625
+ to: "garage",
1626
+ kind: "direct-write",
1627
+ label: "transcript",
1628
+ level: "compact",
1629
+ requiresFeature: "s3Blobs",
1630
+ note: "The same bypass; transcript bytes live only in S3 (ADR 0014)."
1672
1631
  },
1632
+ { from: "webui", to: "webapi", kind: "http-client", label: "HTTP", level: "compact" },
1633
+ { from: "cli", to: "webapi", kind: "http-client", label: "HTTP", level: "compact" },
1634
+ { from: "agents", to: "webapi", kind: "http-client", label: "HTTP", level: "compact" },
1673
1635
  {
1674
- key: "cli",
1675
- name: "cli",
1676
- role: "cli",
1677
- runsOnHostInDev: true,
1678
- notes: "User-facing CLI; no port."
1636
+ from: "webapi",
1637
+ to: "couchdb",
1638
+ kind: "read-only-proxy",
1639
+ label: "reads \xB7 /api/couch",
1640
+ level: "compact"
1679
1641
  },
1680
1642
  {
1681
- key: "couchdb",
1682
- name: "CouchDB",
1683
- role: "backing",
1684
- image: { name: "couchdb", tagEnv: "COUCHDB_TAG", defaultTag: "3", upstream: "couchdb" },
1685
- ports: [
1686
- { internal: 5984, hostEnv: "COUCHDB_PORT", defaultHost: 7652, label: "HTTP API + Fauxton" }
1687
- ],
1688
- adminUiServiceKey: "couchdbFauxton",
1689
- adminUiPath: "/_utils/",
1690
- volumes: [{ host: "./data/couchdb", container: "/opt/couchdb/data" }],
1691
- containerEnv: {
1692
- COUCHDB_USER: "${COUCHDB_USER:-admin}",
1693
- COUCHDB_PASSWORD: "${COUCHDB_PASSWORD:-admin}"
1694
- },
1695
- healthcheck: { test: ["CMD", "curl", "-f", "http://localhost:5984/_up"] },
1696
- notes: "Source of truth. Bundled dev = fixed default admin (CouchDB 3 requires one)."
1643
+ from: "webapi",
1644
+ to: "garage",
1645
+ kind: "read-only-proxy",
1646
+ label: "reads \xB7 /api/s3",
1647
+ level: "compact",
1648
+ requiresFeature: "s3Blobs"
1649
+ },
1650
+ {
1651
+ from: "webapi",
1652
+ to: "fossil",
1653
+ kind: "read-only-proxy",
1654
+ label: "reads \xB7 /api/fossil",
1655
+ level: "compact",
1656
+ note: "Fossil's JSON API, allowlisted to read-only commands: over GET it would otherwise write."
1657
+ },
1658
+ {
1659
+ from: "webapi",
1660
+ to: "meilisearch",
1661
+ kind: "indexes",
1662
+ label: "search",
1663
+ level: "compact",
1664
+ requiresFeature: "meilisearch"
1697
1665
  },
1698
1666
  {
1699
- key: "garage",
1700
- name: "Garage (S3)",
1701
- role: "backing",
1702
- image: {
1703
- name: "garage",
1704
- tagEnv: "GARAGE_TAG",
1705
- defaultTag: "v2.3.0",
1706
- upstream: "dxflrs/garage"
1707
- },
1708
- ports: [
1709
- { internal: 3900, hostEnv: "GARAGE_S3_PORT", defaultHost: 7653, label: "S3 API" },
1710
- { internal: 3903, hostEnv: "GARAGE_ADMIN_PORT", defaultHost: 7654, label: "admin API" }
1711
- ],
1712
- volumes: [
1713
- { host: "./data/garage/meta", container: "/var/lib/garage/meta" },
1714
- { host: "./data/garage/data", container: "/var/lib/garage/data" },
1715
- { host: "./garage.toml", container: "/etc/garage.toml", readonly: true }
1716
- ],
1717
- containerEnv: {
1718
- GARAGE_RPC_SECRET: "${GARAGE_RPC_SECRET}",
1719
- GARAGE_ADMIN_TOKEN: "${GARAGE_ADMIN_TOKEN}",
1720
- GARAGE_METRICS_TOKEN: "${GARAGE_METRICS_TOKEN}"
1721
- },
1722
- healthcheck: { test: ["CMD", "/garage", "status"] },
1723
- notes: "Distroless image \u2014 healthcheck uses the bundled CLI."
1667
+ from: "webapi",
1668
+ to: "couchdb",
1669
+ kind: "curated-write",
1670
+ label: "/api/ingest",
1671
+ level: "expanded",
1672
+ note: "The curated write surface that owns the document shapes; writes are never proxied."
1724
1673
  },
1725
1674
  {
1726
- key: "garage-ui",
1727
- name: "Garage Web UI",
1728
- role: "admin-ui",
1729
- image: {
1730
- name: "garage-ui",
1731
- tagEnv: "GARAGE_UI_TAG",
1732
- defaultTag: "1.1.0",
1733
- upstream: "khairul169/garage-webui"
1734
- },
1735
- ports: [{ internal: 3909, hostEnv: "GARAGE_WEBUI_PORT", defaultHost: 7655 }],
1736
- adminUiServiceKey: "garageWebui",
1737
- dependsOn: ["garage"],
1738
- containerEnv: {
1739
- API_BASE_URL: "http://garage:3903",
1740
- API_ADMIN_KEY: "${GARAGE_ADMIN_TOKEN}",
1741
- S3_ENDPOINT_URL: "http://garage:3900",
1742
- S3_REGION: "garage"
1743
- }
1675
+ from: "couchdb",
1676
+ to: "webapi",
1677
+ kind: "changes-follower",
1678
+ label: "_changes",
1679
+ level: "expanded",
1680
+ note: "Exists because of the hook bypass: hook-written docs get no write-time validation."
1744
1681
  },
1682
+ { from: "couchdb-fauxton", to: "couchdb", kind: "admin-of", level: "expanded" },
1683
+ { from: "garage-ui", to: "garage", kind: "admin-of", level: "expanded" },
1684
+ { from: "meilisearch-ui", to: "meilisearch", kind: "admin-of", level: "expanded" }
1685
+ ];
1686
+ var TOPOLOGY_GROUPS = [
1745
1687
  {
1746
- key: "meilisearch",
1747
- name: "Meilisearch",
1748
- role: "backing",
1749
- image: {
1750
- name: "meilisearch",
1751
- tagEnv: "MEILI_TAG",
1752
- defaultTag: "v1.10",
1753
- upstream: "getmeili/meilisearch"
1754
- },
1755
- ports: [{ internal: 7700, hostEnv: "MEILI_PORT", defaultHost: 7656 }],
1756
- adminUiServiceKey: "meilisearch",
1757
- volumes: [{ host: "./data/meilisearch", container: "/meili_data" }],
1758
- containerEnv: { MEILI_ENV: "development" },
1759
- notes: "Derived search index (optional/removable). Built-in UI on /."
1688
+ key: "clients",
1689
+ title: "read / query",
1690
+ members: ["webui", "cli", "agents"],
1691
+ level: "compact"
1760
1692
  },
1761
1693
  {
1762
- key: "meilisearch-ui",
1763
- name: "Meilisearch UI",
1764
- role: "admin-ui",
1765
- image: {
1766
- name: "meilisearch-ui",
1767
- tagEnv: "MEILI_UI_TAG",
1768
- defaultTag: "v0.14.1",
1769
- upstream: "riccoxie/meilisearch-ui"
1694
+ key: "stores",
1695
+ title: "your infrastructure",
1696
+ members: ["couchdb", "garage", "meilisearch", "fossil"],
1697
+ level: "compact"
1698
+ }
1699
+ ];
1700
+ var TOPOLOGY = {
1701
+ nodes: TOPOLOGY_NODES,
1702
+ edges: TOPOLOGY_EDGES,
1703
+ groups: TOPOLOGY_GROUPS
1704
+ };
1705
+
1706
+ // packages/shared/src/blueprint/user-settings.ts
1707
+ var DEFAULT_USER_SETTINGS = {
1708
+ sessionListPageSize: 100,
1709
+ transcriptPageSize: 100,
1710
+ transcriptAutoLoadMax: 2000
1711
+ };
1712
+ var LIMITS = {
1713
+ sessionListPageSize: 500,
1714
+ transcriptPageSize: 500,
1715
+ transcriptAutoLoadMax: 50000
1716
+ };
1717
+ function resolveUserSettings(fromConfig) {
1718
+ const c = fromConfig ?? {};
1719
+ return {
1720
+ sessionListPageSize: clamp(c.sessionListPageSize, "sessionListPageSize"),
1721
+ transcriptPageSize: clamp(c.transcriptPageSize, "transcriptPageSize"),
1722
+ transcriptAutoLoadMax: clamp(c.transcriptAutoLoadMax, "transcriptAutoLoadMax")
1723
+ };
1724
+ }
1725
+ function clamp(value, key) {
1726
+ const fallback = DEFAULT_USER_SETTINGS[key];
1727
+ if (typeof value !== "number" || !Number.isFinite(value))
1728
+ return fallback;
1729
+ const rounded = Math.floor(value);
1730
+ if (rounded < 1)
1731
+ return fallback;
1732
+ return Math.min(rounded, LIMITS[key]);
1733
+ }
1734
+
1735
+ // packages/shared/src/blueprint/build.ts
1736
+ var STALE_TEMPLATE_LINKS = {
1737
+ couchdbFauxton: "http://127.0.0.1:7652/_utils/",
1738
+ garageWebui: "http://127.0.0.1:7655/",
1739
+ meilisearch: "http://127.0.0.1:7656/"
1740
+ };
1741
+ function buildServicesMenu(services, fromConfig) {
1742
+ const derived = {};
1743
+ for (const s of services) {
1744
+ if (!s.adminUiServiceKey)
1745
+ continue;
1746
+ const port = s.resolvedPorts?.[0]?.host;
1747
+ if (!port)
1748
+ continue;
1749
+ const path = s.adminUiPath ?? "/";
1750
+ derived[s.adminUiServiceKey] = `http://127.0.0.1:${port}${path}`;
1751
+ }
1752
+ const configured = Object.entries(fromConfig).filter(([k, v]) => STALE_TEMPLATE_LINKS[k] !== v);
1753
+ return { ...derived, ...Object.fromEntries(configured) };
1754
+ }
1755
+ var FOSSIL_REPOSITORY_NAME = /^[A-Za-z0-9_][A-Za-z0-9_-]*$/;
1756
+ function resolveRepositories(config) {
1757
+ const repositories = {
1758
+ sessions: "claude-transcripts-sessions",
1759
+ ...config.fossil?.repositories ?? {}
1760
+ };
1761
+ for (const [key, name] of Object.entries(repositories)) {
1762
+ if (!FOSSIL_REPOSITORY_NAME.test(name)) {
1763
+ throw new Error(`config: fossil.repositories.${key} = ${JSON.stringify(name)} \u2014 use letters, digits, '-' and '_' (not starting with '-')`);
1764
+ }
1765
+ }
1766
+ return repositories;
1767
+ }
1768
+ function buildAppBlueprint(config, env = {}) {
1769
+ const version = env.CT_VERSION ?? "0.0.0-dev";
1770
+ const services = SERVICES.map((s) => ({
1771
+ ...s,
1772
+ resolvedPorts: (s.ports ?? []).map((p) => ({
1773
+ internal: p.internal,
1774
+ host: env[p.hostEnv] ? Number(env[p.hostEnv]) : p.defaultHost,
1775
+ label: p.label
1776
+ }))
1777
+ }));
1778
+ return {
1779
+ identity: {
1780
+ codename: "claude-transcripts",
1781
+ slug: config.app?.name ?? "claude-transcripts",
1782
+ title: "Claude Transcripts",
1783
+ version,
1784
+ repository: "https://github.com/vredchenko/claude-transcripts"
1770
1785
  },
1771
- ports: [{ internal: 24900, hostEnv: "MEILI_UI_PORT", defaultHost: 7657 }],
1772
- adminUiServiceKey: "meilisearchUi",
1773
- dependsOn: ["meilisearch"]
1774
- },
1775
- {
1776
- key: "app",
1777
- name: "app (combined image)",
1778
- role: "app",
1779
- image: { name: "app", tagEnv: "APP_TAG", defaultTag: "latest" },
1780
- ports: [{ internal: 7650, hostEnv: "WEBAPI_PORT", defaultHost: 7650 }],
1781
- profiles: ["app"],
1782
- dependsOn: ["couchdb", "garage", "meilisearch"],
1783
- envFile: "../.env",
1784
- containerEnv: {
1785
- WEBAPI_HOST: "0.0.0.0",
1786
- WEBAPI_PORT: "7650",
1787
- COUCHDB_URL: "http://couchdb:5984",
1788
- S3_ENDPOINT: "http://garage:3900",
1789
- MEILI_HOST: "http://meilisearch:7700"
1786
+ services,
1787
+ stores: {
1788
+ databases: { ...config.couchdb.databases },
1789
+ buckets: { ...config.s3.buckets },
1790
+ indexes: { ...config.meilisearch?.indexes },
1791
+ repositories: resolveRepositories(config)
1790
1792
  },
1791
- httpHealth: { path: "/health", port: 7650 },
1792
- notes: "webapi + webui SPA + Scalar + bundled CLI; serves /app + /api."
1793
+ hooks: HOOK_TYPES,
1794
+ actions: ACTIONS,
1795
+ bindings: BINDINGS,
1796
+ routes: ROUTES,
1797
+ topology: TOPOLOGY,
1798
+ env: ENV_VARS,
1799
+ features: config.features,
1800
+ servicesMenu: buildServicesMenu(services, config.servicesMenu),
1801
+ system: config.system,
1802
+ recall: resolveRecall(config.recall, env),
1803
+ userSettings: resolveUserSettings(config.userSettings),
1804
+ cliSpec: CLI_SPEC
1805
+ };
1806
+ }
1807
+ // packages/shared/src/blueprint/k8s.ts
1808
+ var WORKLOAD_ROLES = new Set(["backing", "admin-ui", "app"]);
1809
+ // packages/shared/src/blueprint/project.ts
1810
+ function toStoreEnv(model) {
1811
+ return { FOSSIL_REPOSITORIES: Object.values(model.stores.repositories).join(",") };
1812
+ }
1813
+ // packages/shared/src/couch-url.ts
1814
+ var DEFAULT_COUCHDB_HOST = "127.0.0.1";
1815
+ var DEFAULT_COUCHDB_PORT = "7652";
1816
+ function trimTrailingSlash(url) {
1817
+ return url.replace(/\/+$/, "");
1818
+ }
1819
+ function resolveCouchUrl(env = {}) {
1820
+ const explicit = env.COUCHDB_URL?.trim();
1821
+ if (explicit) {
1822
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(explicit) ? explicit : `http://${explicit}`;
1823
+ return trimTrailingSlash(withScheme);
1824
+ }
1825
+ const host = env.COUCHDB_HOST || DEFAULT_COUCHDB_HOST;
1826
+ const port = env.COUCHDB_PORT || DEFAULT_COUCHDB_PORT;
1827
+ return `http://${host}:${port}`;
1828
+ }
1829
+ // packages/shared/src/highlight.ts
1830
+ var HIGHLIGHT_PRE = "\uE000";
1831
+ var HIGHLIGHT_POST = "\uE001";
1832
+ function stripHighlightMarks(marked) {
1833
+ return marked.replaceAll(HIGHLIGHT_PRE, "").replaceAll(HIGHLIGHT_POST, "");
1834
+ }
1835
+ // packages/shared/src/migrations/designs.ts
1836
+ var CHUNK_ENTRIES_MAP = `function (doc) {
1837
+ if (doc.type !== "chunk" || !doc.entries) return;
1838
+ for (var i = 0; i < doc.entries.length; i++) {
1839
+ var e = doc.entries[i];
1840
+ if (!e || !e.role) continue;
1841
+ emit([doc.session_id, doc.byte_start, i], {
1842
+ role: e.role,
1843
+ timestamp: e.timestamp || "",
1844
+ text: e.text || "",
1845
+ toolUses: e.toolUses || null,
1846
+ toolUseId: e.toolUseId || null,
1847
+ isError: e.isError || false,
1848
+ isSidechain: e.isSidechain || false,
1849
+ kind: e.kind || null
1850
+ });
1851
+ }
1852
+ }`;
1853
+ var CHUNKS_DESIGN = {
1854
+ _id: "_design/chunks",
1855
+ language: "javascript",
1856
+ views: {
1857
+ by_session: {
1858
+ map: 'function(doc){if(doc.type==="chunk"){emit([doc.session_id,doc.byte_start],{byte_start:doc.byte_start,byte_end:doc.byte_end,entry_count:doc.entry_count});}}'
1859
+ },
1860
+ entry_count_by_session: {
1861
+ map: 'function(doc){if(doc.type==="chunk"){emit(doc.session_id,doc.entry_count||0);}}',
1862
+ reduce: "_sum"
1863
+ },
1864
+ entries_by_session: { map: CHUNK_ENTRIES_MAP, reduce: "_count" }
1793
1865
  }
1794
- ];
1795
-
1796
- // packages/shared/src/model/topology.ts
1797
- var TOPOLOGY_NODES = [
1798
- {
1799
- key: "claude-code",
1800
- label: "Claude Code",
1801
- caption: "your sessions",
1802
- role: "producer",
1803
- icon: "claude",
1804
- rank: 0,
1805
- lane: "write",
1806
- level: "compact",
1807
- summary: "Fires a hook event at each session lifecycle point."
1808
- },
1809
- {
1810
- key: "webui",
1811
- serviceKey: "webui",
1812
- caption: "browse",
1813
- role: "client",
1814
- rank: 0,
1815
- lane: "read",
1816
- level: "compact",
1817
- summary: "React SPA. Optional \u2014 losing it costs the UI, not the data."
1818
- },
1819
- {
1820
- key: "cli",
1821
- serviceKey: "cli",
1822
- caption: "script",
1823
- role: "client",
1824
- rank: 0,
1825
- lane: "read",
1826
- level: "compact",
1827
- summary: "User-facing CLI and admin utility."
1828
- },
1829
- {
1830
- key: "agents",
1831
- label: "agents",
1832
- caption: "query",
1833
- role: "client",
1834
- rank: 0,
1835
- lane: "read",
1836
- level: "compact",
1837
- summary: "Anything that speaks the OpenAPI contract."
1838
- },
1839
- {
1840
- key: "hook",
1841
- label: "hook",
1842
- caption: "the writer",
1843
- role: "writer",
1844
- rank: 1,
1845
- lane: "write",
1846
- level: "compact",
1847
- ref: "design/decisions/0016-webapi-is-the-io-gateway.md",
1848
- summary: "Writes each event, the end-of-session summary and the transcript blob directly, so recording never depends on the webapi being up."
1849
- },
1850
- {
1851
- key: "webapi",
1852
- serviceKey: "webapi",
1853
- caption: "the I/O gateway",
1854
- role: "gateway",
1855
- icon: "mark",
1856
- rank: 2,
1857
- lane: "read",
1858
- level: "compact",
1859
- ref: "design/decisions/0016-webapi-is-the-io-gateway.md",
1860
- summary: "Every read goes through it; writes are never proxied."
1861
- },
1862
- {
1863
- key: "couchdb",
1864
- serviceKey: "couchdb",
1865
- label: "CouchDB",
1866
- caption: "source of truth",
1867
- role: "store",
1868
- icon: "couchdb",
1869
- rank: 3,
1870
- lane: "store",
1871
- level: "compact",
1872
- summary: "Events, summaries and chunks. Append-only."
1873
- },
1866
+ };
1867
+ var INITIAL_DESIGNS = [
1874
1868
  {
1875
- key: "garage",
1876
- serviceKey: "garage",
1877
- label: "Garage",
1878
- caption: "transcript blobs",
1879
- role: "store",
1880
- icon: "garage",
1881
- rank: 3,
1882
- lane: "store",
1883
- level: "compact",
1884
- requiresFeature: "s3Blobs",
1885
- ref: "design/decisions/0014-transcripts-live-in-s3-only.md",
1886
- summary: "S3-compatible object storage \u2014 the transcript's sole durable home."
1869
+ _id: "_design/sessions",
1870
+ language: "javascript",
1871
+ views: {
1872
+ by_date: {
1873
+ map: 'function(doc){if(doc.type==="summary"){var t=doc.timestamp.split(/[-T:]/);emit([parseInt(t[0]),parseInt(t[1]),parseInt(t[2])],{session_id:doc.session_id,event_count:doc.event_count,prompt_count:doc.prompt_count,error_count:doc.error_count,cwd:doc.cwd});}}',
1874
+ reduce: "_count"
1875
+ },
1876
+ by_cwd: {
1877
+ map: 'function(doc){if(doc.type==="summary"){emit([doc.cwd,doc.timestamp],{session_id:doc.session_id,event_count:doc.event_count,prompt_count:doc.prompt_count});}}',
1878
+ reduce: "_count"
1879
+ }
1880
+ }
1887
1881
  },
1888
1882
  {
1889
- key: "meilisearch",
1890
- serviceKey: "meilisearch",
1891
- label: "Meilisearch",
1892
- caption: "derived index",
1893
- role: "index",
1894
- icon: "meilisearch",
1895
- rank: 3,
1896
- lane: "store",
1897
- level: "compact",
1898
- requiresFeature: "meilisearch",
1899
- summary: "Rebuildable full-text index. Optional \u2014 losing it costs search, not history."
1883
+ _id: "_design/events",
1884
+ language: "javascript",
1885
+ views: {
1886
+ by_session: {
1887
+ map: 'function(doc){if(doc.type==="event"){emit([doc.session_id,doc.timestamp],{event:doc.event,tool_name:doc.tool_name||null,input_preview:doc.input_preview||doc.prompt_preview||doc.error_preview||null});}}'
1888
+ },
1889
+ by_type: {
1890
+ map: 'function(doc){if(doc.type==="event"){var t=doc.timestamp.split(/[-T:]/);emit([doc.event,parseInt(t[0]),parseInt(t[1]),parseInt(t[2])],1);}}',
1891
+ reduce: "_count"
1892
+ }
1893
+ }
1900
1894
  },
1901
1895
  {
1902
- key: "couchdb-fauxton",
1903
- serviceKey: "couchdb",
1904
- label: "Fauxton",
1905
- role: "admin-ui",
1906
- rank: 4,
1907
- lane: "store",
1908
- level: "expanded",
1909
- summary: "CouchDB's built-in admin UI, served at /_utils/."
1896
+ _id: "_design/tools",
1897
+ language: "javascript",
1898
+ views: {
1899
+ usage: {
1900
+ map: 'function(doc){if(doc.type==="event"&&doc.tool_name){var t=doc.timestamp.split(/[-T:]/);emit([doc.tool_name,parseInt(t[0]),parseInt(t[1]),parseInt(t[2])],1);}}',
1901
+ reduce: "_count"
1902
+ },
1903
+ failures: {
1904
+ map: 'function(doc){if(doc.event==="PostToolUseFailure"){emit([doc.tool_name,doc.timestamp],{session_id:doc.session_id,error_preview:doc.error_preview,cwd:doc.cwd});}}'
1905
+ },
1906
+ errors: {
1907
+ map: 'function(doc){if(doc.event==="PostToolUseFailure"||doc.error){emit([doc.tool_name||"unknown",doc.timestamp],{session_id:doc.session_id,error_preview:doc.error_preview||doc.error,cwd:doc.cwd});}}'
1908
+ }
1909
+ }
1910
1910
  },
1911
1911
  {
1912
- key: "garage-ui",
1913
- serviceKey: "garage-ui",
1914
- role: "admin-ui",
1915
- rank: 4,
1916
- lane: "store",
1917
- level: "expanded",
1918
- requiresFeature: "s3Blobs",
1919
- summary: "Bucket and access-key admin for Garage."
1912
+ _id: "_design/activity",
1913
+ language: "javascript",
1914
+ views: {
1915
+ timeline: {
1916
+ map: 'function(doc){if(doc.type==="event"){var t=doc.timestamp.split(/[-T:]/);emit([parseInt(t[0]),parseInt(t[1]),parseInt(t[2]),parseInt(t[3])],1);}}',
1917
+ reduce: "_count"
1918
+ }
1919
+ }
1920
1920
  },
1921
+ CHUNKS_DESIGN,
1921
1922
  {
1922
- key: "meilisearch-ui",
1923
- serviceKey: "meilisearch-ui",
1924
- role: "admin-ui",
1925
- rank: 4,
1926
- lane: "store",
1927
- level: "expanded",
1928
- requiresFeature: "meilisearch",
1929
- summary: "Index browser for Meilisearch."
1923
+ _id: "_design/session_meta",
1924
+ language: "javascript",
1925
+ views: {
1926
+ start_meta: {
1927
+ map: 'function(doc){if(doc.event==="SessionStart"){emit(doc.session_id,{timestamp:doc.timestamp,model:doc.model||"",cwd:doc.cwd||"",hostname:doc.hostname||""});}}'
1928
+ },
1929
+ tokens_by_date: {
1930
+ map: 'function(doc){if(doc.type==="summary"&&doc.token_usage){var t=doc.timestamp.split(/[-T:]/);var u=doc.token_usage;emit([parseInt(t[0]),parseInt(t[1]),parseInt(t[2])],{input:u.input||0,output:u.output||0,cacheCreation:u.cacheCreation||0,cacheRead:u.cacheRead||0,total:u.total||0,sessions:1});}}',
1931
+ reduce: "_sum"
1932
+ }
1933
+ }
1934
+ }
1935
+ ];
1936
+ // packages/shared/src/migrations/session-index.ts
1937
+ var AGGREGATE_MAP = `function (doc) {
1938
+ if (!doc.session_id) return;
1939
+ // A \`chunk\` doc contributes transcript coverage ONLY \u2014 no timestamps, no counts \u2014
1940
+ // so every other metric keeps the exact meaning it had before chunks were indexed.
1941
+ // \`chunkEntries\` counts turns the chunk view can actually serve (byte-range-only
1942
+ // chunks carry no \`entries[]\`), while \`chunkBytes\` tracks raw byte coverage.
1943
+ if (doc.type === "chunk") {
1944
+ emit(doc.session_id, {
1945
+ ended: 0, events: 0, prompts: 0, errors: 0, started: 0, tools: {},
1946
+ first: "", last: "", model: "", cwd: "", hostname: "",
1947
+ modelAt: "", cwdAt: "", hostnameAt: "", summary: null,
1948
+ chunkEntries: doc.entries ? doc.entries.length : 0,
1949
+ chunkBytes: doc.byte_end || 0
1950
+ });
1951
+ return;
1952
+ }
1953
+ if (doc.type !== "event" && doc.type !== "summary") return;
1954
+ var isSummary = doc.type === "summary";
1955
+ var ts = doc.timestamp || "";
1956
+ var model = (doc.event === "SessionStart" || isSummary) ? (doc.model || "") : "";
1957
+ var tools = {};
1958
+ if (doc.event === "PostToolUse" && doc.tool_name) tools[doc.tool_name] = 1;
1959
+ emit(doc.session_id, {
1960
+ ended: isSummary ? 1 : 0,
1961
+ events: doc.type === "event" ? 1 : 0,
1962
+ prompts: doc.event === "UserPromptSubmit" ? 1 : 0,
1963
+ errors: doc.event === "PostToolUseFailure" ? 1 : 0,
1964
+ started: doc.event === "SessionStart" ? 1 : 0,
1965
+ tools: tools,
1966
+ first: ts,
1967
+ last: ts,
1968
+ model: model,
1969
+ cwd: doc.cwd || "",
1970
+ hostname: doc.hostname || "",
1971
+ modelAt: model ? ts : "",
1972
+ cwdAt: doc.cwd ? ts : "",
1973
+ hostnameAt: doc.hostname ? ts : "",
1974
+ summary: isSummary ? {
1975
+ event_count: doc.event_count || 0,
1976
+ prompt_count: doc.prompt_count || 0,
1977
+ error_count: doc.error_count || 0,
1978
+ tool_counts: doc.tool_counts || {},
1979
+ end_reason: doc.end_reason || "",
1980
+ transcript_bytes: doc.transcript_bytes || 0,
1981
+ token_usage: doc.token_usage || null,
1982
+ timestamp: doc.timestamp || "",
1983
+ source: doc.source || ""
1984
+ } : null,
1985
+ chunkEntries: 0,
1986
+ chunkBytes: 0
1987
+ });
1988
+ }`;
1989
+ var AGGREGATE_REDUCE = `function (keys, values, rereduce) {
1990
+ var acc = { ended:0, events:0, prompts:0, errors:0, started:0, tools:{}, first:"", last:"", model:"", cwd:"", hostname:"", modelAt:"", cwdAt:"", hostnameAt:"", summary:null, chunkEntries:0, chunkBytes:0 };
1991
+ // Keep the earliest-stamped value; an unstamped one sorts last, ties go to the smaller value.
1992
+ function earliest(v, f) {
1993
+ var val = v[f];
1994
+ if (!val) return;
1995
+ var at = v[f + "At"] || "", a = at || "\\uffff", b = acc[f + "At"] || "\\uffff";
1996
+ if (acc[f] === "" || a < b || (a === b && val < acc[f])) { acc[f] = val; acc[f + "At"] = at; }
1997
+ }
1998
+ for (var i = 0; i < values.length; i++) {
1999
+ var v = values[i];
2000
+ if (!v) continue;
2001
+ acc.chunkEntries += v.chunkEntries || 0;
2002
+ if ((v.chunkBytes || 0) > acc.chunkBytes) acc.chunkBytes = v.chunkBytes;
2003
+ acc.ended += v.ended || 0;
2004
+ acc.events += v.events || 0;
2005
+ acc.prompts += v.prompts || 0;
2006
+ acc.errors += v.errors || 0;
2007
+ acc.started += v.started || 0;
2008
+ if (v.last && v.last > acc.last) acc.last = v.last;
2009
+ if (v.first && (acc.first === "" || v.first < acc.first)) acc.first = v.first;
2010
+ earliest(v, "model");
2011
+ earliest(v, "cwd");
2012
+ earliest(v, "hostname");
2013
+ // A session has one \`summary:<id>\` doc, so there is only ever one to keep.
2014
+ if (v.summary) acc.summary = v.summary;
2015
+ var t = v.tools || {};
2016
+ for (var k in t) { if (t.hasOwnProperty(k)) acc.tools[k] = (acc.tools[k] || 0) + t[k]; }
1930
2017
  }
1931
- ];
1932
- var TOPOLOGY_EDGES = [
1933
- {
1934
- from: "claude-code",
1935
- to: "hook",
1936
- kind: "emits",
1937
- label: "each event",
1938
- level: "compact",
1939
- note: "hooks/scripts/dispatch.ts pipes the payload to `claude-transcripts hook run` and always exits 0."
1940
- },
1941
- {
1942
- from: "hook",
1943
- to: "couchdb",
1944
- kind: "direct-write",
1945
- label: "events + summary",
1946
- level: "compact",
1947
- ref: "design/decisions/0016-webapi-is-the-io-gateway.md",
1948
- note: "Written directly, not through the gateway, so a webapi outage never loses a session."
1949
- },
1950
- {
1951
- from: "hook",
1952
- to: "garage",
1953
- kind: "direct-write",
1954
- label: "transcript",
1955
- level: "compact",
1956
- requiresFeature: "s3Blobs",
1957
- note: "The same bypass; transcript bytes live only in S3 (ADR 0014)."
1958
- },
1959
- { from: "webui", to: "webapi", kind: "http-client", label: "HTTP", level: "compact" },
1960
- { from: "cli", to: "webapi", kind: "http-client", label: "HTTP", level: "compact" },
1961
- { from: "agents", to: "webapi", kind: "http-client", label: "HTTP", level: "compact" },
1962
- {
1963
- from: "webapi",
1964
- to: "couchdb",
1965
- kind: "read-only-proxy",
1966
- label: "reads \xB7 /api/couch",
1967
- level: "compact"
2018
+ var names = Object.keys(acc.tools).sort(), tools = {};
2019
+ for (var j = 0; j < names.length; j++) tools[names[j]] = acc.tools[names[j]];
2020
+ acc.tools = tools;
2021
+ return acc;
2022
+ }`;
2023
+ var EVENT_TIMES_MAP = `function (doc) {
2024
+ if (!doc.session_id || !doc.timestamp) return;
2025
+ if (doc.type !== "event" && doc.type !== "summary") return;
2026
+ emit(doc.session_id, doc.timestamp);
2027
+ }`;
2028
+ var SESSION_INDEX_DESIGN = {
2029
+ _id: "_design/session_index",
2030
+ language: "javascript",
2031
+ views: {
2032
+ aggregate: { map: AGGREGATE_MAP, reduce: AGGREGATE_REDUCE },
2033
+ event_times: { map: EVENT_TIMES_MAP }
2034
+ }
2035
+ };
2036
+
2037
+ // packages/shared/src/migrations/speaker-split.ts
2038
+ var BY_ROLE_MAP = `function (doc) {
2039
+ if (doc.type !== "chunk" || !doc.entries) return;
2040
+ for (var i = 0; i < doc.entries.length; i++) {
2041
+ var e = doc.entries[i];
2042
+ if (!e || !e.role) continue;
2043
+ emit([doc.session_id, e.role, doc.byte_start, i], {
2044
+ role: e.role,
2045
+ timestamp: e.timestamp || "",
2046
+ text: e.text || "",
2047
+ toolUses: e.toolUses || null,
2048
+ toolUseId: e.toolUseId || null,
2049
+ isError: e.isError || false
2050
+ });
2051
+ }
2052
+ }`;
2053
+ var BY_ROLE_TIME_MAP = `function (doc) {
2054
+ if (doc.type !== "chunk" || !doc.entries) return;
2055
+ for (var i = 0; i < doc.entries.length; i++) {
2056
+ var e = doc.entries[i];
2057
+ if (!e || !e.role) continue;
2058
+ emit([e.role, e.timestamp || "", doc.session_id, doc.byte_start, i], {
2059
+ sessionId: doc.session_id,
2060
+ cwd: doc.cwd || "",
2061
+ role: e.role,
2062
+ timestamp: e.timestamp || "",
2063
+ text: e.text || ""
2064
+ });
2065
+ }
2066
+ }`;
2067
+ var SPEAKER_SPLIT_DESIGN = {
2068
+ _id: "_design/speaker_split",
2069
+ language: "javascript",
2070
+ views: {
2071
+ by_role: { map: BY_ROLE_MAP, reduce: "_count" },
2072
+ by_role_time: { map: BY_ROLE_TIME_MAP, reduce: "_count" }
2073
+ }
2074
+ };
2075
+
2076
+ // packages/shared/src/migrations/registry.ts
2077
+ var initialSchema = {
2078
+ id: 1,
2079
+ name: "initial-schema",
2080
+ async up(ctx) {
2081
+ for (const design of INITIAL_DESIGNS) {
2082
+ ctx.log(`+ ${design._id}`);
2083
+ const { _rev, ...body } = design;
2084
+ await ctx.putDoc(design._id, body);
2085
+ }
1968
2086
  },
1969
- {
1970
- from: "webapi",
1971
- to: "garage",
1972
- kind: "read-only-proxy",
1973
- label: "reads \xB7 /api/s3",
1974
- level: "compact",
1975
- requiresFeature: "s3Blobs"
2087
+ async down(ctx) {
2088
+ for (const design of INITIAL_DESIGNS) {
2089
+ ctx.log(`- ${design._id}`);
2090
+ await ctx.deleteDoc(design._id);
2091
+ }
2092
+ }
2093
+ };
2094
+ var sessionIndexView = {
2095
+ id: 2,
2096
+ name: "session-index-view",
2097
+ async up(ctx) {
2098
+ ctx.log(`+ ${SESSION_INDEX_DESIGN._id}`);
2099
+ const { _rev, ...body } = SESSION_INDEX_DESIGN;
2100
+ await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
1976
2101
  },
1977
- {
1978
- from: "webapi",
1979
- to: "meilisearch",
1980
- kind: "indexes",
1981
- label: "search",
1982
- level: "compact",
1983
- requiresFeature: "meilisearch"
2102
+ async down(ctx) {
2103
+ ctx.log(`- ${SESSION_INDEX_DESIGN._id}`);
2104
+ await ctx.deleteDoc(SESSION_INDEX_DESIGN._id);
2105
+ }
2106
+ };
2107
+ var sessionIndexSource = {
2108
+ id: 3,
2109
+ name: "session-index-source",
2110
+ async up(ctx) {
2111
+ ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (emit summary.source)`);
2112
+ const { _rev, ...body } = SESSION_INDEX_DESIGN;
2113
+ await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
1984
2114
  },
1985
- {
1986
- from: "webapi",
1987
- to: "couchdb",
1988
- kind: "curated-write",
1989
- label: "/api/ingest",
1990
- level: "expanded",
1991
- note: "The curated write surface that owns the document shapes; writes are never proxied."
2115
+ async down(ctx) {
2116
+ ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (source field left in place \u2014 additive)`);
2117
+ }
2118
+ };
2119
+ var speakerSplitView = {
2120
+ id: 4,
2121
+ name: "speaker-split-view",
2122
+ async up(ctx) {
2123
+ ctx.log(`+ ${SPEAKER_SPLIT_DESIGN._id}`);
2124
+ const { _rev, ...body } = SPEAKER_SPLIT_DESIGN;
2125
+ await ctx.putDoc(SPEAKER_SPLIT_DESIGN._id, body);
1992
2126
  },
1993
- {
1994
- from: "couchdb",
1995
- to: "webapi",
1996
- kind: "changes-follower",
1997
- label: "_changes",
1998
- level: "expanded",
1999
- note: "Exists because of the hook bypass: hook-written docs get no write-time validation."
2127
+ async down(ctx) {
2128
+ ctx.log(`- ${SPEAKER_SPLIT_DESIGN._id}`);
2129
+ await ctx.deleteDoc(SPEAKER_SPLIT_DESIGN._id);
2130
+ }
2131
+ };
2132
+ var speakerSplitTimeView = {
2133
+ id: 5,
2134
+ name: "speaker-split-time-view",
2135
+ async up(ctx) {
2136
+ ctx.log(`~ ${SPEAKER_SPLIT_DESIGN._id} (add by_role_time)`);
2137
+ const { _rev, ...body } = SPEAKER_SPLIT_DESIGN;
2138
+ await ctx.putDoc(SPEAKER_SPLIT_DESIGN._id, body);
2000
2139
  },
2001
- { from: "couchdb-fauxton", to: "couchdb", kind: "admin-of", level: "expanded" },
2002
- { from: "garage-ui", to: "garage", kind: "admin-of", level: "expanded" },
2003
- { from: "meilisearch-ui", to: "meilisearch", kind: "admin-of", level: "expanded" }
2004
- ];
2005
- var TOPOLOGY_GROUPS = [
2006
- {
2007
- key: "clients",
2008
- title: "read / query",
2009
- members: ["webui", "cli", "agents"],
2010
- level: "compact"
2140
+ async down(ctx) {
2141
+ ctx.log(`~ ${SPEAKER_SPLIT_DESIGN._id} (by_role_time left in place \u2014 additive)`);
2142
+ }
2143
+ };
2144
+ var chunkEntriesView = {
2145
+ id: 6,
2146
+ name: "chunk-entries-view",
2147
+ async up(ctx) {
2148
+ ctx.log(`~ ${CHUNKS_DESIGN._id} (add entries_by_session)`);
2149
+ const { _rev, ...body } = CHUNKS_DESIGN;
2150
+ await ctx.putDoc(CHUNKS_DESIGN._id, body);
2011
2151
  },
2012
- {
2013
- key: "stores",
2014
- title: "your infrastructure",
2015
- members: ["couchdb", "garage", "meilisearch"],
2016
- level: "compact"
2152
+ async down(ctx) {
2153
+ ctx.log(`~ ${CHUNKS_DESIGN._id} (entries_by_session left in place \u2014 additive)`);
2017
2154
  }
2018
- ];
2019
- var TOPOLOGY = {
2020
- nodes: TOPOLOGY_NODES,
2021
- edges: TOPOLOGY_EDGES,
2022
- groups: TOPOLOGY_GROUPS
2023
2155
  };
2024
-
2025
- // packages/shared/src/model/user-settings.ts
2026
- var DEFAULT_USER_SETTINGS = {
2027
- sessionListPageSize: 100,
2028
- transcriptPageSize: 100,
2029
- transcriptAutoLoadMax: 2000
2156
+ var sessionIndexChunkCoverage = {
2157
+ id: 7,
2158
+ name: "session-index-chunk-coverage",
2159
+ async up(ctx) {
2160
+ ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (emit chunk coverage)`);
2161
+ const { _rev, ...body } = SESSION_INDEX_DESIGN;
2162
+ await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
2163
+ },
2164
+ async down(ctx) {
2165
+ ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (chunk coverage left in place \u2014 additive)`);
2166
+ }
2030
2167
  };
2031
- var LIMITS = {
2032
- sessionListPageSize: 500,
2033
- transcriptPageSize: 500,
2034
- transcriptAutoLoadMax: 50000
2168
+ var chunkEntryKind = {
2169
+ id: 8,
2170
+ name: "chunk-entry-kind",
2171
+ async up(ctx) {
2172
+ ctx.log(`~ ${CHUNKS_DESIGN._id} (emit entry kind)`);
2173
+ const { _rev, ...body } = CHUNKS_DESIGN;
2174
+ await ctx.putDoc(CHUNKS_DESIGN._id, body);
2175
+ },
2176
+ async down(ctx) {
2177
+ ctx.log(`~ ${CHUNKS_DESIGN._id} (kind left in place \u2014 additive)`);
2178
+ }
2035
2179
  };
2036
- function resolveUserSettings(fromConfig) {
2037
- const c = fromConfig ?? {};
2038
- return {
2039
- sessionListPageSize: clamp(c.sessionListPageSize, "sessionListPageSize"),
2040
- transcriptPageSize: clamp(c.transcriptPageSize, "transcriptPageSize"),
2041
- transcriptAutoLoadMax: clamp(c.transcriptAutoLoadMax, "transcriptAutoLoadMax")
2042
- };
2043
- }
2044
- function clamp(value, key) {
2045
- const fallback = DEFAULT_USER_SETTINGS[key];
2046
- if (typeof value !== "number" || !Number.isFinite(value))
2047
- return fallback;
2048
- const rounded = Math.floor(value);
2049
- if (rounded < 1)
2050
- return fallback;
2051
- return Math.min(rounded, LIMITS[key]);
2052
- }
2053
-
2054
- // packages/shared/src/model/build.ts
2055
- var STALE_TEMPLATE_LINKS = {
2056
- couchdbFauxton: "http://127.0.0.1:7652/_utils/",
2057
- garageWebui: "http://127.0.0.1:7655/",
2058
- meilisearch: "http://127.0.0.1:7656/"
2180
+ var sessionIndexEventTimes = {
2181
+ id: 9,
2182
+ name: "session-index-event-times",
2183
+ async up(ctx) {
2184
+ ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (add event_times view)`);
2185
+ const { _rev, ...body } = SESSION_INDEX_DESIGN;
2186
+ await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
2187
+ },
2188
+ async down(ctx) {
2189
+ ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (event_times left in place \u2014 additive)`);
2190
+ }
2059
2191
  };
2060
- function buildServicesMenu(services, fromConfig) {
2061
- const derived = {};
2062
- for (const s of services) {
2063
- if (!s.adminUiServiceKey)
2064
- continue;
2065
- const port = s.resolvedPorts?.[0]?.host;
2066
- if (!port)
2067
- continue;
2068
- const path = s.adminUiPath ?? "/";
2069
- derived[s.adminUiServiceKey] = `http://127.0.0.1:${port}${path}`;
2192
+ var sessionIndexDeterministic = {
2193
+ id: 10,
2194
+ name: "session-index-deterministic",
2195
+ async up(ctx) {
2196
+ ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (earliest model/cwd/hostname)`);
2197
+ const { _rev, ...body } = SESSION_INDEX_DESIGN;
2198
+ await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
2199
+ },
2200
+ async down(ctx) {
2201
+ ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (deterministic pick left in place)`);
2070
2202
  }
2071
- const configured = Object.entries(fromConfig).filter(([k, v]) => STALE_TEMPLATE_LINKS[k] !== v);
2072
- return { ...derived, ...Object.fromEntries(configured) };
2203
+ };
2204
+ var MIGRATIONS = [
2205
+ initialSchema,
2206
+ sessionIndexView,
2207
+ sessionIndexSource,
2208
+ speakerSplitView,
2209
+ speakerSplitTimeView,
2210
+ chunkEntriesView,
2211
+ sessionIndexChunkCoverage,
2212
+ chunkEntryKind,
2213
+ sessionIndexEventTimes,
2214
+ sessionIndexDeterministic
2215
+ ];
2216
+ function latestVersion() {
2217
+ return MIGRATIONS.reduce((max, m) => Math.max(max, m.id), 0);
2073
2218
  }
2074
- function buildAppModel(config, env = {}) {
2075
- const version = env.CT_VERSION ?? "0.0.0-dev";
2076
- const services = SERVICES.map((s) => ({
2077
- ...s,
2078
- resolvedPorts: (s.ports ?? []).map((p) => ({
2079
- internal: p.internal,
2080
- host: env[p.hostEnv] ? Number(env[p.hostEnv]) : p.defaultHost,
2081
- label: p.label
2082
- }))
2083
- }));
2084
- return {
2085
- identity: {
2086
- codename: "claude-transcripts",
2087
- slug: config.app?.name ?? "claude-transcripts",
2088
- title: "Claude Transcripts",
2089
- version
2090
- },
2091
- services,
2092
- stores: {
2093
- databases: { ...config.couchdb.databases },
2094
- buckets: { ...config.s3.buckets }
2095
- },
2096
- hooks: HOOK_TYPES,
2097
- actions: ACTIONS,
2098
- bindings: BINDINGS,
2099
- routes: ROUTES,
2100
- topology: TOPOLOGY,
2101
- env: ENV_VARS,
2102
- features: config.features,
2103
- servicesMenu: buildServicesMenu(services, config.servicesMenu),
2104
- system: config.system,
2105
- recall: resolveRecall(config.recall, env),
2106
- userSettings: resolveUserSettings(config.userSettings),
2107
- cliSpec: CLI_SPEC
2108
- };
2219
+ function docTransformsBetween(from, to, migrations = MIGRATIONS) {
2220
+ return migrations.filter((m) => m.id > from && m.id <= to && m.transformsDocs === true).sort((a, b) => a.id - b.id).map((m) => ({ id: m.id, name: m.name }));
2109
2221
  }
2110
- // packages/shared/src/model/k8s.ts
2111
- var WORKLOAD_ROLES = new Set(["backing", "admin-ui", "app"]);
2112
2222
  // packages/shared/src/index.ts
2113
2223
  function usageWeight(u) {
2114
2224
  return (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);
@@ -2312,43 +2422,6 @@ function buildChunkEntries(jsonl) {
2312
2422
  return out;
2313
2423
  }
2314
2424
 
2315
- // packages/cli/src/lib/args.ts
2316
- function parseFlags(argv) {
2317
- const positionals = [];
2318
- const options = {};
2319
- for (let i = 0;i < argv.length; i++) {
2320
- const a = argv[i];
2321
- if (a === undefined)
2322
- continue;
2323
- if (a.startsWith("--")) {
2324
- const body = a.slice(2);
2325
- const eq = body.indexOf("=");
2326
- if (eq >= 0) {
2327
- options[body.slice(0, eq)] = body.slice(eq + 1);
2328
- } else {
2329
- const next = argv[i + 1];
2330
- if (next !== undefined && !next.startsWith("--")) {
2331
- options[body] = next;
2332
- i++;
2333
- } else {
2334
- options[body] = true;
2335
- }
2336
- }
2337
- } else {
2338
- positionals.push(a);
2339
- }
2340
- }
2341
- return { positionals, options };
2342
- }
2343
- function strOpt(opts, key) {
2344
- const v = opts[key];
2345
- return typeof v === "string" ? v : undefined;
2346
- }
2347
-
2348
- // packages/cli/src/lib/version.ts
2349
- var DEV_VERSION = "0.0.0-dev";
2350
- var VERSION = process.env.CT_VERSION ?? DEV_VERSION;
2351
-
2352
2425
  // packages/cli/src/commands/backfill.ts
2353
2426
  import { hostname } from "os";
2354
2427
 
@@ -2512,6 +2585,39 @@ async function putRaw(path, body, contentType) {
2512
2585
  }
2513
2586
  }
2514
2587
 
2588
+ // packages/cli/src/lib/args.ts
2589
+ function parseFlags(argv) {
2590
+ const positionals = [];
2591
+ const options = {};
2592
+ for (let i = 0;i < argv.length; i++) {
2593
+ const a = argv[i];
2594
+ if (a === undefined)
2595
+ continue;
2596
+ if (a.startsWith("--")) {
2597
+ const body = a.slice(2);
2598
+ const eq = body.indexOf("=");
2599
+ if (eq >= 0) {
2600
+ options[body.slice(0, eq)] = body.slice(eq + 1);
2601
+ } else {
2602
+ const next = argv[i + 1];
2603
+ if (next !== undefined && !next.startsWith("--")) {
2604
+ options[body] = next;
2605
+ i++;
2606
+ } else {
2607
+ options[body] = true;
2608
+ }
2609
+ }
2610
+ } else {
2611
+ positionals.push(a);
2612
+ }
2613
+ }
2614
+ return { positionals, options };
2615
+ }
2616
+ function strOpt(opts, key) {
2617
+ const v = opts[key];
2618
+ return typeof v === "string" ? v : undefined;
2619
+ }
2620
+
2515
2621
  // packages/cli/src/lib/claude-fs.ts
2516
2622
  import { readdir, readFile } from "fs/promises";
2517
2623
  import { homedir as homedir2 } from "os";
@@ -3373,9 +3479,9 @@ var EMBEDDED_ASSETS = {
3373
3479
  compose: {
3374
3480
  path: "deploy/docker-compose.yml",
3375
3481
  source: "deploy/docker-compose.yml",
3376
- content: `# GENERATED from the app model (@claude-transcripts/shared) by scripts/gen-compose.ts.
3377
- # Do NOT edit by hand \u2014 run \`bun run gen:compose\` to regenerate, and change the model
3378
- # (packages/shared/src/model/services.ts) instead. Inline edits here are lost silently.
3482
+ content: `# GENERATED from the blueprint (@claude-transcripts/shared) by scripts/gen-compose.ts.
3483
+ # Do NOT edit by hand \u2014 run \`bun run gen:compose\` to regenerate, and change the blueprint
3484
+ # (packages/shared/src/blueprint/services.ts) instead. Inline edits here are lost silently.
3379
3485
  # See docs/operate/containers.md.
3380
3486
  #
3381
3487
  # Claude Transcripts \u2014 backing services + app.
@@ -3466,6 +3572,18 @@ services:
3466
3572
  restart: unless-stopped
3467
3573
  networks:
3468
3574
  - claude-transcripts
3575
+ fossil:
3576
+ image: \${IMAGE_NS}/claude-transcripts-fossil:\${FOSSIL_TAG:-2.28}
3577
+ container_name: claude-transcripts-fossil
3578
+ environment:
3579
+ - FOSSIL_REPOSITORIES=\${FOSSIL_REPOSITORIES:-claude-transcripts-sessions}
3580
+ ports:
3581
+ - 127.0.0.1:\${FOSSIL_PORT:-7658}:8080
3582
+ volumes:
3583
+ - ./data/fossil:/museum
3584
+ restart: unless-stopped
3585
+ networks:
3586
+ - claude-transcripts
3469
3587
  meilisearch:
3470
3588
  image: \${IMAGE_NS}/claude-transcripts-meilisearch:\${MEILI_TAG:-v1.10}
3471
3589
  container_name: claude-transcripts-meilisearch
@@ -3497,6 +3615,7 @@ services:
3497
3615
  - couchdb
3498
3616
  - garage
3499
3617
  - meilisearch
3618
+ - fossil
3500
3619
  env_file: ../.env
3501
3620
  environment:
3502
3621
  - WEBAPI_HOST=0.0.0.0
@@ -3504,6 +3623,7 @@ services:
3504
3623
  - COUCHDB_URL=http://couchdb:5984
3505
3624
  - S3_ENDPOINT=http://garage:3900
3506
3625
  - MEILI_HOST=http://meilisearch:7700
3626
+ - FOSSIL_URL=http://fossil:8080
3507
3627
  ports:
3508
3628
  - 127.0.0.1:\${WEBAPI_PORT:-7650}:7650
3509
3629
  restart: unless-stopped
@@ -3518,7 +3638,7 @@ networks:
3518
3638
  composeUpstream: {
3519
3639
  path: "deploy/docker-compose.upstream.yml",
3520
3640
  source: "deploy/docker-compose.upstream.yml",
3521
- content: `# GENERATED from the app model (@claude-transcripts/shared) by scripts/gen-compose-override.ts.
3641
+ content: `# GENERATED from the blueprint (@claude-transcripts/shared) by scripts/gen-compose-override.ts.
3522
3642
  # Do NOT edit by hand \u2014 run \`bun run gen:compose-override\` to regenerate.
3523
3643
  #
3524
3644
  # Upstream-image dev override: pins each mirrored backing service to its canonical
@@ -3529,7 +3649,7 @@ networks:
3529
3649
  # bun run scripts/stack.ts up --upstream # the runner does this for you
3530
3650
  #
3531
3651
  # Tags still come from .env (COUCHDB_TAG, GARAGE_TAG, \u2026). Override an image here by
3532
- # changing its \`upstream\` in packages/shared/src/model/services.ts. The community
3652
+ # changing its \`upstream\` in packages/shared/src/blueprint/services.ts. The community
3533
3653
  # admin-UI images are best-effort; swap them if your registry differs.
3534
3654
 
3535
3655
  name: claude-transcripts
@@ -3540,6 +3660,10 @@ services:
3540
3660
  image: dxflrs/garage:\${GARAGE_TAG:-v2.3.0}
3541
3661
  garage-ui:
3542
3662
  image: khairul169/garage-webui:\${GARAGE_UI_TAG:-1.1.0}
3663
+ fossil:
3664
+ image: claude-transcripts-fossil:\${FOSSIL_TAG:-2.28}-local
3665
+ build:
3666
+ context: ./fossil
3543
3667
  meilisearch:
3544
3668
  image: getmeili/meilisearch:\${MEILI_TAG:-v1.10}
3545
3669
  meilisearch-ui:
@@ -3570,6 +3694,154 @@ api_bind_addr = "[::]:3900"
3570
3694
 
3571
3695
  [admin]
3572
3696
  api_bind_addr = "[::]:3903"
3697
+ `
3698
+ },
3699
+ fossilDockerfile: {
3700
+ path: "deploy/fossil/Dockerfile",
3701
+ source: "deploy/fossil/Dockerfile",
3702
+ content: `# Fossil SCM \u2014 built from the official release source (https://fossil-scm.org).
3703
+ #
3704
+ # Fossil publishes no container image, only a Dockerfile in its source tree
3705
+ # (www/containers.md). This is that file's recipe, pinned to a release instead of
3706
+ # trunk and verified by checksum: a static, musl-linked binary on an empty base, so
3707
+ # the image holds Fossil and nothing else. Hand-maintained (NOT generated); its
3708
+ # version must equal the \`fossil\` service's \`defaultTag\` in
3709
+ # packages/shared/src/blueprint/services.ts \u2014 a test holds the two together.
3710
+ #
3711
+ # Build context is this directory, and only entrypoint.sh is COPY'd from it, so the
3712
+ # image builds the same from a repo checkout, a user install (which ships both files),
3713
+ # or CI.
3714
+ #
3715
+ # To bump: take the release's tarball link from https://fossil-scm.org/home/uv/download.html,
3716
+ # set FOSSIL_VERSION / FOSSIL_CHECKIN / FOSSIL_SHA256, and the blueprint's defaultTag.
3717
+
3718
+ ## \u2500\u2500 build: a static Fossil binary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3719
+ FROM alpine:3.22 AS build
3720
+ RUN apk add --no-cache gcc make linux-headers musl-dev \\
3721
+ openssl-dev openssl-libs-static zlib-dev zlib-static
3722
+ ARG FOSSIL_VERSION=2.28
3723
+ # The release check-in (\`fossil version\` prints its prefix) and the sha256 of the
3724
+ # tarball fossil-scm.org serves for it.
3725
+ ARG FOSSIL_CHECKIN=52445a27f116603d784ad753c6acd494e0b02aeaf8502ee0fee0aa30030f4c4e
3726
+ ARG FOSSIL_SHA256=84c18824ca227e7602d2408b663c3747f754ad306ed5c73ddab959d6589538a6
3727
+ WORKDIR /fsl
3728
+ ADD --checksum=sha256:\${FOSSIL_SHA256} \\
3729
+ https://fossil-scm.org/home/tarball/\${FOSSIL_CHECKIN}/fossil-src-\${FOSSIL_VERSION}.tar.gz \\
3730
+ src.tar.gz
3731
+ RUN tar -xzf src.tar.gz --strip-components=1 \\
3732
+ && ./configure --static --json CFLAGS='-Os -s' \\
3733
+ && make -j"$(nproc)" \\
3734
+ && ./fossil version | grep -q "version \${FOSSIL_VERSION} "
3735
+
3736
+ ## \u2500\u2500 os: the few files a scratch image still needs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3737
+ # The musl variant: a static busybox, so the one binary runs on \`scratch\`.
3738
+ FROM busybox:1.37-musl AS os
3739
+ RUN mkdir -p /e /museum \\
3740
+ && echo "root:x:0:0:Admin:/:/false" > /e/passwd \\
3741
+ && echo "root:x:0:root" > /e/group
3742
+
3743
+ ## \u2500\u2500 run \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3744
+ FROM scratch
3745
+ COPY --from=build --chmod=755 /fsl/fossil /bin/fossil
3746
+ COPY --from=os /e/passwd /e/group /etc/
3747
+ COPY --from=os --chmod=1777 /tmp /tmp/
3748
+ COPY --from=os /museum /museum/
3749
+ # Only for the seed script (entrypoint.sh): one static binary, no applet links.
3750
+ COPY --from=os /bin/busybox /bin/busybox
3751
+ COPY --chmod=755 entrypoint.sh /bin/fossil-entrypoint
3752
+ ENV PATH=/bin
3753
+ EXPOSE 8080/tcp
3754
+ # Runs as root, unlike upstream's image (a fixed UID 499). The repo lives on a
3755
+ # bind mount or volume created by whoever runs the stack, and a fixed UID can't write
3756
+ # to a directory it doesn't own. Fossil handles root itself: serving a repository
3757
+ # file owned by a non-root user, it chroots into the repo's directory and drops to
3758
+ # that user (\`chown\` the repo to opt in). See https://fossil-scm.org/home/help/server.
3759
+ # Seeds the repositories named in FOSSIL_REPOSITORIES (from \`fossil.repositories\` in
3760
+ # config/), then execs \`fossil server\` with CMD \u2014 see entrypoint.sh.
3761
+ ENTRYPOINT ["/bin/busybox", "sh", "/bin/fossil-entrypoint"]
3762
+ # Serve the directory, not a file: every \`<name>.fossil\` in /museum is served at
3763
+ # /<name>/, and "/" lists them. Not upstream's \`--create\`, which makes a generic
3764
+ # repository with a generated admin login; ours are made by the seed.
3765
+ # --jsmode bundled: one JS file, fewer requests.
3766
+ CMD ["--repolist", "--jsmode", "bundled", "/museum"]
3767
+ `
3768
+ },
3769
+ fossilEntrypoint: {
3770
+ path: "deploy/fossil/entrypoint.sh",
3771
+ source: "deploy/fossil/entrypoint.sh",
3772
+ content: `#!/bin/sh
3773
+ # Seed, then serve. Run by busybox sh inside the Fossil image (deploy/fossil/Dockerfile).
3774
+ #
3775
+ # FOSSIL_REPOSITORIES names the repositories this instance owns, comma-separated \u2014
3776
+ # projected from \`fossil.repositories\` in config/ (ADR 0031). Each one missing from
3777
+ # the data directory is created here, because Fossil has no HTTP call that creates a
3778
+ # repository: the seed has to run where the binary and the volume are. Existing
3779
+ # repositories are never touched, so a restart is a no-op.
3780
+ #
3781
+ # A seeded repository needs no login to READ: the built-in \`nobody\` user can browse,
3782
+ # clone and download (ADR 0020). It can't write. Fossil's JSON API takes parameters
3783
+ # from the query string, so with write capabilities a plain GET could change the
3784
+ # repository \u2014 and any web page the user visits can send one to a localhost port
3785
+ # (an <img> tag is enough). The admin user exists because Fossil requires one; its
3786
+ # generated password is discarded, and \`fossil user password\` sets one when needed.
3787
+ #
3788
+ # FOSSIL_MUSEUM and FOSSIL_BUSYBOX exist for the tests (entrypoint.test.ts); the image
3789
+ # uses the defaults.
3790
+ set -euf # -f: an entry like \`*\` must stay a (rejected) name, not glob the cwd
3791
+
3792
+ museum="\${FOSSIL_MUSEUM:-/museum}"
3793
+ bb="\${FOSSIL_BUSYBOX-/bin/busybox}"
3794
+
3795
+ # Read-only for anonymous visitors: check-out/browse (o), clone (g), wiki (j), tickets
3796
+ # (r), hyperlinks (h), zip/tarball downloads (z).
3797
+ NOBODY_CAPS="ghjorz"
3798
+
3799
+ # What a new repository holds beyond \`fossil new\` and its users. A placeholder: the
3800
+ # seed's contents (users and roles, wiki, ticket schema, tags, settings) are still to
3801
+ # be defined \u2014 https://github.com/vredchenko/claude-transcripts/issues/210. Runs once,
3802
+ # on the not-yet-served copy, so anything added here must only ever create.
3803
+ seed_content() {
3804
+ : "$1"
3805
+ }
3806
+
3807
+ trim() {
3808
+ s="$1"
3809
+ while :; do
3810
+ case "$s" in
3811
+ [[:space:]]*) s="\${s#?}" ;;
3812
+ *[[:space:]]) s="\${s%?}" ;;
3813
+ *) break ;;
3814
+ esac
3815
+ done
3816
+ printf '%s' "$s"
3817
+ }
3818
+
3819
+ IFS=','
3820
+ for entry in \${FOSSIL_REPOSITORIES:-}; do
3821
+ name="$(trim "$entry")"
3822
+ [ -n "$name" ] || continue
3823
+ # The URL rules \`fossil server\` applies to a repository directory, so a name that
3824
+ # can't be served is refused here rather than created and then unreachable.
3825
+ case "$name" in
3826
+ -* | *[!A-Za-z0-9_-]*)
3827
+ echo "fossil-seed: skipping '$name' (letters, digits, '-' and '_' only)" >&2
3828
+ continue
3829
+ ;;
3830
+ esac
3831
+ repo="$museum/$name.fossil"
3832
+ [ -e "$repo" ] && continue
3833
+ # Built under a name \`--repolist\` doesn't serve, then renamed: a seed interrupted
3834
+ # half-way leaves no half-made repository to be served or skipped forever.
3835
+ $bb rm -f "$repo.new"
3836
+ fossil new --admin-user claude-transcripts --project-name "$name" "$repo.new" >/dev/null
3837
+ fossil user capabilities nobody "$NOBODY_CAPS" -R "$repo.new" >/dev/null
3838
+ seed_content "$repo.new"
3839
+ $bb mv "$repo.new" "$repo"
3840
+ echo "fossil-seed: created $repo (anonymous read-only)"
3841
+ done
3842
+ unset IFS
3843
+
3844
+ exec fossil server "$@"
3573
3845
  `
3574
3846
  },
3575
3847
  appConfig: {
@@ -3602,6 +3874,11 @@ api_bind_addr = "[::]:3903"
3602
3874
  "turns": "claude-transcripts-turns"
3603
3875
  }
3604
3876
  },
3877
+ "fossil": {
3878
+ "repositories": {
3879
+ "sessions": "claude-transcripts-sessions"
3880
+ }
3881
+ },
3605
3882
  "features": {
3606
3883
  "s3Blobs": true,
3607
3884
  "midFlightChunking": true,
@@ -3887,6 +4164,12 @@ import { dirname as dirname2 } from "path";
3887
4164
 
3888
4165
  // packages/cli/src/lib/statusline.ts
3889
4166
  import { statSync } from "fs";
4167
+
4168
+ // packages/cli/src/lib/version.ts
4169
+ var DEV_VERSION = "0.0.0-dev";
4170
+ var VERSION = process.env.CT_VERSION ?? DEV_VERSION;
4171
+
4172
+ // packages/cli/src/lib/statusline.ts
3890
4173
  var STALL_AFTER_MS = 60000;
3891
4174
  var HOOK_SILENT_AFTER_MS = 5 * 60000;
3892
4175
  function transcriptMtimeMs(path) {
@@ -4074,6 +4357,31 @@ import {
4074
4357
  import { hostname as hostname4 } from "os";
4075
4358
  var {S3Client } = globalThis.Bun;
4076
4359
 
4360
+ // packages/cli/src/hook/detach.ts
4361
+ import { spawn } from "child_process";
4362
+ function cliArgv() {
4363
+ const exec = process.execPath;
4364
+ if (!/\/bun$/.test(exec))
4365
+ return [exec];
4366
+ return [exec, "run", Bun.main];
4367
+ }
4368
+ function spawnDetached(argv) {
4369
+ const [cmd, ...args] = argv;
4370
+ if (!cmd)
4371
+ return false;
4372
+ try {
4373
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore", cwd: "/" });
4374
+ child.on("error", () => {});
4375
+ child.unref();
4376
+ return child.pid !== undefined;
4377
+ } catch {
4378
+ return false;
4379
+ }
4380
+ }
4381
+ function uploadArgv(sessionId, transcriptPath) {
4382
+ return [...cliArgv(), "hook", "upload", "--session", sessionId, "--transcript", transcriptPath];
4383
+ }
4384
+
4077
4385
  // packages/cli/src/hook/mirror.ts
4078
4386
  var DEFAULT_TIMEOUT_MS = 2000;
4079
4387
  var DEFAULT_BLOB_TIMEOUT_MS = 60000;
@@ -4531,7 +4839,8 @@ function buildContext(payload, config) {
4531
4839
  counts: makeCounts(sessionId),
4532
4840
  targets,
4533
4841
  sessionsDb,
4534
- sessionsBucket: config.blob?.buckets.sessions
4842
+ sessionsBucket: config.blob?.buckets.sessions,
4843
+ detachUpload: (sid, path) => spawnDetached(uploadArgv(sid, path))
4535
4844
  };
4536
4845
  }
4537
4846
  function commonFields(ctx) {
@@ -4822,16 +5131,21 @@ var writeSummary = async (ctx) => {
4822
5131
  }
4823
5132
  ctx.targets.clear();
4824
5133
  };
4825
- var uploadBlobs = async (ctx) => {
4826
- if (!ctx.blob.enabled || !ctx.sessionsBucket || !ctx.transcriptPath)
4827
- return;
5134
+ async function uploadTranscript(blob, bucket, sessionId, transcriptPath) {
4828
5135
  let data;
4829
5136
  try {
4830
- data = readFileSync5(ctx.transcriptPath, "utf8");
5137
+ data = readFileSync5(transcriptPath, "utf8");
4831
5138
  } catch {
4832
5139
  return;
4833
5140
  }
4834
- await ctx.blob.put(ctx.sessionsBucket, `${ctx.sessionId}/transcript.jsonl`, data, "application/x-ndjson");
5141
+ await blob.put(bucket, `${sessionId}/transcript.jsonl`, data, "application/x-ndjson");
5142
+ }
5143
+ var uploadBlobs = async (ctx) => {
5144
+ if (!ctx.blob.enabled || !ctx.sessionsBucket || !ctx.transcriptPath)
5145
+ return;
5146
+ if (ctx.detachUpload(ctx.sessionId, ctx.transcriptPath))
5147
+ return;
5148
+ await uploadTranscript(ctx.blob, ctx.sessionsBucket, ctx.sessionId, ctx.transcriptPath);
4835
5149
  };
4836
5150
  var HANDLERS = {
4837
5151
  "seed-session-start": seedSessionStart,
@@ -4846,15 +5160,15 @@ var HANDLERS = {
4846
5160
 
4847
5161
  // packages/cli/src/hook/index.ts
4848
5162
  function hookBindings() {
4849
- const model = buildAppModel(loadAppConfig(), process.env);
5163
+ const model = buildAppBlueprint(loadAppConfig(), process.env);
4850
5164
  const out = {};
4851
5165
  for (const b of model.bindings)
4852
5166
  out[b.event] = b.actions;
4853
5167
  return out;
4854
5168
  }
4855
- var LONG_TIMEOUT_EVENTS = new Set(["SessionStart", "SessionEnd"]);
5169
+ var TIMEOUTS = { SessionStart: 180, SessionEnd: 60 };
4856
5170
  function hookTimeout(event) {
4857
- return LONG_TIMEOUT_EVENTS.has(event) ? 180 : 5;
5171
+ return TIMEOUTS[event] ?? 5;
4858
5172
  }
4859
5173
  var SYNCHRONOUS_EVENTS = new Set(["SessionStart", "SessionEnd", "UserPromptSubmit"]);
4860
5174
  function hookAsync(event) {
@@ -4963,11 +5277,8 @@ function hookCommand() {
4963
5277
  return hookLaunch(`${cliInvocation()} hook run`);
4964
5278
  }
4965
5279
  function cliInvocation() {
4966
- const exec = process.execPath;
4967
- const isCompiled = !/\/bun$/.test(exec);
4968
- if (isCompiled)
4969
- return `"${exec}"`;
4970
- return `"${exec}" run "${Bun.main}"`;
5280
+ const [exec, , entry] = cliArgv();
5281
+ return entry ? `"${exec}" run "${entry}"` : `"${exec}"`;
4971
5282
  }
4972
5283
  function isOurCommand(command) {
4973
5284
  if (!command?.includes("hook run"))
@@ -5146,11 +5457,34 @@ async function run() {
5146
5457
  }
5147
5458
  return 0;
5148
5459
  }
5460
+ var UPLOAD_DEADLINE_MS = 10 * 60000;
5461
+ async function upload(argv) {
5462
+ setTimeout(() => process.exit(0), UPLOAD_DEADLINE_MS).unref();
5463
+ const { options } = parseFlags(argv);
5464
+ const sessionId = strOpt(options, "session");
5465
+ const transcriptPath = strOpt(options, "transcript");
5466
+ if (!sessionId || !transcriptPath) {
5467
+ console.error("hook: upload needs --session <id> --transcript <path>");
5468
+ return 0;
5469
+ }
5470
+ try {
5471
+ const config = loadHookConfig(installPaths().hookConfig);
5472
+ const bucket = config?.blob?.buckets.sessions;
5473
+ if (!config || !bucket)
5474
+ return 0;
5475
+ await uploadTranscript(makeBlob(config), bucket, sessionId, transcriptPath);
5476
+ } catch (err) {
5477
+ console.error("[hook] upload failed (ignored):", err);
5478
+ }
5479
+ return 0;
5480
+ }
5149
5481
  async function runHook(argv) {
5150
5482
  const [sub = "status", ...rest] = argv;
5151
5483
  switch (sub) {
5152
5484
  case "run":
5153
5485
  return run();
5486
+ case "upload":
5487
+ return upload(rest);
5154
5488
  case "install":
5155
5489
  return install(rest);
5156
5490
  case "uninstall":
@@ -5158,7 +5492,7 @@ async function runHook(argv) {
5158
5492
  case "status":
5159
5493
  return status();
5160
5494
  default:
5161
- console.error(`hook: unknown subcommand "${sub}" (run | install | uninstall | status)`);
5495
+ console.error(`hook: unknown subcommand "${sub}" (run | upload | install | uninstall | status)`);
5162
5496
  return 2;
5163
5497
  }
5164
5498
  }
@@ -5461,17 +5795,20 @@ function composeFiles(paths, upstream) {
5461
5795
 
5462
5796
  // packages/cli/src/lib/compose.ts
5463
5797
  var {$ } = globalThis.Bun;
5798
+ function storeEnv() {
5799
+ return toStoreEnv(buildAppBlueprint(loadAppConfig(), {}));
5800
+ }
5464
5801
  function fileArgs(paths) {
5465
5802
  return composeFiles(paths, true).flatMap((f) => ["-f", f]);
5466
5803
  }
5467
5804
  async function compose(opts, args) {
5468
5805
  const profile = opts.app ? ["--profile", "app"] : [];
5469
- await $`docker compose --env-file ${opts.paths.instanceEnv} ${fileArgs(opts.paths)} ${profile} ${args}`.env({ ...process.env, ...opts.env });
5806
+ await $`docker compose --env-file ${opts.paths.instanceEnv} ${fileArgs(opts.paths)} ${profile} ${args}`.env({ ...process.env, ...storeEnv(), ...opts.env });
5470
5807
  }
5471
5808
  async function composeQuiet(opts, args) {
5472
5809
  const profile = opts.app ? ["--profile", "app"] : [];
5473
5810
  try {
5474
- const text = await $`docker compose --env-file ${opts.paths.instanceEnv} ${fileArgs(opts.paths)} ${profile} ${args}`.env({ ...process.env, ...opts.env }).quiet().text();
5811
+ const text = await $`docker compose --env-file ${opts.paths.instanceEnv} ${fileArgs(opts.paths)} ${profile} ${args}`.env({ ...process.env, ...storeEnv(), ...opts.env }).quiet().text();
5475
5812
  return { ok: true, text };
5476
5813
  } catch (err) {
5477
5814
  return { ok: false, text: String(err.stderr ?? err) };
@@ -5659,7 +5996,8 @@ var PORT_KEYS = [
5659
5996
  "GARAGE_ADMIN_PORT",
5660
5997
  "GARAGE_WEBUI_PORT",
5661
5998
  "MEILI_PORT",
5662
- "MEILI_UI_PORT"
5999
+ "MEILI_UI_PORT",
6000
+ "FOSSIL_PORT"
5663
6001
  ];
5664
6002
  function secret(bytes = 32) {
5665
6003
  return randomBytes(bytes).toString("hex");
@@ -5707,6 +6045,7 @@ function buildInstanceEnv(opts = {}) {
5707
6045
  GARAGE_UI_TAG: "1.1.0",
5708
6046
  MEILI_TAG: "v1.10",
5709
6047
  MEILI_UI_TAG: "latest",
6048
+ FOSSIL_TAG: "2.28",
5710
6049
  COUCHDB_URL: "",
5711
6050
  COUCHDB_HOST: "127.0.0.1",
5712
6051
  COUCHDB_USER: "admin",
@@ -5719,6 +6058,7 @@ function buildInstanceEnv(opts = {}) {
5719
6058
  GARAGE_ADMIN_TOKEN: secret(32),
5720
6059
  GARAGE_METRICS_TOKEN: secret(32),
5721
6060
  MEILI_HOST: `http://127.0.0.1:${ports.MEILI_PORT}`,
6061
+ FOSSIL_URL: `http://127.0.0.1:${ports.FOSSIL_PORT}`,
5722
6062
  MEILI_MASTER_KEY: opts.meiliMasterKey ? secret(24) : "",
5723
6063
  MEILI_API_KEY: ""
5724
6064
  };
@@ -5729,10 +6069,24 @@ var HEADER = [
5729
6069
  "Rotating a secret here does NOT rotate it in an already-provisioned store."
5730
6070
  ];
5731
6071
  var INSTALLER_OWNED = ["APP_TAG"];
6072
+ function backfillPorts(existing) {
6073
+ const base = Number(existing.WEBAPI_PORT);
6074
+ if (!Number.isInteger(base) || base <= 0)
6075
+ return {};
6076
+ const out = {};
6077
+ PORT_KEYS.forEach((key, i) => {
6078
+ if (existing[key] === undefined)
6079
+ out[key] = String(base + i);
6080
+ });
6081
+ if (out.FOSSIL_PORT && existing.FOSSIL_URL === undefined) {
6082
+ out.FOSSIL_URL = `http://127.0.0.1:${out.FOSSIL_PORT}`;
6083
+ }
6084
+ return out;
6085
+ }
5732
6086
  function loadOrCreateInstanceEnv(path, opts = {}) {
5733
6087
  const generated = buildInstanceEnv(opts);
5734
6088
  const existing = existsSync6(path) ? parseEnv(readFileSync10(path, "utf8")) : {};
5735
- const merged = { ...generated, ...existing };
6089
+ const merged = { ...generated, ...backfillPorts(existing), ...existing };
5736
6090
  for (const key of INSTALLER_OWNED) {
5737
6091
  if (generated[key] !== undefined)
5738
6092
  merged[key] = generated[key];
@@ -6177,6 +6531,29 @@ async function resolvePortBase(base, span) {
6177
6531
  }
6178
6532
  return null;
6179
6533
  }
6534
+ async function relocateAddedPorts(path, env, priorKeys) {
6535
+ const added = PORT_KEYS.filter((k) => !priorKeys.has(k));
6536
+ if (added.length === 0)
6537
+ return env;
6538
+ const own = new Set(PORT_KEYS.map((k) => Number(env[k])));
6539
+ const patch = {};
6540
+ for (const key of added) {
6541
+ let port = Number(env[key]);
6542
+ const isOwnDuplicate = PORT_KEYS.some((k) => k !== key && Number(env[k]) === port);
6543
+ if (!isOwnDuplicate && await isPortFree(port))
6544
+ continue;
6545
+ do
6546
+ port += 1;
6547
+ while (own.has(port) || !await isPortFree(port));
6548
+ own.add(port);
6549
+ patch[key] = String(port);
6550
+ console.log(` \u2192 ${key} ${env[key]} is taken; using ${port}`);
6551
+ }
6552
+ if (patch.FOSSIL_PORT && !priorKeys.has("FOSSIL_URL")) {
6553
+ patch.FOSSIL_URL = `http://127.0.0.1:${patch.FOSSIL_PORT}`;
6554
+ }
6555
+ return Object.keys(patch).length ? updateInstanceEnv(path, patch) : env;
6556
+ }
6180
6557
  async function warnOnVersionSkew(env) {
6181
6558
  try {
6182
6559
  const res = await fetch(`http://127.0.0.1:${env.WEBAPI_PORT}/health`, {
@@ -6248,12 +6625,15 @@ async function runInstall(argv) {
6248
6625
  }
6249
6626
  step(2, TOTAL, "Configuration");
6250
6627
  const firstRun = !existsSync8(paths.instanceEnv);
6628
+ const priorKeys = new Set(firstRun ? [] : Object.keys(parseEnv(readFileSync12(paths.instanceEnv, "utf8"))));
6251
6629
  let env = loadOrCreateInstanceEnv(paths.instanceEnv, {
6252
6630
  portBase,
6253
6631
  appTag: VERSION === DEV_VERSION ? "main" : VERSION,
6254
6632
  meiliMasterKey: meiliKey
6255
6633
  });
6256
6634
  console.log(` ${firstRun ? "generated" : "reused"} ${paths.instanceEnv}`);
6635
+ if (!firstRun)
6636
+ env = await relocateAddedPorts(paths.instanceEnv, env, priorKeys);
6257
6637
  if (!firstRun && priorAppTag && priorAppTag !== env.APP_TAG) {
6258
6638
  console.log(` app image pin ${priorAppTag} \u2192 ${env.APP_TAG} (follows this CLI)`);
6259
6639
  }