@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/
|
|
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/
|
|
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/
|
|
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 = [
|
|
813
|
+
// packages/shared/src/blueprint/env.ts
|
|
814
|
+
var ENV_VARS = [
|
|
775
815
|
{
|
|
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
|
-
}
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
878
|
+
name: "COUCHDB_PASSWORD",
|
|
879
|
+
scope: "secret",
|
|
880
|
+
description: "CouchDB password (blank in bundled dev)"
|
|
1135
881
|
},
|
|
1136
882
|
{
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
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
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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: "
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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: "
|
|
1198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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
|
-
|
|
1298
|
-
|
|
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:
|
|
1310
|
-
|
|
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: "
|
|
1314
|
-
category: "
|
|
1070
|
+
event: "CwdChanged",
|
|
1071
|
+
category: "environment",
|
|
1315
1072
|
canBlock: false,
|
|
1316
1073
|
wired: false,
|
|
1317
|
-
ignoreReason: "
|
|
1318
|
-
summary: "The
|
|
1074
|
+
ignoreReason: "Host signal, out of session scope.",
|
|
1075
|
+
summary: "The working directory changes during a session."
|
|
1319
1076
|
},
|
|
1320
1077
|
{
|
|
1321
|
-
event: "
|
|
1322
|
-
category: "
|
|
1078
|
+
event: "FileChanged",
|
|
1079
|
+
category: "environment",
|
|
1323
1080
|
canBlock: false,
|
|
1324
1081
|
wired: false,
|
|
1325
|
-
ignoreReason: "
|
|
1326
|
-
summary: "
|
|
1082
|
+
ignoreReason: "Watcher signal, out of session scope.",
|
|
1083
|
+
summary: "A watched file changes on disk."
|
|
1327
1084
|
},
|
|
1328
1085
|
{
|
|
1329
|
-
event: "
|
|
1330
|
-
category: "
|
|
1086
|
+
event: "ConfigChange",
|
|
1087
|
+
category: "environment",
|
|
1331
1088
|
canBlock: true,
|
|
1332
|
-
wired:
|
|
1333
|
-
|
|
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: "
|
|
1337
|
-
category: "
|
|
1094
|
+
event: "WorktreeCreate",
|
|
1095
|
+
category: "worktree",
|
|
1338
1096
|
canBlock: true,
|
|
1339
1097
|
wired: false,
|
|
1340
|
-
ignoreReason: "
|
|
1341
|
-
summary: "A
|
|
1098
|
+
ignoreReason: "Git-worktree lifecycle, irrelevant to session logging.",
|
|
1099
|
+
summary: "A git worktree is created (`--worktree`)."
|
|
1342
1100
|
},
|
|
1343
1101
|
{
|
|
1344
|
-
event: "
|
|
1345
|
-
category: "
|
|
1346
|
-
canBlock:
|
|
1102
|
+
event: "WorktreeRemove",
|
|
1103
|
+
category: "worktree",
|
|
1104
|
+
canBlock: false,
|
|
1347
1105
|
wired: false,
|
|
1348
|
-
ignoreReason: "
|
|
1349
|
-
summary: "
|
|
1106
|
+
ignoreReason: "Git-worktree lifecycle, irrelevant to session logging.",
|
|
1107
|
+
summary: "A git worktree is removed."
|
|
1350
1108
|
},
|
|
1351
1109
|
{
|
|
1352
|
-
event: "
|
|
1353
|
-
category: "
|
|
1110
|
+
event: "PreCompact",
|
|
1111
|
+
category: "compaction",
|
|
1354
1112
|
canBlock: true,
|
|
1355
|
-
wired:
|
|
1356
|
-
|
|
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: "
|
|
1361
|
-
category: "
|
|
1117
|
+
event: "PostCompact",
|
|
1118
|
+
category: "compaction",
|
|
1362
1119
|
canBlock: false,
|
|
1363
|
-
wired:
|
|
1364
|
-
|
|
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: "
|
|
1369
|
-
category: "
|
|
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
|
|
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: "
|
|
1376
|
-
category: "
|
|
1138
|
+
event: "SessionEnd",
|
|
1139
|
+
category: "session-end",
|
|
1377
1140
|
canBlock: false,
|
|
1378
1141
|
wired: true,
|
|
1379
|
-
summary: "
|
|
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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
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
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
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
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
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
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1645
|
-
|
|
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
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
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
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
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
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
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
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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: "
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
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: "
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
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
|
-
|
|
1792
|
-
|
|
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
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
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
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
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
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
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
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
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
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
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
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
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
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
var
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
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
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
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
|
-
|
|
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/"
|
|
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
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
const
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
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
|
-
|
|
2072
|
-
|
|
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
|
|
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
|
-
};
|
|
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
|
|
3377
|
-
# Do NOT edit by hand \u2014 run \`bun run gen:compose\` to regenerate, and change the
|
|
3378
|
-
# (packages/shared/src/
|
|
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
|
|
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/
|
|
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
|
-
|
|
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(
|
|
5137
|
+
data = readFileSync5(transcriptPath, "utf8");
|
|
4831
5138
|
} catch {
|
|
4832
5139
|
return;
|
|
4833
5140
|
}
|
|
4834
|
-
await
|
|
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 =
|
|
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
|
|
5169
|
+
var TIMEOUTS = { SessionStart: 180, SessionEnd: 60 };
|
|
4856
5170
|
function hookTimeout(event) {
|
|
4857
|
-
return
|
|
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 =
|
|
4967
|
-
|
|
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
|
}
|