@claude-transcripts/cli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// packages/shared/src/
|
|
2
|
+
// packages/shared/src/blueprint/cli-project.ts
|
|
3
3
|
var isFlag = (a) => a.name.startsWith("--");
|
|
4
4
|
function cliArgType(a) {
|
|
5
5
|
if (a.type)
|
|
@@ -380,7 +380,7 @@ ${lines.join(`
|
|
|
380
380
|
`;
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
// packages/shared/src/
|
|
383
|
+
// packages/shared/src/blueprint/cli.ts
|
|
384
384
|
var WEBAPI = {
|
|
385
385
|
name: "--webapi",
|
|
386
386
|
type: "string",
|
|
@@ -717,470 +717,61 @@ var CLI_SPEC = {
|
|
|
717
717
|
]
|
|
718
718
|
};
|
|
719
719
|
|
|
720
|
-
// packages/shared/src/
|
|
721
|
-
var
|
|
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 = [
|
|
720
|
+
// packages/shared/src/blueprint/actions.ts
|
|
721
|
+
var ACTIONS = [
|
|
775
722
|
{
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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
|
-
}
|
|
723
|
+
key: "write-event-marker",
|
|
724
|
+
summary: "Append a light append-only event marker doc",
|
|
725
|
+
implemented: true
|
|
788
726
|
},
|
|
789
727
|
{
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
}
|
|
728
|
+
key: "update-counts",
|
|
729
|
+
summary: "Bump per-session counters (events/prompts/errors/tools)",
|
|
730
|
+
implemented: true
|
|
801
731
|
},
|
|
802
732
|
{
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
}
|
|
733
|
+
key: "flush-transcript-chunk",
|
|
734
|
+
summary: "Tail the transcript \u2192 append chunk docs",
|
|
735
|
+
implemented: true
|
|
817
736
|
},
|
|
737
|
+
{ key: "write-summary", summary: "Write summary:<id> at session end", implemented: true },
|
|
818
738
|
{
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
-
}
|
|
739
|
+
key: "upload-blobs",
|
|
740
|
+
summary: "Upload summary.json + transcript.jsonl to S3",
|
|
741
|
+
implemented: true
|
|
827
742
|
},
|
|
828
|
-
CHUNKS_DESIGN,
|
|
829
743
|
{
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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);
|
|
1082
|
-
},
|
|
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);
|
|
1094
|
-
},
|
|
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);
|
|
1106
|
-
},
|
|
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 = [
|
|
1131
|
-
{
|
|
1132
|
-
key: "write-event-marker",
|
|
1133
|
-
summary: "Append a light append-only event marker doc",
|
|
1134
|
-
implemented: true
|
|
1135
|
-
},
|
|
1136
|
-
{
|
|
1137
|
-
key: "update-counts",
|
|
1138
|
-
summary: "Bump per-session counters (events/prompts/errors/tools)",
|
|
1139
|
-
implemented: true
|
|
1140
|
-
},
|
|
1141
|
-
{
|
|
1142
|
-
key: "flush-transcript-chunk",
|
|
1143
|
-
summary: "Tail the transcript \u2192 append chunk docs",
|
|
1144
|
-
implemented: true
|
|
1145
|
-
},
|
|
1146
|
-
{ key: "write-summary", summary: "Write summary:<id> at session end", implemented: true },
|
|
1147
|
-
{
|
|
1148
|
-
key: "upload-blobs",
|
|
1149
|
-
summary: "Upload summary.json + transcript.jsonl to S3",
|
|
1150
|
-
implemented: true
|
|
1151
|
-
},
|
|
1152
|
-
{
|
|
1153
|
-
key: "seed-session-start",
|
|
1154
|
-
summary: "Reset counters + seed the chunk offset at start",
|
|
1155
|
-
implemented: true
|
|
1156
|
-
},
|
|
1157
|
-
{
|
|
1158
|
-
key: "announce-recording",
|
|
1159
|
-
summary: "Tell the user, in the transcript, whether and where this session is recorded",
|
|
1160
|
-
implemented: true
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
key: "inject-recall-policy",
|
|
1164
|
-
summary: "Prime the session with the recall policy + how much history this cwd has",
|
|
1165
|
-
implemented: true
|
|
1166
|
-
},
|
|
1167
|
-
{ key: "enrich-metadata", summary: "Post additional session metadata", implemented: false },
|
|
1168
|
-
{
|
|
1169
|
-
key: "extract-feature",
|
|
1170
|
-
summary: "Map-reduce feature extraction (urls/repos/PRs/\u2026)",
|
|
1171
|
-
implemented: false
|
|
1172
|
-
},
|
|
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
|
-
{
|
|
1176
|
-
key: "index-for-search",
|
|
1177
|
-
summary: "Push derived content to the search backend",
|
|
1178
|
-
implemented: false
|
|
1179
|
-
},
|
|
1180
|
-
{
|
|
1181
|
-
key: "reconcile-session",
|
|
1182
|
-
summary: "Finalize a stale running session from chunks/S3",
|
|
1183
|
-
implemented: false
|
|
744
|
+
key: "seed-session-start",
|
|
745
|
+
summary: "Reset counters + seed the chunk offset at start",
|
|
746
|
+
implemented: true
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
key: "announce-recording",
|
|
750
|
+
summary: "Tell the user, in the transcript, whether and where this session is recorded",
|
|
751
|
+
implemented: true
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
key: "inject-recall-policy",
|
|
755
|
+
summary: "Prime the session with the recall policy + how much history this cwd has",
|
|
756
|
+
implemented: true
|
|
757
|
+
},
|
|
758
|
+
{ key: "enrich-metadata", summary: "Post additional session metadata", implemented: false },
|
|
759
|
+
{
|
|
760
|
+
key: "extract-feature",
|
|
761
|
+
summary: "Map-reduce feature extraction (urls/repos/PRs/\u2026)",
|
|
762
|
+
implemented: false
|
|
763
|
+
},
|
|
764
|
+
{ key: "detect-memory-write", summary: "Flag Edit/Write to memory paths", implemented: false },
|
|
765
|
+
{ key: "app-log", summary: "Emit an operational log record (app-logs DB)", implemented: false },
|
|
766
|
+
{
|
|
767
|
+
key: "index-for-search",
|
|
768
|
+
summary: "Push derived content to the search backend",
|
|
769
|
+
implemented: false
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
key: "reconcile-session",
|
|
773
|
+
summary: "Finalize a stale running session from chunks/S3",
|
|
774
|
+
implemented: false
|
|
1184
775
|
}
|
|
1185
776
|
];
|
|
1186
777
|
var BINDINGS = [
|
|
@@ -1213,7 +804,7 @@ var BINDINGS = [
|
|
|
1213
804
|
{ event: "PostCompact", actions: ["write-event-marker"] },
|
|
1214
805
|
{ event: "SessionEnd", actions: ["flush-transcript-chunk", "write-summary", "upload-blobs"] }
|
|
1215
806
|
];
|
|
1216
|
-
// packages/shared/src/
|
|
807
|
+
// packages/shared/src/blueprint/env.ts
|
|
1217
808
|
var ENV_VARS = [
|
|
1218
809
|
{
|
|
1219
810
|
name: "IMAGE_NS",
|
|
@@ -1223,6 +814,7 @@ var ENV_VARS = [
|
|
|
1223
814
|
{ name: "COUCHDB_TAG", scope: "image", default: "3", description: "CouchDB image tag" },
|
|
1224
815
|
{ name: "GARAGE_TAG", scope: "image", default: "v2.3.0", description: "Garage image tag" },
|
|
1225
816
|
{ name: "GARAGE_UI_TAG", scope: "image", default: "1.1.0", description: "Garage UI image tag" },
|
|
817
|
+
{ name: "FOSSIL_TAG", scope: "image", default: "2.28", description: "Fossil image tag" },
|
|
1226
818
|
{ name: "MEILI_TAG", scope: "image", default: "v1.10", description: "Meilisearch image tag" },
|
|
1227
819
|
{
|
|
1228
820
|
name: "MEILI_UI_TAG",
|
|
@@ -1259,6 +851,12 @@ var ENV_VARS = [
|
|
|
1259
851
|
default: "7657",
|
|
1260
852
|
description: "Meilisearch UI host port"
|
|
1261
853
|
},
|
|
854
|
+
{
|
|
855
|
+
name: "FOSSIL_PORT",
|
|
856
|
+
scope: "port",
|
|
857
|
+
default: "7658",
|
|
858
|
+
description: "Fossil web UI + sync host port"
|
|
859
|
+
},
|
|
1262
860
|
{ name: "WEBAPI_HOST", scope: "host", default: "127.0.0.1", description: "webapi bind host" },
|
|
1263
861
|
{ name: "WEBUI_HOST", scope: "host", default: "127.0.0.1", description: "webui bind host" },
|
|
1264
862
|
{ name: "CT_STATIC_DIR", scope: "host", description: "prebuilt SPA dir (prod image only)" },
|
|
@@ -1293,6 +891,12 @@ var ENV_VARS = [
|
|
|
1293
891
|
default: "http://127.0.0.1:7656",
|
|
1294
892
|
description: "Meilisearch endpoint URL"
|
|
1295
893
|
},
|
|
894
|
+
{
|
|
895
|
+
name: "FOSSIL_URL",
|
|
896
|
+
scope: "endpoint",
|
|
897
|
+
default: "http://127.0.0.1:7658",
|
|
898
|
+
description: "Fossil server base URL (serves each repository at /<name>/)"
|
|
899
|
+
},
|
|
1296
900
|
{
|
|
1297
901
|
name: "MEILI_API_KEY",
|
|
1298
902
|
scope: "secret",
|
|
@@ -1300,7 +904,7 @@ var ENV_VARS = [
|
|
|
1300
904
|
}
|
|
1301
905
|
];
|
|
1302
906
|
|
|
1303
|
-
// packages/shared/src/
|
|
907
|
+
// packages/shared/src/blueprint/hooks.ts
|
|
1304
908
|
var HOOK_TYPES = [
|
|
1305
909
|
{
|
|
1306
910
|
event: "SessionStart",
|
|
@@ -1537,7 +1141,7 @@ function hookLaunch(command) {
|
|
|
1537
1141
|
}
|
|
1538
1142
|
var PLUGIN_HOOK_COMMAND = hookLaunch('bun run "${CLAUDE_PLUGIN_ROOT}/scripts/dispatch.ts"');
|
|
1539
1143
|
|
|
1540
|
-
// packages/shared/src/
|
|
1144
|
+
// packages/shared/src/blueprint/recall.ts
|
|
1541
1145
|
var RECALL_MODES = ["off", "suggest", "auto"];
|
|
1542
1146
|
var RECALL_SCOPES = ["project", "host", "all"];
|
|
1543
1147
|
var DEFAULT_RECALL = {
|
|
@@ -1628,30 +1232,59 @@ function joinOr(items) {
|
|
|
1628
1232
|
return `${items.slice(0, -1).join(", ")}, or ${items[items.length - 1]}`;
|
|
1629
1233
|
}
|
|
1630
1234
|
|
|
1631
|
-
// packages/shared/src/
|
|
1235
|
+
// packages/shared/src/blueprint/routes.ts
|
|
1632
1236
|
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" },
|
|
1643
1237
|
{
|
|
1644
|
-
path: "/
|
|
1238
|
+
path: "/",
|
|
1239
|
+
serves: "machine-readable app manifest (agent entrypoint)",
|
|
1240
|
+
nav: { label: "App manifest", section: "Reference" }
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
path: "/health",
|
|
1244
|
+
serves: "liveness + store reachability",
|
|
1245
|
+
nav: { label: "Health", section: "Reference" }
|
|
1246
|
+
},
|
|
1247
|
+
{ path: "/api", serves: "app JSON API (the gateway)" },
|
|
1248
|
+
{
|
|
1249
|
+
path: "/api/docs",
|
|
1250
|
+
serves: "Scalar API reference (renders the OpenAPI spec)",
|
|
1251
|
+
nav: { label: "API reference", section: "Reference" }
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
path: "/api/openapi.json",
|
|
1255
|
+
serves: "OpenAPI spec (contract source of truth)",
|
|
1256
|
+
nav: { label: "OpenAPI spec", section: "Reference" }
|
|
1257
|
+
},
|
|
1258
|
+
{ path: "/api/couch/*", serves: "read-only CouchDB proxy" },
|
|
1259
|
+
{ path: "/api/s3/*", serves: "read-only S3 proxy" },
|
|
1260
|
+
{ path: "/api/fossil/*", serves: "read-only Fossil JSON API proxy" },
|
|
1261
|
+
{ path: "/api/sessions", serves: "session list / detail / transcript / turns" },
|
|
1262
|
+
{ path: "/api/turns", serves: "turns across all sessions, time-ordered by speaker" },
|
|
1263
|
+
{ path: "/api/search", serves: "full-text search over sessions + conversation content" },
|
|
1264
|
+
{
|
|
1265
|
+
path: "/api/ingest",
|
|
1645
1266
|
serves: "curated write surface (summary/events/chunks/transcript, reset)"
|
|
1646
1267
|
},
|
|
1647
1268
|
{ path: "/api/migrate", serves: "schema migration status / up / down" },
|
|
1648
|
-
{
|
|
1269
|
+
{
|
|
1270
|
+
path: "/api/blueprint",
|
|
1271
|
+
serves: "app blueprint introspection (services/hooks/actions/env)",
|
|
1272
|
+
nav: { label: "App blueprint", section: "Reference" }
|
|
1273
|
+
},
|
|
1649
1274
|
{ path: "/app", serves: "webui SPA" },
|
|
1650
|
-
{
|
|
1651
|
-
|
|
1275
|
+
{
|
|
1276
|
+
path: "/docs",
|
|
1277
|
+
serves: "rendered technical docs",
|
|
1278
|
+
nav: { label: "Technical docs", section: "Reference" }
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
path: "/cli/download",
|
|
1282
|
+
serves: "the bundled CLI binary",
|
|
1283
|
+
nav: { label: "Download CLI", section: "Install" }
|
|
1284
|
+
}
|
|
1652
1285
|
];
|
|
1653
1286
|
|
|
1654
|
-
// packages/shared/src/
|
|
1287
|
+
// packages/shared/src/blueprint/services.ts
|
|
1655
1288
|
var RELEASE_IMAGE_NS = "ghcr.io/vredchenko";
|
|
1656
1289
|
var SERVICES = [
|
|
1657
1290
|
{
|
|
@@ -1687,6 +1320,7 @@ var SERVICES = [
|
|
|
1687
1320
|
],
|
|
1688
1321
|
adminUiServiceKey: "couchdbFauxton",
|
|
1689
1322
|
adminUiPath: "/_utils/",
|
|
1323
|
+
holds: "databases",
|
|
1690
1324
|
volumes: [{ host: "./data/couchdb", container: "/opt/couchdb/data" }],
|
|
1691
1325
|
containerEnv: {
|
|
1692
1326
|
COUCHDB_USER: "${COUCHDB_USER:-admin}",
|
|
@@ -1709,6 +1343,7 @@ var SERVICES = [
|
|
|
1709
1343
|
{ internal: 3900, hostEnv: "GARAGE_S3_PORT", defaultHost: 7653, label: "S3 API" },
|
|
1710
1344
|
{ internal: 3903, hostEnv: "GARAGE_ADMIN_PORT", defaultHost: 7654, label: "admin API" }
|
|
1711
1345
|
],
|
|
1346
|
+
holds: "buckets",
|
|
1712
1347
|
volumes: [
|
|
1713
1348
|
{ host: "./data/garage/meta", container: "/var/lib/garage/meta" },
|
|
1714
1349
|
{ host: "./data/garage/data", container: "/var/lib/garage/data" },
|
|
@@ -1742,6 +1377,24 @@ var SERVICES = [
|
|
|
1742
1377
|
S3_REGION: "garage"
|
|
1743
1378
|
}
|
|
1744
1379
|
},
|
|
1380
|
+
{
|
|
1381
|
+
key: "fossil",
|
|
1382
|
+
name: "Fossil",
|
|
1383
|
+
role: "backing",
|
|
1384
|
+
image: {
|
|
1385
|
+
name: "fossil",
|
|
1386
|
+
tagEnv: "FOSSIL_TAG",
|
|
1387
|
+
defaultTag: "2.28",
|
|
1388
|
+
build: { context: "./fossil" }
|
|
1389
|
+
},
|
|
1390
|
+
ports: [{ internal: 8080, hostEnv: "FOSSIL_PORT", defaultHost: 7658, label: "web UI + sync" }],
|
|
1391
|
+
adminUiServiceKey: "fossil",
|
|
1392
|
+
holds: "repositories",
|
|
1393
|
+
volumes: [{ host: "./data/fossil", container: "/museum" }],
|
|
1394
|
+
containerEnv: { FOSSIL_REPOSITORIES: "${FOSSIL_REPOSITORIES:-claude-transcripts-sessions}" },
|
|
1395
|
+
tcpHealth: { port: 8080 },
|
|
1396
|
+
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."
|
|
1397
|
+
},
|
|
1745
1398
|
{
|
|
1746
1399
|
key: "meilisearch",
|
|
1747
1400
|
name: "Meilisearch",
|
|
@@ -1754,6 +1407,7 @@ var SERVICES = [
|
|
|
1754
1407
|
},
|
|
1755
1408
|
ports: [{ internal: 7700, hostEnv: "MEILI_PORT", defaultHost: 7656 }],
|
|
1756
1409
|
adminUiServiceKey: "meilisearch",
|
|
1410
|
+
holds: "indexes",
|
|
1757
1411
|
volumes: [{ host: "./data/meilisearch", container: "/meili_data" }],
|
|
1758
1412
|
containerEnv: { MEILI_ENV: "development" },
|
|
1759
1413
|
notes: "Derived search index (optional/removable). Built-in UI on /."
|
|
@@ -1779,21 +1433,22 @@ var SERVICES = [
|
|
|
1779
1433
|
image: { name: "app", tagEnv: "APP_TAG", defaultTag: "latest" },
|
|
1780
1434
|
ports: [{ internal: 7650, hostEnv: "WEBAPI_PORT", defaultHost: 7650 }],
|
|
1781
1435
|
profiles: ["app"],
|
|
1782
|
-
dependsOn: ["couchdb", "garage", "meilisearch"],
|
|
1436
|
+
dependsOn: ["couchdb", "garage", "meilisearch", "fossil"],
|
|
1783
1437
|
envFile: "../.env",
|
|
1784
1438
|
containerEnv: {
|
|
1785
1439
|
WEBAPI_HOST: "0.0.0.0",
|
|
1786
1440
|
WEBAPI_PORT: "7650",
|
|
1787
1441
|
COUCHDB_URL: "http://couchdb:5984",
|
|
1788
1442
|
S3_ENDPOINT: "http://garage:3900",
|
|
1789
|
-
MEILI_HOST: "http://meilisearch:7700"
|
|
1443
|
+
MEILI_HOST: "http://meilisearch:7700",
|
|
1444
|
+
FOSSIL_URL: "http://fossil:8080"
|
|
1790
1445
|
},
|
|
1791
1446
|
httpHealth: { path: "/health", port: 7650 },
|
|
1792
1447
|
notes: "webapi + webui SPA + Scalar + bundled CLI; serves /app + /api."
|
|
1793
1448
|
}
|
|
1794
1449
|
];
|
|
1795
1450
|
|
|
1796
|
-
// packages/shared/src/
|
|
1451
|
+
// packages/shared/src/blueprint/topology.ts
|
|
1797
1452
|
var TOPOLOGY_NODES = [
|
|
1798
1453
|
{
|
|
1799
1454
|
key: "claude-code",
|
|
@@ -1898,6 +1553,18 @@ var TOPOLOGY_NODES = [
|
|
|
1898
1553
|
requiresFeature: "meilisearch",
|
|
1899
1554
|
summary: "Rebuildable full-text index. Optional \u2014 losing it costs search, not history."
|
|
1900
1555
|
},
|
|
1556
|
+
{
|
|
1557
|
+
key: "fossil",
|
|
1558
|
+
serviceKey: "fossil",
|
|
1559
|
+
label: "Fossil",
|
|
1560
|
+
caption: "version control",
|
|
1561
|
+
role: "store",
|
|
1562
|
+
icon: "fossil",
|
|
1563
|
+
rank: 3,
|
|
1564
|
+
lane: "store",
|
|
1565
|
+
level: "compact",
|
|
1566
|
+
summary: "Repositories, wiki and tickets with a web UI, from one binary. Read through the gateway; nothing writes it yet."
|
|
1567
|
+
},
|
|
1901
1568
|
{
|
|
1902
1569
|
key: "couchdb-fauxton",
|
|
1903
1570
|
serviceKey: "couchdb",
|
|
@@ -1974,6 +1641,14 @@ var TOPOLOGY_EDGES = [
|
|
|
1974
1641
|
level: "compact",
|
|
1975
1642
|
requiresFeature: "s3Blobs"
|
|
1976
1643
|
},
|
|
1644
|
+
{
|
|
1645
|
+
from: "webapi",
|
|
1646
|
+
to: "fossil",
|
|
1647
|
+
kind: "read-only-proxy",
|
|
1648
|
+
label: "reads \xB7 /api/fossil",
|
|
1649
|
+
level: "compact",
|
|
1650
|
+
note: "Fossil's JSON API, allowlisted to read-only commands: over GET it would otherwise write."
|
|
1651
|
+
},
|
|
1977
1652
|
{
|
|
1978
1653
|
from: "webapi",
|
|
1979
1654
|
to: "meilisearch",
|
|
@@ -1982,133 +1657,562 @@ var TOPOLOGY_EDGES = [
|
|
|
1982
1657
|
level: "compact",
|
|
1983
1658
|
requiresFeature: "meilisearch"
|
|
1984
1659
|
},
|
|
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."
|
|
1660
|
+
{
|
|
1661
|
+
from: "webapi",
|
|
1662
|
+
to: "couchdb",
|
|
1663
|
+
kind: "curated-write",
|
|
1664
|
+
label: "/api/ingest",
|
|
1665
|
+
level: "expanded",
|
|
1666
|
+
note: "The curated write surface that owns the document shapes; writes are never proxied."
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
from: "couchdb",
|
|
1670
|
+
to: "webapi",
|
|
1671
|
+
kind: "changes-follower",
|
|
1672
|
+
label: "_changes",
|
|
1673
|
+
level: "expanded",
|
|
1674
|
+
note: "Exists because of the hook bypass: hook-written docs get no write-time validation."
|
|
1675
|
+
},
|
|
1676
|
+
{ from: "couchdb-fauxton", to: "couchdb", kind: "admin-of", level: "expanded" },
|
|
1677
|
+
{ from: "garage-ui", to: "garage", kind: "admin-of", level: "expanded" },
|
|
1678
|
+
{ from: "meilisearch-ui", to: "meilisearch", kind: "admin-of", level: "expanded" }
|
|
1679
|
+
];
|
|
1680
|
+
var TOPOLOGY_GROUPS = [
|
|
1681
|
+
{
|
|
1682
|
+
key: "clients",
|
|
1683
|
+
title: "read / query",
|
|
1684
|
+
members: ["webui", "cli", "agents"],
|
|
1685
|
+
level: "compact"
|
|
1686
|
+
},
|
|
1687
|
+
{
|
|
1688
|
+
key: "stores",
|
|
1689
|
+
title: "your infrastructure",
|
|
1690
|
+
members: ["couchdb", "garage", "meilisearch", "fossil"],
|
|
1691
|
+
level: "compact"
|
|
1692
|
+
}
|
|
1693
|
+
];
|
|
1694
|
+
var TOPOLOGY = {
|
|
1695
|
+
nodes: TOPOLOGY_NODES,
|
|
1696
|
+
edges: TOPOLOGY_EDGES,
|
|
1697
|
+
groups: TOPOLOGY_GROUPS
|
|
1698
|
+
};
|
|
1699
|
+
|
|
1700
|
+
// packages/shared/src/blueprint/user-settings.ts
|
|
1701
|
+
var DEFAULT_USER_SETTINGS = {
|
|
1702
|
+
sessionListPageSize: 100,
|
|
1703
|
+
transcriptPageSize: 100,
|
|
1704
|
+
transcriptAutoLoadMax: 2000
|
|
1705
|
+
};
|
|
1706
|
+
var LIMITS = {
|
|
1707
|
+
sessionListPageSize: 500,
|
|
1708
|
+
transcriptPageSize: 500,
|
|
1709
|
+
transcriptAutoLoadMax: 50000
|
|
1710
|
+
};
|
|
1711
|
+
function resolveUserSettings(fromConfig) {
|
|
1712
|
+
const c = fromConfig ?? {};
|
|
1713
|
+
return {
|
|
1714
|
+
sessionListPageSize: clamp(c.sessionListPageSize, "sessionListPageSize"),
|
|
1715
|
+
transcriptPageSize: clamp(c.transcriptPageSize, "transcriptPageSize"),
|
|
1716
|
+
transcriptAutoLoadMax: clamp(c.transcriptAutoLoadMax, "transcriptAutoLoadMax")
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function clamp(value, key) {
|
|
1720
|
+
const fallback = DEFAULT_USER_SETTINGS[key];
|
|
1721
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1722
|
+
return fallback;
|
|
1723
|
+
const rounded = Math.floor(value);
|
|
1724
|
+
if (rounded < 1)
|
|
1725
|
+
return fallback;
|
|
1726
|
+
return Math.min(rounded, LIMITS[key]);
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// packages/shared/src/blueprint/build.ts
|
|
1730
|
+
var STALE_TEMPLATE_LINKS = {
|
|
1731
|
+
couchdbFauxton: "http://127.0.0.1:7652/_utils/",
|
|
1732
|
+
garageWebui: "http://127.0.0.1:7655/",
|
|
1733
|
+
meilisearch: "http://127.0.0.1:7656/"
|
|
1734
|
+
};
|
|
1735
|
+
function buildServicesMenu(services, fromConfig) {
|
|
1736
|
+
const derived = {};
|
|
1737
|
+
for (const s of services) {
|
|
1738
|
+
if (!s.adminUiServiceKey)
|
|
1739
|
+
continue;
|
|
1740
|
+
const port = s.resolvedPorts?.[0]?.host;
|
|
1741
|
+
if (!port)
|
|
1742
|
+
continue;
|
|
1743
|
+
const path = s.adminUiPath ?? "/";
|
|
1744
|
+
derived[s.adminUiServiceKey] = `http://127.0.0.1:${port}${path}`;
|
|
1745
|
+
}
|
|
1746
|
+
const configured = Object.entries(fromConfig).filter(([k, v]) => STALE_TEMPLATE_LINKS[k] !== v);
|
|
1747
|
+
return { ...derived, ...Object.fromEntries(configured) };
|
|
1748
|
+
}
|
|
1749
|
+
var FOSSIL_REPOSITORY_NAME = /^[A-Za-z0-9_][A-Za-z0-9_-]*$/;
|
|
1750
|
+
function resolveRepositories(config) {
|
|
1751
|
+
const repositories = {
|
|
1752
|
+
sessions: "claude-transcripts-sessions",
|
|
1753
|
+
...config.fossil?.repositories ?? {}
|
|
1754
|
+
};
|
|
1755
|
+
for (const [key, name] of Object.entries(repositories)) {
|
|
1756
|
+
if (!FOSSIL_REPOSITORY_NAME.test(name)) {
|
|
1757
|
+
throw new Error(`config: fossil.repositories.${key} = ${JSON.stringify(name)} \u2014 use letters, digits, '-' and '_' (not starting with '-')`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return repositories;
|
|
1761
|
+
}
|
|
1762
|
+
function buildAppBlueprint(config, env = {}) {
|
|
1763
|
+
const version = env.CT_VERSION ?? "0.0.0-dev";
|
|
1764
|
+
const services = SERVICES.map((s) => ({
|
|
1765
|
+
...s,
|
|
1766
|
+
resolvedPorts: (s.ports ?? []).map((p) => ({
|
|
1767
|
+
internal: p.internal,
|
|
1768
|
+
host: env[p.hostEnv] ? Number(env[p.hostEnv]) : p.defaultHost,
|
|
1769
|
+
label: p.label
|
|
1770
|
+
}))
|
|
1771
|
+
}));
|
|
1772
|
+
return {
|
|
1773
|
+
identity: {
|
|
1774
|
+
codename: "claude-transcripts",
|
|
1775
|
+
slug: config.app?.name ?? "claude-transcripts",
|
|
1776
|
+
title: "Claude Transcripts",
|
|
1777
|
+
version,
|
|
1778
|
+
repository: "https://github.com/vredchenko/claude-transcripts"
|
|
1779
|
+
},
|
|
1780
|
+
services,
|
|
1781
|
+
stores: {
|
|
1782
|
+
databases: { ...config.couchdb.databases },
|
|
1783
|
+
buckets: { ...config.s3.buckets },
|
|
1784
|
+
indexes: { ...config.meilisearch?.indexes },
|
|
1785
|
+
repositories: resolveRepositories(config)
|
|
1786
|
+
},
|
|
1787
|
+
hooks: HOOK_TYPES,
|
|
1788
|
+
actions: ACTIONS,
|
|
1789
|
+
bindings: BINDINGS,
|
|
1790
|
+
routes: ROUTES,
|
|
1791
|
+
topology: TOPOLOGY,
|
|
1792
|
+
env: ENV_VARS,
|
|
1793
|
+
features: config.features,
|
|
1794
|
+
servicesMenu: buildServicesMenu(services, config.servicesMenu),
|
|
1795
|
+
system: config.system,
|
|
1796
|
+
recall: resolveRecall(config.recall, env),
|
|
1797
|
+
userSettings: resolveUserSettings(config.userSettings),
|
|
1798
|
+
cliSpec: CLI_SPEC
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
// packages/shared/src/blueprint/k8s.ts
|
|
1802
|
+
var WORKLOAD_ROLES = new Set(["backing", "admin-ui", "app"]);
|
|
1803
|
+
// packages/shared/src/blueprint/project.ts
|
|
1804
|
+
function toStoreEnv(model) {
|
|
1805
|
+
return { FOSSIL_REPOSITORIES: Object.values(model.stores.repositories).join(",") };
|
|
1806
|
+
}
|
|
1807
|
+
// packages/shared/src/couch-url.ts
|
|
1808
|
+
var DEFAULT_COUCHDB_HOST = "127.0.0.1";
|
|
1809
|
+
var DEFAULT_COUCHDB_PORT = "7652";
|
|
1810
|
+
function trimTrailingSlash(url) {
|
|
1811
|
+
return url.replace(/\/+$/, "");
|
|
1812
|
+
}
|
|
1813
|
+
function resolveCouchUrl(env = {}) {
|
|
1814
|
+
const explicit = env.COUCHDB_URL?.trim();
|
|
1815
|
+
if (explicit) {
|
|
1816
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(explicit) ? explicit : `http://${explicit}`;
|
|
1817
|
+
return trimTrailingSlash(withScheme);
|
|
1818
|
+
}
|
|
1819
|
+
const host = env.COUCHDB_HOST || DEFAULT_COUCHDB_HOST;
|
|
1820
|
+
const port = env.COUCHDB_PORT || DEFAULT_COUCHDB_PORT;
|
|
1821
|
+
return `http://${host}:${port}`;
|
|
1822
|
+
}
|
|
1823
|
+
// packages/shared/src/highlight.ts
|
|
1824
|
+
var HIGHLIGHT_PRE = "\uE000";
|
|
1825
|
+
var HIGHLIGHT_POST = "\uE001";
|
|
1826
|
+
function stripHighlightMarks(marked) {
|
|
1827
|
+
return marked.replaceAll(HIGHLIGHT_PRE, "").replaceAll(HIGHLIGHT_POST, "");
|
|
1828
|
+
}
|
|
1829
|
+
// packages/shared/src/migrations/designs.ts
|
|
1830
|
+
var CHUNK_ENTRIES_MAP = `function (doc) {
|
|
1831
|
+
if (doc.type !== "chunk" || !doc.entries) return;
|
|
1832
|
+
for (var i = 0; i < doc.entries.length; i++) {
|
|
1833
|
+
var e = doc.entries[i];
|
|
1834
|
+
if (!e || !e.role) continue;
|
|
1835
|
+
emit([doc.session_id, doc.byte_start, i], {
|
|
1836
|
+
role: e.role,
|
|
1837
|
+
timestamp: e.timestamp || "",
|
|
1838
|
+
text: e.text || "",
|
|
1839
|
+
toolUses: e.toolUses || null,
|
|
1840
|
+
toolUseId: e.toolUseId || null,
|
|
1841
|
+
isError: e.isError || false,
|
|
1842
|
+
isSidechain: e.isSidechain || false,
|
|
1843
|
+
kind: e.kind || null
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}`;
|
|
1847
|
+
var CHUNKS_DESIGN = {
|
|
1848
|
+
_id: "_design/chunks",
|
|
1849
|
+
language: "javascript",
|
|
1850
|
+
views: {
|
|
1851
|
+
by_session: {
|
|
1852
|
+
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});}}'
|
|
1853
|
+
},
|
|
1854
|
+
entry_count_by_session: {
|
|
1855
|
+
map: 'function(doc){if(doc.type==="chunk"){emit(doc.session_id,doc.entry_count||0);}}',
|
|
1856
|
+
reduce: "_sum"
|
|
1857
|
+
},
|
|
1858
|
+
entries_by_session: { map: CHUNK_ENTRIES_MAP, reduce: "_count" }
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
var INITIAL_DESIGNS = [
|
|
1862
|
+
{
|
|
1863
|
+
_id: "_design/sessions",
|
|
1864
|
+
language: "javascript",
|
|
1865
|
+
views: {
|
|
1866
|
+
by_date: {
|
|
1867
|
+
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});}}',
|
|
1868
|
+
reduce: "_count"
|
|
1869
|
+
},
|
|
1870
|
+
by_cwd: {
|
|
1871
|
+
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});}}',
|
|
1872
|
+
reduce: "_count"
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
},
|
|
1876
|
+
{
|
|
1877
|
+
_id: "_design/events",
|
|
1878
|
+
language: "javascript",
|
|
1879
|
+
views: {
|
|
1880
|
+
by_session: {
|
|
1881
|
+
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});}}'
|
|
1882
|
+
},
|
|
1883
|
+
by_type: {
|
|
1884
|
+
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);}}',
|
|
1885
|
+
reduce: "_count"
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
_id: "_design/tools",
|
|
1891
|
+
language: "javascript",
|
|
1892
|
+
views: {
|
|
1893
|
+
usage: {
|
|
1894
|
+
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);}}',
|
|
1895
|
+
reduce: "_count"
|
|
1896
|
+
},
|
|
1897
|
+
failures: {
|
|
1898
|
+
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});}}'
|
|
1899
|
+
},
|
|
1900
|
+
errors: {
|
|
1901
|
+
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});}}'
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
_id: "_design/activity",
|
|
1907
|
+
language: "javascript",
|
|
1908
|
+
views: {
|
|
1909
|
+
timeline: {
|
|
1910
|
+
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);}}',
|
|
1911
|
+
reduce: "_count"
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
},
|
|
1915
|
+
CHUNKS_DESIGN,
|
|
1916
|
+
{
|
|
1917
|
+
_id: "_design/session_meta",
|
|
1918
|
+
language: "javascript",
|
|
1919
|
+
views: {
|
|
1920
|
+
start_meta: {
|
|
1921
|
+
map: 'function(doc){if(doc.event==="SessionStart"){emit(doc.session_id,{timestamp:doc.timestamp,model:doc.model||"",cwd:doc.cwd||"",hostname:doc.hostname||""});}}'
|
|
1922
|
+
},
|
|
1923
|
+
tokens_by_date: {
|
|
1924
|
+
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});}}',
|
|
1925
|
+
reduce: "_sum"
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
];
|
|
1930
|
+
// packages/shared/src/migrations/session-index.ts
|
|
1931
|
+
var AGGREGATE_MAP = `function (doc) {
|
|
1932
|
+
if (!doc.session_id) return;
|
|
1933
|
+
// A \`chunk\` doc contributes transcript coverage ONLY \u2014 no timestamps, no counts \u2014
|
|
1934
|
+
// so every other metric keeps the exact meaning it had before chunks were indexed.
|
|
1935
|
+
// \`chunkEntries\` counts turns the chunk view can actually serve (byte-range-only
|
|
1936
|
+
// chunks carry no \`entries[]\`), while \`chunkBytes\` tracks raw byte coverage.
|
|
1937
|
+
if (doc.type === "chunk") {
|
|
1938
|
+
emit(doc.session_id, {
|
|
1939
|
+
ended: 0, events: 0, prompts: 0, errors: 0, started: 0, tools: {},
|
|
1940
|
+
first: "", last: "", model: "", cwd: "", hostname: "",
|
|
1941
|
+
modelAt: "", cwdAt: "", hostnameAt: "", summary: null,
|
|
1942
|
+
chunkEntries: doc.entries ? doc.entries.length : 0,
|
|
1943
|
+
chunkBytes: doc.byte_end || 0
|
|
1944
|
+
});
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (doc.type !== "event" && doc.type !== "summary") return;
|
|
1948
|
+
var isSummary = doc.type === "summary";
|
|
1949
|
+
var ts = doc.timestamp || "";
|
|
1950
|
+
var model = (doc.event === "SessionStart" || isSummary) ? (doc.model || "") : "";
|
|
1951
|
+
var tools = {};
|
|
1952
|
+
if (doc.event === "PostToolUse" && doc.tool_name) tools[doc.tool_name] = 1;
|
|
1953
|
+
emit(doc.session_id, {
|
|
1954
|
+
ended: isSummary ? 1 : 0,
|
|
1955
|
+
events: doc.type === "event" ? 1 : 0,
|
|
1956
|
+
prompts: doc.event === "UserPromptSubmit" ? 1 : 0,
|
|
1957
|
+
errors: doc.event === "PostToolUseFailure" ? 1 : 0,
|
|
1958
|
+
started: doc.event === "SessionStart" ? 1 : 0,
|
|
1959
|
+
tools: tools,
|
|
1960
|
+
first: ts,
|
|
1961
|
+
last: ts,
|
|
1962
|
+
model: model,
|
|
1963
|
+
cwd: doc.cwd || "",
|
|
1964
|
+
hostname: doc.hostname || "",
|
|
1965
|
+
modelAt: model ? ts : "",
|
|
1966
|
+
cwdAt: doc.cwd ? ts : "",
|
|
1967
|
+
hostnameAt: doc.hostname ? ts : "",
|
|
1968
|
+
summary: isSummary ? {
|
|
1969
|
+
event_count: doc.event_count || 0,
|
|
1970
|
+
prompt_count: doc.prompt_count || 0,
|
|
1971
|
+
error_count: doc.error_count || 0,
|
|
1972
|
+
tool_counts: doc.tool_counts || {},
|
|
1973
|
+
end_reason: doc.end_reason || "",
|
|
1974
|
+
transcript_bytes: doc.transcript_bytes || 0,
|
|
1975
|
+
token_usage: doc.token_usage || null,
|
|
1976
|
+
timestamp: doc.timestamp || "",
|
|
1977
|
+
source: doc.source || ""
|
|
1978
|
+
} : null,
|
|
1979
|
+
chunkEntries: 0,
|
|
1980
|
+
chunkBytes: 0
|
|
1981
|
+
});
|
|
1982
|
+
}`;
|
|
1983
|
+
var AGGREGATE_REDUCE = `function (keys, values, rereduce) {
|
|
1984
|
+
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 };
|
|
1985
|
+
// Keep the earliest-stamped value; an unstamped one sorts last, ties go to the smaller value.
|
|
1986
|
+
function earliest(v, f) {
|
|
1987
|
+
var val = v[f];
|
|
1988
|
+
if (!val) return;
|
|
1989
|
+
var at = v[f + "At"] || "", a = at || "\\uffff", b = acc[f + "At"] || "\\uffff";
|
|
1990
|
+
if (acc[f] === "" || a < b || (a === b && val < acc[f])) { acc[f] = val; acc[f + "At"] = at; }
|
|
1991
|
+
}
|
|
1992
|
+
for (var i = 0; i < values.length; i++) {
|
|
1993
|
+
var v = values[i];
|
|
1994
|
+
if (!v) continue;
|
|
1995
|
+
acc.chunkEntries += v.chunkEntries || 0;
|
|
1996
|
+
if ((v.chunkBytes || 0) > acc.chunkBytes) acc.chunkBytes = v.chunkBytes;
|
|
1997
|
+
acc.ended += v.ended || 0;
|
|
1998
|
+
acc.events += v.events || 0;
|
|
1999
|
+
acc.prompts += v.prompts || 0;
|
|
2000
|
+
acc.errors += v.errors || 0;
|
|
2001
|
+
acc.started += v.started || 0;
|
|
2002
|
+
if (v.last && v.last > acc.last) acc.last = v.last;
|
|
2003
|
+
if (v.first && (acc.first === "" || v.first < acc.first)) acc.first = v.first;
|
|
2004
|
+
earliest(v, "model");
|
|
2005
|
+
earliest(v, "cwd");
|
|
2006
|
+
earliest(v, "hostname");
|
|
2007
|
+
// A session has one \`summary:<id>\` doc, so there is only ever one to keep.
|
|
2008
|
+
if (v.summary) acc.summary = v.summary;
|
|
2009
|
+
var t = v.tools || {};
|
|
2010
|
+
for (var k in t) { if (t.hasOwnProperty(k)) acc.tools[k] = (acc.tools[k] || 0) + t[k]; }
|
|
2011
|
+
}
|
|
2012
|
+
var names = Object.keys(acc.tools).sort(), tools = {};
|
|
2013
|
+
for (var j = 0; j < names.length; j++) tools[names[j]] = acc.tools[names[j]];
|
|
2014
|
+
acc.tools = tools;
|
|
2015
|
+
return acc;
|
|
2016
|
+
}`;
|
|
2017
|
+
var EVENT_TIMES_MAP = `function (doc) {
|
|
2018
|
+
if (!doc.session_id || !doc.timestamp) return;
|
|
2019
|
+
if (doc.type !== "event" && doc.type !== "summary") return;
|
|
2020
|
+
emit(doc.session_id, doc.timestamp);
|
|
2021
|
+
}`;
|
|
2022
|
+
var SESSION_INDEX_DESIGN = {
|
|
2023
|
+
_id: "_design/session_index",
|
|
2024
|
+
language: "javascript",
|
|
2025
|
+
views: {
|
|
2026
|
+
aggregate: { map: AGGREGATE_MAP, reduce: AGGREGATE_REDUCE },
|
|
2027
|
+
event_times: { map: EVENT_TIMES_MAP }
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
|
|
2031
|
+
// packages/shared/src/migrations/speaker-split.ts
|
|
2032
|
+
var BY_ROLE_MAP = `function (doc) {
|
|
2033
|
+
if (doc.type !== "chunk" || !doc.entries) return;
|
|
2034
|
+
for (var i = 0; i < doc.entries.length; i++) {
|
|
2035
|
+
var e = doc.entries[i];
|
|
2036
|
+
if (!e || !e.role) continue;
|
|
2037
|
+
emit([doc.session_id, e.role, doc.byte_start, i], {
|
|
2038
|
+
role: e.role,
|
|
2039
|
+
timestamp: e.timestamp || "",
|
|
2040
|
+
text: e.text || "",
|
|
2041
|
+
toolUses: e.toolUses || null,
|
|
2042
|
+
toolUseId: e.toolUseId || null,
|
|
2043
|
+
isError: e.isError || false
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
}`;
|
|
2047
|
+
var BY_ROLE_TIME_MAP = `function (doc) {
|
|
2048
|
+
if (doc.type !== "chunk" || !doc.entries) return;
|
|
2049
|
+
for (var i = 0; i < doc.entries.length; i++) {
|
|
2050
|
+
var e = doc.entries[i];
|
|
2051
|
+
if (!e || !e.role) continue;
|
|
2052
|
+
emit([e.role, e.timestamp || "", doc.session_id, doc.byte_start, i], {
|
|
2053
|
+
sessionId: doc.session_id,
|
|
2054
|
+
cwd: doc.cwd || "",
|
|
2055
|
+
role: e.role,
|
|
2056
|
+
timestamp: e.timestamp || "",
|
|
2057
|
+
text: e.text || ""
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
}`;
|
|
2061
|
+
var SPEAKER_SPLIT_DESIGN = {
|
|
2062
|
+
_id: "_design/speaker_split",
|
|
2063
|
+
language: "javascript",
|
|
2064
|
+
views: {
|
|
2065
|
+
by_role: { map: BY_ROLE_MAP, reduce: "_count" },
|
|
2066
|
+
by_role_time: { map: BY_ROLE_TIME_MAP, reduce: "_count" }
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
|
|
2070
|
+
// packages/shared/src/migrations/registry.ts
|
|
2071
|
+
var initialSchema = {
|
|
2072
|
+
id: 1,
|
|
2073
|
+
name: "initial-schema",
|
|
2074
|
+
async up(ctx) {
|
|
2075
|
+
for (const design of INITIAL_DESIGNS) {
|
|
2076
|
+
ctx.log(`+ ${design._id}`);
|
|
2077
|
+
const { _rev, ...body } = design;
|
|
2078
|
+
await ctx.putDoc(design._id, body);
|
|
2079
|
+
}
|
|
2080
|
+
},
|
|
2081
|
+
async down(ctx) {
|
|
2082
|
+
for (const design of INITIAL_DESIGNS) {
|
|
2083
|
+
ctx.log(`- ${design._id}`);
|
|
2084
|
+
await ctx.deleteDoc(design._id);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
};
|
|
2088
|
+
var sessionIndexView = {
|
|
2089
|
+
id: 2,
|
|
2090
|
+
name: "session-index-view",
|
|
2091
|
+
async up(ctx) {
|
|
2092
|
+
ctx.log(`+ ${SESSION_INDEX_DESIGN._id}`);
|
|
2093
|
+
const { _rev, ...body } = SESSION_INDEX_DESIGN;
|
|
2094
|
+
await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
|
|
2095
|
+
},
|
|
2096
|
+
async down(ctx) {
|
|
2097
|
+
ctx.log(`- ${SESSION_INDEX_DESIGN._id}`);
|
|
2098
|
+
await ctx.deleteDoc(SESSION_INDEX_DESIGN._id);
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
var sessionIndexSource = {
|
|
2102
|
+
id: 3,
|
|
2103
|
+
name: "session-index-source",
|
|
2104
|
+
async up(ctx) {
|
|
2105
|
+
ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (emit summary.source)`);
|
|
2106
|
+
const { _rev, ...body } = SESSION_INDEX_DESIGN;
|
|
2107
|
+
await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
|
|
2108
|
+
},
|
|
2109
|
+
async down(ctx) {
|
|
2110
|
+
ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (source field left in place \u2014 additive)`);
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
var speakerSplitView = {
|
|
2114
|
+
id: 4,
|
|
2115
|
+
name: "speaker-split-view",
|
|
2116
|
+
async up(ctx) {
|
|
2117
|
+
ctx.log(`+ ${SPEAKER_SPLIT_DESIGN._id}`);
|
|
2118
|
+
const { _rev, ...body } = SPEAKER_SPLIT_DESIGN;
|
|
2119
|
+
await ctx.putDoc(SPEAKER_SPLIT_DESIGN._id, body);
|
|
1992
2120
|
},
|
|
1993
|
-
{
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2121
|
+
async down(ctx) {
|
|
2122
|
+
ctx.log(`- ${SPEAKER_SPLIT_DESIGN._id}`);
|
|
2123
|
+
await ctx.deleteDoc(SPEAKER_SPLIT_DESIGN._id);
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
var speakerSplitTimeView = {
|
|
2127
|
+
id: 5,
|
|
2128
|
+
name: "speaker-split-time-view",
|
|
2129
|
+
async up(ctx) {
|
|
2130
|
+
ctx.log(`~ ${SPEAKER_SPLIT_DESIGN._id} (add by_role_time)`);
|
|
2131
|
+
const { _rev, ...body } = SPEAKER_SPLIT_DESIGN;
|
|
2132
|
+
await ctx.putDoc(SPEAKER_SPLIT_DESIGN._id, body);
|
|
2000
2133
|
},
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
var
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2134
|
+
async down(ctx) {
|
|
2135
|
+
ctx.log(`~ ${SPEAKER_SPLIT_DESIGN._id} (by_role_time left in place \u2014 additive)`);
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
var chunkEntriesView = {
|
|
2139
|
+
id: 6,
|
|
2140
|
+
name: "chunk-entries-view",
|
|
2141
|
+
async up(ctx) {
|
|
2142
|
+
ctx.log(`~ ${CHUNKS_DESIGN._id} (add entries_by_session)`);
|
|
2143
|
+
const { _rev, ...body } = CHUNKS_DESIGN;
|
|
2144
|
+
await ctx.putDoc(CHUNKS_DESIGN._id, body);
|
|
2011
2145
|
},
|
|
2012
|
-
{
|
|
2013
|
-
|
|
2014
|
-
title: "your infrastructure",
|
|
2015
|
-
members: ["couchdb", "garage", "meilisearch"],
|
|
2016
|
-
level: "compact"
|
|
2146
|
+
async down(ctx) {
|
|
2147
|
+
ctx.log(`~ ${CHUNKS_DESIGN._id} (entries_by_session left in place \u2014 additive)`);
|
|
2017
2148
|
}
|
|
2018
|
-
];
|
|
2019
|
-
var TOPOLOGY = {
|
|
2020
|
-
nodes: TOPOLOGY_NODES,
|
|
2021
|
-
edges: TOPOLOGY_EDGES,
|
|
2022
|
-
groups: TOPOLOGY_GROUPS
|
|
2023
2149
|
};
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2150
|
+
var sessionIndexChunkCoverage = {
|
|
2151
|
+
id: 7,
|
|
2152
|
+
name: "session-index-chunk-coverage",
|
|
2153
|
+
async up(ctx) {
|
|
2154
|
+
ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (emit chunk coverage)`);
|
|
2155
|
+
const { _rev, ...body } = SESSION_INDEX_DESIGN;
|
|
2156
|
+
await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
|
|
2157
|
+
},
|
|
2158
|
+
async down(ctx) {
|
|
2159
|
+
ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (chunk coverage left in place \u2014 additive)`);
|
|
2160
|
+
}
|
|
2030
2161
|
};
|
|
2031
|
-
var
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2162
|
+
var chunkEntryKind = {
|
|
2163
|
+
id: 8,
|
|
2164
|
+
name: "chunk-entry-kind",
|
|
2165
|
+
async up(ctx) {
|
|
2166
|
+
ctx.log(`~ ${CHUNKS_DESIGN._id} (emit entry kind)`);
|
|
2167
|
+
const { _rev, ...body } = CHUNKS_DESIGN;
|
|
2168
|
+
await ctx.putDoc(CHUNKS_DESIGN._id, body);
|
|
2169
|
+
},
|
|
2170
|
+
async down(ctx) {
|
|
2171
|
+
ctx.log(`~ ${CHUNKS_DESIGN._id} (kind left in place \u2014 additive)`);
|
|
2172
|
+
}
|
|
2035
2173
|
};
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
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/"
|
|
2174
|
+
var sessionIndexEventTimes = {
|
|
2175
|
+
id: 9,
|
|
2176
|
+
name: "session-index-event-times",
|
|
2177
|
+
async up(ctx) {
|
|
2178
|
+
ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (add event_times view)`);
|
|
2179
|
+
const { _rev, ...body } = SESSION_INDEX_DESIGN;
|
|
2180
|
+
await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
|
|
2181
|
+
},
|
|
2182
|
+
async down(ctx) {
|
|
2183
|
+
ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (event_times left in place \u2014 additive)`);
|
|
2184
|
+
}
|
|
2059
2185
|
};
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
const
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2186
|
+
var sessionIndexDeterministic = {
|
|
2187
|
+
id: 10,
|
|
2188
|
+
name: "session-index-deterministic",
|
|
2189
|
+
async up(ctx) {
|
|
2190
|
+
ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (earliest model/cwd/hostname)`);
|
|
2191
|
+
const { _rev, ...body } = SESSION_INDEX_DESIGN;
|
|
2192
|
+
await ctx.putDoc(SESSION_INDEX_DESIGN._id, body);
|
|
2193
|
+
},
|
|
2194
|
+
async down(ctx) {
|
|
2195
|
+
ctx.log(`~ ${SESSION_INDEX_DESIGN._id} (deterministic pick left in place)`);
|
|
2070
2196
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2197
|
+
};
|
|
2198
|
+
var MIGRATIONS = [
|
|
2199
|
+
initialSchema,
|
|
2200
|
+
sessionIndexView,
|
|
2201
|
+
sessionIndexSource,
|
|
2202
|
+
speakerSplitView,
|
|
2203
|
+
speakerSplitTimeView,
|
|
2204
|
+
chunkEntriesView,
|
|
2205
|
+
sessionIndexChunkCoverage,
|
|
2206
|
+
chunkEntryKind,
|
|
2207
|
+
sessionIndexEventTimes,
|
|
2208
|
+
sessionIndexDeterministic
|
|
2209
|
+
];
|
|
2210
|
+
function latestVersion() {
|
|
2211
|
+
return MIGRATIONS.reduce((max, m) => Math.max(max, m.id), 0);
|
|
2073
2212
|
}
|
|
2074
|
-
function
|
|
2075
|
-
|
|
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
|
-
};
|
|
2213
|
+
function docTransformsBetween(from, to, migrations = MIGRATIONS) {
|
|
2214
|
+
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
2215
|
}
|
|
2110
|
-
// packages/shared/src/model/k8s.ts
|
|
2111
|
-
var WORKLOAD_ROLES = new Set(["backing", "admin-ui", "app"]);
|
|
2112
2216
|
// packages/shared/src/index.ts
|
|
2113
2217
|
function usageWeight(u) {
|
|
2114
2218
|
return (u.input_tokens || 0) + (u.output_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);
|
|
@@ -3373,9 +3477,9 @@ var EMBEDDED_ASSETS = {
|
|
|
3373
3477
|
compose: {
|
|
3374
3478
|
path: "deploy/docker-compose.yml",
|
|
3375
3479
|
source: "deploy/docker-compose.yml",
|
|
3376
|
-
content: `# GENERATED from the
|
|
3377
|
-
# Do NOT edit by hand \u2014 run \`bun run gen:compose\` to regenerate, and change the
|
|
3378
|
-
# (packages/shared/src/
|
|
3480
|
+
content: `# GENERATED from the blueprint (@claude-transcripts/shared) by scripts/gen-compose.ts.
|
|
3481
|
+
# Do NOT edit by hand \u2014 run \`bun run gen:compose\` to regenerate, and change the blueprint
|
|
3482
|
+
# (packages/shared/src/blueprint/services.ts) instead. Inline edits here are lost silently.
|
|
3379
3483
|
# See docs/operate/containers.md.
|
|
3380
3484
|
#
|
|
3381
3485
|
# Claude Transcripts \u2014 backing services + app.
|
|
@@ -3466,6 +3570,18 @@ services:
|
|
|
3466
3570
|
restart: unless-stopped
|
|
3467
3571
|
networks:
|
|
3468
3572
|
- claude-transcripts
|
|
3573
|
+
fossil:
|
|
3574
|
+
image: \${IMAGE_NS}/claude-transcripts-fossil:\${FOSSIL_TAG:-2.28}
|
|
3575
|
+
container_name: claude-transcripts-fossil
|
|
3576
|
+
environment:
|
|
3577
|
+
- FOSSIL_REPOSITORIES=\${FOSSIL_REPOSITORIES:-claude-transcripts-sessions}
|
|
3578
|
+
ports:
|
|
3579
|
+
- 127.0.0.1:\${FOSSIL_PORT:-7658}:8080
|
|
3580
|
+
volumes:
|
|
3581
|
+
- ./data/fossil:/museum
|
|
3582
|
+
restart: unless-stopped
|
|
3583
|
+
networks:
|
|
3584
|
+
- claude-transcripts
|
|
3469
3585
|
meilisearch:
|
|
3470
3586
|
image: \${IMAGE_NS}/claude-transcripts-meilisearch:\${MEILI_TAG:-v1.10}
|
|
3471
3587
|
container_name: claude-transcripts-meilisearch
|
|
@@ -3497,6 +3613,7 @@ services:
|
|
|
3497
3613
|
- couchdb
|
|
3498
3614
|
- garage
|
|
3499
3615
|
- meilisearch
|
|
3616
|
+
- fossil
|
|
3500
3617
|
env_file: ../.env
|
|
3501
3618
|
environment:
|
|
3502
3619
|
- WEBAPI_HOST=0.0.0.0
|
|
@@ -3504,6 +3621,7 @@ services:
|
|
|
3504
3621
|
- COUCHDB_URL=http://couchdb:5984
|
|
3505
3622
|
- S3_ENDPOINT=http://garage:3900
|
|
3506
3623
|
- MEILI_HOST=http://meilisearch:7700
|
|
3624
|
+
- FOSSIL_URL=http://fossil:8080
|
|
3507
3625
|
ports:
|
|
3508
3626
|
- 127.0.0.1:\${WEBAPI_PORT:-7650}:7650
|
|
3509
3627
|
restart: unless-stopped
|
|
@@ -3518,7 +3636,7 @@ networks:
|
|
|
3518
3636
|
composeUpstream: {
|
|
3519
3637
|
path: "deploy/docker-compose.upstream.yml",
|
|
3520
3638
|
source: "deploy/docker-compose.upstream.yml",
|
|
3521
|
-
content: `# GENERATED from the
|
|
3639
|
+
content: `# GENERATED from the blueprint (@claude-transcripts/shared) by scripts/gen-compose-override.ts.
|
|
3522
3640
|
# Do NOT edit by hand \u2014 run \`bun run gen:compose-override\` to regenerate.
|
|
3523
3641
|
#
|
|
3524
3642
|
# Upstream-image dev override: pins each mirrored backing service to its canonical
|
|
@@ -3529,7 +3647,7 @@ networks:
|
|
|
3529
3647
|
# bun run scripts/stack.ts up --upstream # the runner does this for you
|
|
3530
3648
|
#
|
|
3531
3649
|
# Tags still come from .env (COUCHDB_TAG, GARAGE_TAG, \u2026). Override an image here by
|
|
3532
|
-
# changing its \`upstream\` in packages/shared/src/
|
|
3650
|
+
# changing its \`upstream\` in packages/shared/src/blueprint/services.ts. The community
|
|
3533
3651
|
# admin-UI images are best-effort; swap them if your registry differs.
|
|
3534
3652
|
|
|
3535
3653
|
name: claude-transcripts
|
|
@@ -3540,6 +3658,10 @@ services:
|
|
|
3540
3658
|
image: dxflrs/garage:\${GARAGE_TAG:-v2.3.0}
|
|
3541
3659
|
garage-ui:
|
|
3542
3660
|
image: khairul169/garage-webui:\${GARAGE_UI_TAG:-1.1.0}
|
|
3661
|
+
fossil:
|
|
3662
|
+
image: claude-transcripts-fossil:\${FOSSIL_TAG:-2.28}-local
|
|
3663
|
+
build:
|
|
3664
|
+
context: ./fossil
|
|
3543
3665
|
meilisearch:
|
|
3544
3666
|
image: getmeili/meilisearch:\${MEILI_TAG:-v1.10}
|
|
3545
3667
|
meilisearch-ui:
|
|
@@ -3570,6 +3692,154 @@ api_bind_addr = "[::]:3900"
|
|
|
3570
3692
|
|
|
3571
3693
|
[admin]
|
|
3572
3694
|
api_bind_addr = "[::]:3903"
|
|
3695
|
+
`
|
|
3696
|
+
},
|
|
3697
|
+
fossilDockerfile: {
|
|
3698
|
+
path: "deploy/fossil/Dockerfile",
|
|
3699
|
+
source: "deploy/fossil/Dockerfile",
|
|
3700
|
+
content: `# Fossil SCM \u2014 built from the official release source (https://fossil-scm.org).
|
|
3701
|
+
#
|
|
3702
|
+
# Fossil publishes no container image, only a Dockerfile in its source tree
|
|
3703
|
+
# (www/containers.md). This is that file's recipe, pinned to a release instead of
|
|
3704
|
+
# trunk and verified by checksum: a static, musl-linked binary on an empty base, so
|
|
3705
|
+
# the image holds Fossil and nothing else. Hand-maintained (NOT generated); its
|
|
3706
|
+
# version must equal the \`fossil\` service's \`defaultTag\` in
|
|
3707
|
+
# packages/shared/src/blueprint/services.ts \u2014 a test holds the two together.
|
|
3708
|
+
#
|
|
3709
|
+
# Build context is this directory, and only entrypoint.sh is COPY'd from it, so the
|
|
3710
|
+
# image builds the same from a repo checkout, a user install (which ships both files),
|
|
3711
|
+
# or CI.
|
|
3712
|
+
#
|
|
3713
|
+
# To bump: take the release's tarball link from https://fossil-scm.org/home/uv/download.html,
|
|
3714
|
+
# set FOSSIL_VERSION / FOSSIL_CHECKIN / FOSSIL_SHA256, and the blueprint's defaultTag.
|
|
3715
|
+
|
|
3716
|
+
## \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
|
|
3717
|
+
FROM alpine:3.22 AS build
|
|
3718
|
+
RUN apk add --no-cache gcc make linux-headers musl-dev \\
|
|
3719
|
+
openssl-dev openssl-libs-static zlib-dev zlib-static
|
|
3720
|
+
ARG FOSSIL_VERSION=2.28
|
|
3721
|
+
# The release check-in (\`fossil version\` prints its prefix) and the sha256 of the
|
|
3722
|
+
# tarball fossil-scm.org serves for it.
|
|
3723
|
+
ARG FOSSIL_CHECKIN=52445a27f116603d784ad753c6acd494e0b02aeaf8502ee0fee0aa30030f4c4e
|
|
3724
|
+
ARG FOSSIL_SHA256=84c18824ca227e7602d2408b663c3747f754ad306ed5c73ddab959d6589538a6
|
|
3725
|
+
WORKDIR /fsl
|
|
3726
|
+
ADD --checksum=sha256:\${FOSSIL_SHA256} \\
|
|
3727
|
+
https://fossil-scm.org/home/tarball/\${FOSSIL_CHECKIN}/fossil-src-\${FOSSIL_VERSION}.tar.gz \\
|
|
3728
|
+
src.tar.gz
|
|
3729
|
+
RUN tar -xzf src.tar.gz --strip-components=1 \\
|
|
3730
|
+
&& ./configure --static --json CFLAGS='-Os -s' \\
|
|
3731
|
+
&& make -j"$(nproc)" \\
|
|
3732
|
+
&& ./fossil version | grep -q "version \${FOSSIL_VERSION} "
|
|
3733
|
+
|
|
3734
|
+
## \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
|
|
3735
|
+
# The musl variant: a static busybox, so the one binary runs on \`scratch\`.
|
|
3736
|
+
FROM busybox:1.37-musl AS os
|
|
3737
|
+
RUN mkdir -p /e /museum \\
|
|
3738
|
+
&& echo "root:x:0:0:Admin:/:/false" > /e/passwd \\
|
|
3739
|
+
&& echo "root:x:0:root" > /e/group
|
|
3740
|
+
|
|
3741
|
+
## \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
|
|
3742
|
+
FROM scratch
|
|
3743
|
+
COPY --from=build --chmod=755 /fsl/fossil /bin/fossil
|
|
3744
|
+
COPY --from=os /e/passwd /e/group /etc/
|
|
3745
|
+
COPY --from=os --chmod=1777 /tmp /tmp/
|
|
3746
|
+
COPY --from=os /museum /museum/
|
|
3747
|
+
# Only for the seed script (entrypoint.sh): one static binary, no applet links.
|
|
3748
|
+
COPY --from=os /bin/busybox /bin/busybox
|
|
3749
|
+
COPY --chmod=755 entrypoint.sh /bin/fossil-entrypoint
|
|
3750
|
+
ENV PATH=/bin
|
|
3751
|
+
EXPOSE 8080/tcp
|
|
3752
|
+
# Runs as root, unlike upstream's image (a fixed UID 499). The repo lives on a
|
|
3753
|
+
# bind mount or volume created by whoever runs the stack, and a fixed UID can't write
|
|
3754
|
+
# to a directory it doesn't own. Fossil handles root itself: serving a repository
|
|
3755
|
+
# file owned by a non-root user, it chroots into the repo's directory and drops to
|
|
3756
|
+
# that user (\`chown\` the repo to opt in). See https://fossil-scm.org/home/help/server.
|
|
3757
|
+
# Seeds the repositories named in FOSSIL_REPOSITORIES (from \`fossil.repositories\` in
|
|
3758
|
+
# config/), then execs \`fossil server\` with CMD \u2014 see entrypoint.sh.
|
|
3759
|
+
ENTRYPOINT ["/bin/busybox", "sh", "/bin/fossil-entrypoint"]
|
|
3760
|
+
# Serve the directory, not a file: every \`<name>.fossil\` in /museum is served at
|
|
3761
|
+
# /<name>/, and "/" lists them. Not upstream's \`--create\`, which makes a generic
|
|
3762
|
+
# repository with a generated admin login; ours are made by the seed.
|
|
3763
|
+
# --jsmode bundled: one JS file, fewer requests.
|
|
3764
|
+
CMD ["--repolist", "--jsmode", "bundled", "/museum"]
|
|
3765
|
+
`
|
|
3766
|
+
},
|
|
3767
|
+
fossilEntrypoint: {
|
|
3768
|
+
path: "deploy/fossil/entrypoint.sh",
|
|
3769
|
+
source: "deploy/fossil/entrypoint.sh",
|
|
3770
|
+
content: `#!/bin/sh
|
|
3771
|
+
# Seed, then serve. Run by busybox sh inside the Fossil image (deploy/fossil/Dockerfile).
|
|
3772
|
+
#
|
|
3773
|
+
# FOSSIL_REPOSITORIES names the repositories this instance owns, comma-separated \u2014
|
|
3774
|
+
# projected from \`fossil.repositories\` in config/ (ADR 0031). Each one missing from
|
|
3775
|
+
# the data directory is created here, because Fossil has no HTTP call that creates a
|
|
3776
|
+
# repository: the seed has to run where the binary and the volume are. Existing
|
|
3777
|
+
# repositories are never touched, so a restart is a no-op.
|
|
3778
|
+
#
|
|
3779
|
+
# A seeded repository needs no login to READ: the built-in \`nobody\` user can browse,
|
|
3780
|
+
# clone and download (ADR 0020). It can't write. Fossil's JSON API takes parameters
|
|
3781
|
+
# from the query string, so with write capabilities a plain GET could change the
|
|
3782
|
+
# repository \u2014 and any web page the user visits can send one to a localhost port
|
|
3783
|
+
# (an <img> tag is enough). The admin user exists because Fossil requires one; its
|
|
3784
|
+
# generated password is discarded, and \`fossil user password\` sets one when needed.
|
|
3785
|
+
#
|
|
3786
|
+
# FOSSIL_MUSEUM and FOSSIL_BUSYBOX exist for the tests (entrypoint.test.ts); the image
|
|
3787
|
+
# uses the defaults.
|
|
3788
|
+
set -euf # -f: an entry like \`*\` must stay a (rejected) name, not glob the cwd
|
|
3789
|
+
|
|
3790
|
+
museum="\${FOSSIL_MUSEUM:-/museum}"
|
|
3791
|
+
bb="\${FOSSIL_BUSYBOX-/bin/busybox}"
|
|
3792
|
+
|
|
3793
|
+
# Read-only for anonymous visitors: check-out/browse (o), clone (g), wiki (j), tickets
|
|
3794
|
+
# (r), hyperlinks (h), zip/tarball downloads (z).
|
|
3795
|
+
NOBODY_CAPS="ghjorz"
|
|
3796
|
+
|
|
3797
|
+
# What a new repository holds beyond \`fossil new\` and its users. A placeholder: the
|
|
3798
|
+
# seed's contents (users and roles, wiki, ticket schema, tags, settings) are still to
|
|
3799
|
+
# be defined \u2014 https://github.com/vredchenko/claude-transcripts/issues/210. Runs once,
|
|
3800
|
+
# on the not-yet-served copy, so anything added here must only ever create.
|
|
3801
|
+
seed_content() {
|
|
3802
|
+
: "$1"
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
trim() {
|
|
3806
|
+
s="$1"
|
|
3807
|
+
while :; do
|
|
3808
|
+
case "$s" in
|
|
3809
|
+
[[:space:]]*) s="\${s#?}" ;;
|
|
3810
|
+
*[[:space:]]) s="\${s%?}" ;;
|
|
3811
|
+
*) break ;;
|
|
3812
|
+
esac
|
|
3813
|
+
done
|
|
3814
|
+
printf '%s' "$s"
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
IFS=','
|
|
3818
|
+
for entry in \${FOSSIL_REPOSITORIES:-}; do
|
|
3819
|
+
name="$(trim "$entry")"
|
|
3820
|
+
[ -n "$name" ] || continue
|
|
3821
|
+
# The URL rules \`fossil server\` applies to a repository directory, so a name that
|
|
3822
|
+
# can't be served is refused here rather than created and then unreachable.
|
|
3823
|
+
case "$name" in
|
|
3824
|
+
-* | *[!A-Za-z0-9_-]*)
|
|
3825
|
+
echo "fossil-seed: skipping '$name' (letters, digits, '-' and '_' only)" >&2
|
|
3826
|
+
continue
|
|
3827
|
+
;;
|
|
3828
|
+
esac
|
|
3829
|
+
repo="$museum/$name.fossil"
|
|
3830
|
+
[ -e "$repo" ] && continue
|
|
3831
|
+
# Built under a name \`--repolist\` doesn't serve, then renamed: a seed interrupted
|
|
3832
|
+
# half-way leaves no half-made repository to be served or skipped forever.
|
|
3833
|
+
$bb rm -f "$repo.new"
|
|
3834
|
+
fossil new --admin-user claude-transcripts --project-name "$name" "$repo.new" >/dev/null
|
|
3835
|
+
fossil user capabilities nobody "$NOBODY_CAPS" -R "$repo.new" >/dev/null
|
|
3836
|
+
seed_content "$repo.new"
|
|
3837
|
+
$bb mv "$repo.new" "$repo"
|
|
3838
|
+
echo "fossil-seed: created $repo (anonymous read-only)"
|
|
3839
|
+
done
|
|
3840
|
+
unset IFS
|
|
3841
|
+
|
|
3842
|
+
exec fossil server "$@"
|
|
3573
3843
|
`
|
|
3574
3844
|
},
|
|
3575
3845
|
appConfig: {
|
|
@@ -3602,6 +3872,11 @@ api_bind_addr = "[::]:3903"
|
|
|
3602
3872
|
"turns": "claude-transcripts-turns"
|
|
3603
3873
|
}
|
|
3604
3874
|
},
|
|
3875
|
+
"fossil": {
|
|
3876
|
+
"repositories": {
|
|
3877
|
+
"sessions": "claude-transcripts-sessions"
|
|
3878
|
+
}
|
|
3879
|
+
},
|
|
3605
3880
|
"features": {
|
|
3606
3881
|
"s3Blobs": true,
|
|
3607
3882
|
"midFlightChunking": true,
|
|
@@ -4846,7 +5121,7 @@ var HANDLERS = {
|
|
|
4846
5121
|
|
|
4847
5122
|
// packages/cli/src/hook/index.ts
|
|
4848
5123
|
function hookBindings() {
|
|
4849
|
-
const model =
|
|
5124
|
+
const model = buildAppBlueprint(loadAppConfig(), process.env);
|
|
4850
5125
|
const out = {};
|
|
4851
5126
|
for (const b of model.bindings)
|
|
4852
5127
|
out[b.event] = b.actions;
|
|
@@ -5461,17 +5736,20 @@ function composeFiles(paths, upstream) {
|
|
|
5461
5736
|
|
|
5462
5737
|
// packages/cli/src/lib/compose.ts
|
|
5463
5738
|
var {$ } = globalThis.Bun;
|
|
5739
|
+
function storeEnv() {
|
|
5740
|
+
return toStoreEnv(buildAppBlueprint(loadAppConfig(), {}));
|
|
5741
|
+
}
|
|
5464
5742
|
function fileArgs(paths) {
|
|
5465
5743
|
return composeFiles(paths, true).flatMap((f) => ["-f", f]);
|
|
5466
5744
|
}
|
|
5467
5745
|
async function compose(opts, args) {
|
|
5468
5746
|
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 });
|
|
5747
|
+
await $`docker compose --env-file ${opts.paths.instanceEnv} ${fileArgs(opts.paths)} ${profile} ${args}`.env({ ...process.env, ...storeEnv(), ...opts.env });
|
|
5470
5748
|
}
|
|
5471
5749
|
async function composeQuiet(opts, args) {
|
|
5472
5750
|
const profile = opts.app ? ["--profile", "app"] : [];
|
|
5473
5751
|
try {
|
|
5474
|
-
const text = await $`docker compose --env-file ${opts.paths.instanceEnv} ${fileArgs(opts.paths)} ${profile} ${args}`.env({ ...process.env, ...opts.env }).quiet().text();
|
|
5752
|
+
const text = await $`docker compose --env-file ${opts.paths.instanceEnv} ${fileArgs(opts.paths)} ${profile} ${args}`.env({ ...process.env, ...storeEnv(), ...opts.env }).quiet().text();
|
|
5475
5753
|
return { ok: true, text };
|
|
5476
5754
|
} catch (err) {
|
|
5477
5755
|
return { ok: false, text: String(err.stderr ?? err) };
|
|
@@ -5659,7 +5937,8 @@ var PORT_KEYS = [
|
|
|
5659
5937
|
"GARAGE_ADMIN_PORT",
|
|
5660
5938
|
"GARAGE_WEBUI_PORT",
|
|
5661
5939
|
"MEILI_PORT",
|
|
5662
|
-
"MEILI_UI_PORT"
|
|
5940
|
+
"MEILI_UI_PORT",
|
|
5941
|
+
"FOSSIL_PORT"
|
|
5663
5942
|
];
|
|
5664
5943
|
function secret(bytes = 32) {
|
|
5665
5944
|
return randomBytes(bytes).toString("hex");
|
|
@@ -5707,6 +5986,7 @@ function buildInstanceEnv(opts = {}) {
|
|
|
5707
5986
|
GARAGE_UI_TAG: "1.1.0",
|
|
5708
5987
|
MEILI_TAG: "v1.10",
|
|
5709
5988
|
MEILI_UI_TAG: "latest",
|
|
5989
|
+
FOSSIL_TAG: "2.28",
|
|
5710
5990
|
COUCHDB_URL: "",
|
|
5711
5991
|
COUCHDB_HOST: "127.0.0.1",
|
|
5712
5992
|
COUCHDB_USER: "admin",
|
|
@@ -5719,6 +5999,7 @@ function buildInstanceEnv(opts = {}) {
|
|
|
5719
5999
|
GARAGE_ADMIN_TOKEN: secret(32),
|
|
5720
6000
|
GARAGE_METRICS_TOKEN: secret(32),
|
|
5721
6001
|
MEILI_HOST: `http://127.0.0.1:${ports.MEILI_PORT}`,
|
|
6002
|
+
FOSSIL_URL: `http://127.0.0.1:${ports.FOSSIL_PORT}`,
|
|
5722
6003
|
MEILI_MASTER_KEY: opts.meiliMasterKey ? secret(24) : "",
|
|
5723
6004
|
MEILI_API_KEY: ""
|
|
5724
6005
|
};
|
|
@@ -5729,10 +6010,24 @@ var HEADER = [
|
|
|
5729
6010
|
"Rotating a secret here does NOT rotate it in an already-provisioned store."
|
|
5730
6011
|
];
|
|
5731
6012
|
var INSTALLER_OWNED = ["APP_TAG"];
|
|
6013
|
+
function backfillPorts(existing) {
|
|
6014
|
+
const base = Number(existing.WEBAPI_PORT);
|
|
6015
|
+
if (!Number.isInteger(base) || base <= 0)
|
|
6016
|
+
return {};
|
|
6017
|
+
const out = {};
|
|
6018
|
+
PORT_KEYS.forEach((key, i) => {
|
|
6019
|
+
if (existing[key] === undefined)
|
|
6020
|
+
out[key] = String(base + i);
|
|
6021
|
+
});
|
|
6022
|
+
if (out.FOSSIL_PORT && existing.FOSSIL_URL === undefined) {
|
|
6023
|
+
out.FOSSIL_URL = `http://127.0.0.1:${out.FOSSIL_PORT}`;
|
|
6024
|
+
}
|
|
6025
|
+
return out;
|
|
6026
|
+
}
|
|
5732
6027
|
function loadOrCreateInstanceEnv(path, opts = {}) {
|
|
5733
6028
|
const generated = buildInstanceEnv(opts);
|
|
5734
6029
|
const existing = existsSync6(path) ? parseEnv(readFileSync10(path, "utf8")) : {};
|
|
5735
|
-
const merged = { ...generated, ...existing };
|
|
6030
|
+
const merged = { ...generated, ...backfillPorts(existing), ...existing };
|
|
5736
6031
|
for (const key of INSTALLER_OWNED) {
|
|
5737
6032
|
if (generated[key] !== undefined)
|
|
5738
6033
|
merged[key] = generated[key];
|
|
@@ -6177,6 +6472,29 @@ async function resolvePortBase(base, span) {
|
|
|
6177
6472
|
}
|
|
6178
6473
|
return null;
|
|
6179
6474
|
}
|
|
6475
|
+
async function relocateAddedPorts(path, env, priorKeys) {
|
|
6476
|
+
const added = PORT_KEYS.filter((k) => !priorKeys.has(k));
|
|
6477
|
+
if (added.length === 0)
|
|
6478
|
+
return env;
|
|
6479
|
+
const own = new Set(PORT_KEYS.map((k) => Number(env[k])));
|
|
6480
|
+
const patch = {};
|
|
6481
|
+
for (const key of added) {
|
|
6482
|
+
let port = Number(env[key]);
|
|
6483
|
+
const isOwnDuplicate = PORT_KEYS.some((k) => k !== key && Number(env[k]) === port);
|
|
6484
|
+
if (!isOwnDuplicate && await isPortFree(port))
|
|
6485
|
+
continue;
|
|
6486
|
+
do
|
|
6487
|
+
port += 1;
|
|
6488
|
+
while (own.has(port) || !await isPortFree(port));
|
|
6489
|
+
own.add(port);
|
|
6490
|
+
patch[key] = String(port);
|
|
6491
|
+
console.log(` \u2192 ${key} ${env[key]} is taken; using ${port}`);
|
|
6492
|
+
}
|
|
6493
|
+
if (patch.FOSSIL_PORT && !priorKeys.has("FOSSIL_URL")) {
|
|
6494
|
+
patch.FOSSIL_URL = `http://127.0.0.1:${patch.FOSSIL_PORT}`;
|
|
6495
|
+
}
|
|
6496
|
+
return Object.keys(patch).length ? updateInstanceEnv(path, patch) : env;
|
|
6497
|
+
}
|
|
6180
6498
|
async function warnOnVersionSkew(env) {
|
|
6181
6499
|
try {
|
|
6182
6500
|
const res = await fetch(`http://127.0.0.1:${env.WEBAPI_PORT}/health`, {
|
|
@@ -6248,12 +6566,15 @@ async function runInstall(argv) {
|
|
|
6248
6566
|
}
|
|
6249
6567
|
step(2, TOTAL, "Configuration");
|
|
6250
6568
|
const firstRun = !existsSync8(paths.instanceEnv);
|
|
6569
|
+
const priorKeys = new Set(firstRun ? [] : Object.keys(parseEnv(readFileSync12(paths.instanceEnv, "utf8"))));
|
|
6251
6570
|
let env = loadOrCreateInstanceEnv(paths.instanceEnv, {
|
|
6252
6571
|
portBase,
|
|
6253
6572
|
appTag: VERSION === DEV_VERSION ? "main" : VERSION,
|
|
6254
6573
|
meiliMasterKey: meiliKey
|
|
6255
6574
|
});
|
|
6256
6575
|
console.log(` ${firstRun ? "generated" : "reused"} ${paths.instanceEnv}`);
|
|
6576
|
+
if (!firstRun)
|
|
6577
|
+
env = await relocateAddedPorts(paths.instanceEnv, env, priorKeys);
|
|
6257
6578
|
if (!firstRun && priorAppTag && priorAppTag !== env.APP_TAG) {
|
|
6258
6579
|
console.log(` app image pin ${priorAppTag} \u2192 ${env.APP_TAG} (follows this CLI)`);
|
|
6259
6580
|
}
|