@hybridlabor-api/bdb-synapse 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/bin/synapse +0 -0
- package/cmd/rubriceval/main.go +308 -0
- package/cmd/synapse/main.go +280 -0
- package/cmd/synapse/main_test.go +16 -0
- package/go.mod +7 -0
- package/go.sum +6 -0
- package/internal/adapter/adapter.go +1117 -0
- package/internal/adapter/adapter_test.go +518 -0
- package/internal/adapter/agy/adapter.go +193 -0
- package/internal/adapter/claudecode/adapter.go +415 -0
- package/internal/adapter/claudecode/adapter_test.go +260 -0
- package/internal/adapter/claudecode/agents.go +387 -0
- package/internal/adapter/claudecode/agents_test.go +480 -0
- package/internal/adapter/claudecode/summary_inputs_test.go +19 -0
- package/internal/adapter/codex/adapter.go +919 -0
- package/internal/adapter/codex/adapter_test.go +920 -0
- package/internal/adapter/codex/agents.go +401 -0
- package/internal/adapter/codex/agents_test.go +610 -0
- package/internal/adapter/codex/summary_inputs_test.go +20 -0
- package/internal/adapter/pi/adapter.go +483 -0
- package/internal/adapter/pi/adapter_test.go +517 -0
- package/internal/citymap/builder.go +1124 -0
- package/internal/citymap/builder_test.go +818 -0
- package/internal/judge/cache.go +180 -0
- package/internal/judge/cli.go +240 -0
- package/internal/judge/cli_test.go +68 -0
- package/internal/judge/fresh_summary_test.go +37 -0
- package/internal/judge/input.go +233 -0
- package/internal/judge/judge.go +416 -0
- package/internal/judge/judge_test.go +288 -0
- package/internal/judge/prompt.go +129 -0
- package/internal/judge/rubric.go +275 -0
- package/internal/judge/rubric_test.go +642 -0
- package/internal/model/agent.go +63 -0
- package/internal/model/agent_schema_test.go +86 -0
- package/internal/model/agent_test.go +64 -0
- package/internal/model/model.go +175 -0
- package/internal/model/report.go +166 -0
- package/internal/model/stats.go +151 -0
- package/internal/model/stats_test.go +89 -0
- package/internal/model/trace_schema_test.go +67 -0
- package/internal/server/analyze.go +286 -0
- package/internal/server/analyze_test.go +297 -0
- package/internal/server/codex_index_test.go +40 -0
- package/internal/server/hardening_test.go +147 -0
- package/internal/server/reportindex.go +91 -0
- package/internal/server/reportindex_test.go +116 -0
- package/internal/server/server.go +1099 -0
- package/internal/server/server_test.go +1389 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-italic-CGbN9UgK.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-ext-standard-normal-CJcjJNj7.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-italic-lSdLDfvT.woff2 +0 -0
- package/internal/server/static/assets/fraunces-latin-standard-normal-DihXLNYH.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-italic-DxWqP7Ku.woff2 +0 -0
- package/internal/server/static/assets/fraunces-vietnamese-standard-normal-Czevyj-6.woff2 +0 -0
- package/internal/server/static/assets/index-BNoY_BiB.css +1 -0
- package/internal/server/static/assets/index-C_adLrJr.js +3 -0
- package/internal/server/static/assets/react-gcHzaSmV.js +10 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-ext-wght-normal-hsMS0n0O.woff2 +0 -0
- package/internal/server/static/assets/schibsted-grotesk-latin-wght-normal-Bb8VGrTG.woff2 +0 -0
- package/internal/server/static/assets/three-DnGjZfD1.js +4012 -0
- package/internal/server/static/index.html +26 -0
- package/internal/server/tracestore.go +173 -0
- package/internal/server/tracestore_test.go +51 -0
- package/internal/textutil/truncate.go +30 -0
- package/internal/textutil/truncate_test.go +39 -0
- package/package.json +35 -0
- package/web/e2e/agent-lens.spec.ts +688 -0
- package/web/index.html +23 -0
- package/web/package-lock.json +1933 -0
- package/web/package.json +33 -0
- package/web/playwright.config.ts +24 -0
- package/web/src/App.tsx +876 -0
- package/web/src/api/client.ts +74 -0
- package/web/src/main.tsx +12 -0
- package/web/src/playback/recorder.ts +160 -0
- package/web/src/playback/reducer.ts +91 -0
- package/web/src/scene/CityScene.tsx +638 -0
- package/web/src/scene/TreeScene.tsx +656 -0
- package/web/src/scene/dirLabels.ts +145 -0
- package/web/src/scene/sceneUtils.ts +144 -0
- package/web/src/scene/textures.ts +60 -0
- package/web/src/scene/trail.ts +79 -0
- package/web/src/scene/treeLayout.ts +169 -0
- package/web/src/state/filters.ts +40 -0
- package/web/src/state/store.ts +83 -0
- package/web/src/styles.css +2565 -0
- package/web/src/types.ts +315 -0
- package/web/src/ui/AgentsPanel.tsx +376 -0
- package/web/src/ui/Dock.tsx +104 -0
- package/web/src/ui/Hud.tsx +335 -0
- package/web/src/ui/Inspector.tsx +107 -0
- package/web/src/ui/LogoMark.tsx +38 -0
- package/web/src/ui/ReportPanel.tsx +491 -0
- package/web/src/ui/SessionRail.tsx +316 -0
- package/web/src/ui/Timeline.tsx +458 -0
- package/web/src/ui/ViewPanel.tsx +45 -0
- package/web/src/ui/shortcuts.ts +4 -0
- package/web/tsconfig.json +21 -0
- package/web/vite.config.ts +28 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"flag"
|
|
7
|
+
"fmt"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
|
|
11
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/adapter"
|
|
12
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/adapter/claudecode"
|
|
13
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/adapter/codex"
|
|
14
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/adapter/pi"
|
|
15
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/citymap"
|
|
16
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/judge"
|
|
17
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/model"
|
|
18
|
+
"github.com/hybridlabor-api/bdb-synapse/internal/server"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
func main() {
|
|
22
|
+
if err := run(os.Args[1:]); err != nil {
|
|
23
|
+
fmt.Fprintln(os.Stderr, "synapse:", err)
|
|
24
|
+
os.Exit(1)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func run(args []string) error {
|
|
29
|
+
if len(args) == 0 {
|
|
30
|
+
return serve(args)
|
|
31
|
+
}
|
|
32
|
+
switch args[0] {
|
|
33
|
+
case "serve":
|
|
34
|
+
return serve(args[1:])
|
|
35
|
+
case "open":
|
|
36
|
+
return open(args[1:])
|
|
37
|
+
case "map":
|
|
38
|
+
return openMap(args[1:])
|
|
39
|
+
case "build":
|
|
40
|
+
return build(args[1:])
|
|
41
|
+
case "trace":
|
|
42
|
+
return trace(args[1:])
|
|
43
|
+
case "analyze":
|
|
44
|
+
return analyze(args[1:])
|
|
45
|
+
case "-h", "--help", "help":
|
|
46
|
+
usage()
|
|
47
|
+
return nil
|
|
48
|
+
default:
|
|
49
|
+
return fmt.Errorf("unknown command %q", args[0])
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func serve(args []string) error {
|
|
54
|
+
fs := flag.NewFlagSet("serve", flag.ExitOnError)
|
|
55
|
+
port := fs.Int("port", 0, "port to bind on 127.0.0.1")
|
|
56
|
+
claudeDir := fs.String("claude-dir", claudecode.DefaultDir(), "Claude Code projects directory")
|
|
57
|
+
codexDir := fs.String("codex-dir", codex.DefaultDir(), "Codex sessions directory")
|
|
58
|
+
piDir := fs.String("pi-dir", pi.DefaultDir(), "pi sessions directory")
|
|
59
|
+
agyDir := fs.String("agy-dir", filepath.Join(os.Getenv("HOME"), ".gemini", "antigravity-cli", "brain"), "agy sessions directory")
|
|
60
|
+
dev := fs.Bool("dev", false, "prefer web/dist from the working tree")
|
|
61
|
+
noOpen := fs.Bool("no-open", false, "serve without opening a browser")
|
|
62
|
+
if err := fs.Parse(args); err != nil {
|
|
63
|
+
return err
|
|
64
|
+
}
|
|
65
|
+
return server.New(server.Config{Port: *port, ClaudeDir: *claudeDir, CodexDir: *codexDir, PiDir: *piDir, AgyDir: *agyDir, Dev: *dev}).Start(!*noOpen)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func open(args []string) error {
|
|
69
|
+
fs := flag.NewFlagSet("open", flag.ExitOnError)
|
|
70
|
+
port := fs.Int("port", 0, "port to bind on 127.0.0.1")
|
|
71
|
+
claudeDir := fs.String("claude-dir", claudecode.DefaultDir(), "Claude Code projects directory")
|
|
72
|
+
codexDir := fs.String("codex-dir", codex.DefaultDir(), "Codex sessions directory")
|
|
73
|
+
piDir := fs.String("pi-dir", pi.DefaultDir(), "pi sessions directory")
|
|
74
|
+
agyDir := fs.String("agy-dir", filepath.Join(os.Getenv("HOME"), ".gemini", "antigravity-cli", "brain"), "agy sessions directory")
|
|
75
|
+
noOpen := fs.Bool("no-open", false, "serve without opening a browser")
|
|
76
|
+
if err := fs.Parse(args); err != nil {
|
|
77
|
+
return err
|
|
78
|
+
}
|
|
79
|
+
if fs.NArg() != 1 {
|
|
80
|
+
return fmt.Errorf("usage: synapse open [--no-open] <session.jsonl>")
|
|
81
|
+
}
|
|
82
|
+
session, err := filepath.Abs(fs.Arg(0))
|
|
83
|
+
if err != nil {
|
|
84
|
+
return err
|
|
85
|
+
}
|
|
86
|
+
return server.New(server.Config{Port: *port, ClaudeDir: *claudeDir, CodexDir: *codexDir, PiDir: *piDir, AgyDir: *agyDir, OpenSession: session}).Start(!*noOpen)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func openMap(args []string) error {
|
|
90
|
+
fs := flag.NewFlagSet("map", flag.ExitOnError)
|
|
91
|
+
port := fs.Int("port", 0, "port to bind on 127.0.0.1")
|
|
92
|
+
dev := fs.Bool("dev", false, "prefer web/dist from the working tree")
|
|
93
|
+
noOpen := fs.Bool("no-open", false, "serve without opening a browser")
|
|
94
|
+
if err := fs.Parse(args); err != nil {
|
|
95
|
+
return err
|
|
96
|
+
}
|
|
97
|
+
if fs.NArg() != 1 {
|
|
98
|
+
return fmt.Errorf("usage: synapse map [--no-open] <repo>")
|
|
99
|
+
}
|
|
100
|
+
repo, err := filepath.Abs(fs.Arg(0))
|
|
101
|
+
if err != nil {
|
|
102
|
+
return err
|
|
103
|
+
}
|
|
104
|
+
return server.New(server.Config{Port: *port, Dev: *dev, RepoRoot: repo, MapOnly: true}).Start(!*noOpen)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func build(args []string) error {
|
|
108
|
+
positional, out, err := parseOutputArgs(args)
|
|
109
|
+
if err != nil {
|
|
110
|
+
return err
|
|
111
|
+
}
|
|
112
|
+
if len(positional) != 1 {
|
|
113
|
+
return fmt.Errorf("usage: synapse build <repo> [-o out]")
|
|
114
|
+
}
|
|
115
|
+
city, err := citymap.Builder{}.Build(positional[0], nil)
|
|
116
|
+
if err != nil {
|
|
117
|
+
return err
|
|
118
|
+
}
|
|
119
|
+
return writeJSON(out, city)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func trace(args []string) error {
|
|
123
|
+
positional, out, err := parseOutputArgs(args)
|
|
124
|
+
if err != nil {
|
|
125
|
+
return err
|
|
126
|
+
}
|
|
127
|
+
if len(positional) != 1 {
|
|
128
|
+
return fmt.Errorf("usage: synapse trace <session.jsonl> [-o out]")
|
|
129
|
+
}
|
|
130
|
+
tr, err := parseTrace(positional[0])
|
|
131
|
+
if err != nil {
|
|
132
|
+
return err
|
|
133
|
+
}
|
|
134
|
+
return writeJSON(out, tr)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// judgeMatches reports whether a cached report satisfies an explicit judge
|
|
138
|
+
// choice; unset flags match anything. A model matches on either the
|
|
139
|
+
// canonical name the run recorded (claude-sonnet-5) or the alias it was
|
|
140
|
+
// requested with (sonnet) — so repeating an aliased request hits the cache
|
|
141
|
+
// instead of paying for a fresh run every time.
|
|
142
|
+
func judgeMatches(report *model.Report, cli, modelName string) bool {
|
|
143
|
+
if cli != "" && report.Judge.CLI != cli {
|
|
144
|
+
return false
|
|
145
|
+
}
|
|
146
|
+
if modelName != "" && report.Judge.Model != modelName && report.Judge.RequestedModel != modelName {
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func analyze(args []string) error {
|
|
153
|
+
fs := flag.NewFlagSet("analyze", flag.ExitOnError)
|
|
154
|
+
out := fs.String("o", "", "write the report to this file instead of stdout")
|
|
155
|
+
judgeCLI := fs.String("judge", "", "judge CLI to use: claude or codex (default: auto-detect)")
|
|
156
|
+
judgeModel := fs.String("model", "", "judge model override, e.g. sonnet or gpt-5.6-sol (default: the CLI's default)")
|
|
157
|
+
noCache := fs.Bool("no-cache", false, "re-run the judge even when a fresh cached report exists")
|
|
158
|
+
noRubric := fs.Bool("no-rubric", false, "skip the task rubric layer: one dimensions-only judge call, bypassing the report cache")
|
|
159
|
+
timeout := fs.Duration("timeout", judge.DefaultTimeout, "judge subprocess timeout")
|
|
160
|
+
// Accept flags after the positional argument, matching trace/build.
|
|
161
|
+
var positional []string
|
|
162
|
+
for {
|
|
163
|
+
if err := fs.Parse(args); err != nil {
|
|
164
|
+
return err
|
|
165
|
+
}
|
|
166
|
+
if fs.NArg() == 0 {
|
|
167
|
+
break
|
|
168
|
+
}
|
|
169
|
+
positional = append(positional, fs.Arg(0))
|
|
170
|
+
args = fs.Args()[1:]
|
|
171
|
+
}
|
|
172
|
+
if len(positional) != 1 {
|
|
173
|
+
return fmt.Errorf("usage: synapse analyze <session.jsonl> [-o out] [--judge claude|codex] [--model name] [--no-cache] [--no-rubric]")
|
|
174
|
+
}
|
|
175
|
+
session, err := filepath.Abs(positional[0])
|
|
176
|
+
if err != nil {
|
|
177
|
+
return err
|
|
178
|
+
}
|
|
179
|
+
tr, err := parseTrace(session)
|
|
180
|
+
if err != nil {
|
|
181
|
+
return err
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
cache := judge.Cache{Dir: judge.DefaultCacheDir()}
|
|
185
|
+
key := adapter.SessionKey(tr.Session.Harness, session)
|
|
186
|
+
// --no-cache means a fully fresh run: the cached report is neither
|
|
187
|
+
// returned nor mined for a reusable rubric. --no-rubric bypasses the
|
|
188
|
+
// cache in both directions — returning a cached rubric-ful report would
|
|
189
|
+
// contradict the flag, and storing a rubric-less one would downgrade a
|
|
190
|
+
// richer cache entry — so the flag always costs one fresh call.
|
|
191
|
+
var cached *model.Report
|
|
192
|
+
if !*noCache && !*noRubric {
|
|
193
|
+
cached = cache.Load(key)
|
|
194
|
+
// A rubric-enabled request is only answered from cache when the report
|
|
195
|
+
// already settles the rubric question; a rubric-less fresh report gets
|
|
196
|
+
// re-run rather than silently returned without the layer.
|
|
197
|
+
if judge.FreshAgainstTrace(cached, tr) && judgeMatches(cached, *judgeCLI, *judgeModel) &&
|
|
198
|
+
judge.RubricSatisfied(cached) {
|
|
199
|
+
fmt.Fprintln(os.Stderr, "synapse: using cached report (pass --no-cache to re-run)")
|
|
200
|
+
return writeJSON(*out, cached)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
|
205
|
+
defer cancel()
|
|
206
|
+
fmt.Fprintf(os.Stderr, "synapse: judging %d events, this can take a minute or two…\n", tr.Session.EventCount)
|
|
207
|
+
report, err := judge.Analyze(ctx, tr, judge.Options{CLI: *judgeCLI, Model: *judgeModel, NoRubric: *noRubric, CachedReport: cached})
|
|
208
|
+
if err != nil {
|
|
209
|
+
return err
|
|
210
|
+
}
|
|
211
|
+
if !*noRubric {
|
|
212
|
+
if err := cache.Store(key, report); err != nil {
|
|
213
|
+
fmt.Fprintln(os.Stderr, "synapse: report cache write failed:", err)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return writeJSON(*out, report)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func parseTrace(path string) (*model.Trace, error) {
|
|
220
|
+
var lastErr error
|
|
221
|
+
for _, source := range []adapter.Source{claudecode.Adapter{}, codex.Adapter{}, pi.Adapter{}} {
|
|
222
|
+
trace, err := source.Parse(path)
|
|
223
|
+
if err == nil {
|
|
224
|
+
return trace, nil
|
|
225
|
+
}
|
|
226
|
+
lastErr = err
|
|
227
|
+
}
|
|
228
|
+
if lastErr != nil {
|
|
229
|
+
return nil, lastErr
|
|
230
|
+
}
|
|
231
|
+
return nil, fmt.Errorf("no session adapters configured")
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
func parseOutputArgs(args []string) ([]string, string, error) {
|
|
235
|
+
var out string
|
|
236
|
+
var positional []string
|
|
237
|
+
for i := 0; i < len(args); i++ {
|
|
238
|
+
switch args[i] {
|
|
239
|
+
case "-o", "--output":
|
|
240
|
+
i++
|
|
241
|
+
if i >= len(args) {
|
|
242
|
+
return nil, "", fmt.Errorf("%s requires a value", args[i-1])
|
|
243
|
+
}
|
|
244
|
+
out = args[i]
|
|
245
|
+
default:
|
|
246
|
+
positional = append(positional, args[i])
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return positional, out, nil
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
func writeJSON(out string, v any) error {
|
|
253
|
+
var f *os.File
|
|
254
|
+
var err error
|
|
255
|
+
if out == "" {
|
|
256
|
+
f = os.Stdout
|
|
257
|
+
} else {
|
|
258
|
+
f, err = os.Create(out)
|
|
259
|
+
if err != nil {
|
|
260
|
+
return err
|
|
261
|
+
}
|
|
262
|
+
defer f.Close()
|
|
263
|
+
}
|
|
264
|
+
enc := json.NewEncoder(f)
|
|
265
|
+
enc.SetIndent("", " ")
|
|
266
|
+
return enc.Encode(v)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func usage() {
|
|
270
|
+
fmt.Println(`synapse
|
|
271
|
+
|
|
272
|
+
Usage:
|
|
273
|
+
synapse serve on a random local port and open the UI
|
|
274
|
+
synapse serve [--port N] [--no-open] [--claude-dir DIR] [--codex-dir DIR] [--pi-dir DIR]
|
|
275
|
+
synapse open [--no-open] <session.jsonl> open a specific Claude Code, Codex, or pi session
|
|
276
|
+
synapse map [--no-open] <repo> open the repository citymap with no session
|
|
277
|
+
synapse build <repo> [-o out] write citymap.json
|
|
278
|
+
synapse trace <session> [-o out] write trace.json
|
|
279
|
+
synapse analyze <session> [-o out] [--judge claude|codex] [--no-cache] [--no-rubric] evaluate a session with a local agent CLI`)
|
|
280
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import "testing"
|
|
4
|
+
|
|
5
|
+
func TestParseOutputArgsAllowsTrailingOutput(t *testing.T) {
|
|
6
|
+
positional, out, err := parseOutputArgs([]string{"repo", "-o", "citymap.json"})
|
|
7
|
+
if err != nil {
|
|
8
|
+
t.Fatal(err)
|
|
9
|
+
}
|
|
10
|
+
if len(positional) != 1 || positional[0] != "repo" {
|
|
11
|
+
t.Fatalf("positional = %#v", positional)
|
|
12
|
+
}
|
|
13
|
+
if out != "citymap.json" {
|
|
14
|
+
t.Fatalf("out = %q", out)
|
|
15
|
+
}
|
|
16
|
+
}
|
package/go.mod
ADDED
package/go.sum
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
|
2
|
+
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
|
3
|
+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
|
|
4
|
+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
|
|
5
|
+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
|
6
|
+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|