@bothat-io/molenkopf 0.1.2
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/.env.example +2 -0
- package/LICENSE +21 -0
- package/README.md +199 -0
- package/SECURITY.md +36 -0
- package/bin/launcher.js +76 -0
- package/bin/molenkopf.js +4 -0
- package/docs/DEPLOYMENT.md +104 -0
- package/docs/MOLENKOPF_PLUGIN_API.md +113 -0
- package/docs/MOLENKOPF_PROVIDER_ENV.md +123 -0
- package/docs/MOLENKOPF_USAGE.md +195 -0
- package/docs/PRODUCT_INTENT.md +36 -0
- package/docs/THREAT_MODEL.md +94 -0
- package/molenkopf.config.example.json +68 -0
- package/package.json +98 -0
- package/packages/core/src/auth/password.ts +47 -0
- package/packages/core/src/auth/session.ts +64 -0
- package/packages/core/src/ci/ci-mode.ts +71 -0
- package/packages/core/src/compression/content-classifier.ts +25 -0
- package/packages/core/src/compression/context-compressor.ts +48 -0
- package/packages/core/src/compression/json-compressor.ts +54 -0
- package/packages/core/src/compression/log-compressor.ts +32 -0
- package/packages/core/src/compression/operational-block-compressor.ts +43 -0
- package/packages/core/src/compression/stacktrace-compressor.ts +23 -0
- package/packages/core/src/config/config-policies.ts +146 -0
- package/packages/core/src/config/molenkopf-config.ts +137 -0
- package/packages/core/src/config/provider-config.ts +139 -0
- package/packages/core/src/events/event-bus.ts +88 -0
- package/packages/core/src/identity/api-keys.ts +149 -0
- package/packages/core/src/identity/budget.ts +51 -0
- package/packages/core/src/identity/db.ts +68 -0
- package/packages/core/src/identity/identity-store.ts +175 -0
- package/packages/core/src/identity/identity-validation.ts +102 -0
- package/packages/core/src/identity/key-permissions.ts +18 -0
- package/packages/core/src/identity/pricing.ts +11 -0
- package/packages/core/src/identity/types.ts +87 -0
- package/packages/core/src/identity/usage-snapshot.ts +116 -0
- package/packages/core/src/manifest/audit-activity.ts +74 -0
- package/packages/core/src/manifest/audit-metrics.ts +7 -0
- package/packages/core/src/manifest/audit-safety.ts +113 -0
- package/packages/core/src/manifest/audit-store.ts +189 -0
- package/packages/core/src/manifest/audit-summary.ts +184 -0
- package/packages/core/src/manifest/usage-meter.ts +105 -0
- package/packages/core/src/memory/memory-extractor.ts +57 -0
- package/packages/core/src/memory/memory-graph.ts +55 -0
- package/packages/core/src/pipeline/json-string-spans.ts +143 -0
- package/packages/core/src/pipeline/openai-request-rewriter.ts +66 -0
- package/packages/core/src/plugins/builtin-plugin-descriptors.ts +10 -0
- package/packages/core/src/plugins/builtin-plugin-modules.ts +9 -0
- package/packages/core/src/plugins/plugin-api.ts +96 -0
- package/packages/core/src/plugins/plugin-catalog.ts +42 -0
- package/packages/core/src/plugins/plugin-descriptor.ts +51 -0
- package/packages/core/src/plugins/plugin-sdk.ts +47 -0
- package/packages/core/src/plugins/static-pipeline.ts +5 -0
- package/packages/core/src/profiles/profile-router.ts +45 -0
- package/packages/core/src/providers/provider-catalog.ts +186 -0
- package/packages/core/src/routing/distribution.ts +31 -0
- package/packages/core/src/security/secret-redactor.ts +139 -0
- package/packages/core/src/security/target-policy.ts +61 -0
- package/packages/core/src/storage/local-paths.ts +6 -0
- package/packages/core/src/storage/private-state.ts +30 -0
- package/packages/core/src/storage/purge-dir.ts +10 -0
- package/packages/core/src/store/retrieval-store.ts +114 -0
- package/packages/core/src/utils/hash.ts +9 -0
- package/packages/core/src/utils/text.ts +18 -0
- package/packages/core/src/utils/tokens.ts +3 -0
- package/packages/dashboard/dist/assets/index-B_aSPgHx.js +11 -0
- package/packages/dashboard/dist/assets/index-D6z2TEL2.css +1 -0
- package/packages/dashboard/dist/favicon.png +0 -0
- package/packages/dashboard/dist/index.html +15 -0
- package/packages/dashboard/dist/molenkopf-logo.png +0 -0
- package/packages/dashboard/public/favicon.png +0 -0
- package/packages/dashboard/public/molenkopf-logo.png +0 -0
- package/packages/plugins/context-compressor-plugin/descriptor.ts +19 -0
- package/packages/plugins/context-compressor-plugin/page.html +191 -0
- package/packages/plugins/context-compressor-plugin/plugin.ts +40 -0
- package/packages/plugins/obsidian-graph-plugin/descriptor.ts +19 -0
- package/packages/plugins/obsidian-graph-plugin/page.html +68 -0
- package/packages/plugins/obsidian-graph-plugin/plugin.ts +27 -0
- package/packages/plugins/shared/audit-projects.ts +32 -0
- package/packages/proxy/src/cli/args.ts +34 -0
- package/packages/proxy/src/cli/config-loader.ts +43 -0
- package/packages/proxy/src/cli/env-file.ts +43 -0
- package/packages/proxy/src/cli/main.ts +132 -0
- package/packages/proxy/src/cli/profile-server.ts +176 -0
- package/packages/proxy/src/cli/target.ts +7 -0
- package/packages/proxy/src/http/agent-drafts.ts +103 -0
- package/packages/proxy/src/http/agent-router.ts +69 -0
- package/packages/proxy/src/http/audit-view.ts +15 -0
- package/packages/proxy/src/http/auth-state.ts +44 -0
- package/packages/proxy/src/http/budget-gate.ts +45 -0
- package/packages/proxy/src/http/budget-warnings.ts +7 -0
- package/packages/proxy/src/http/cli-stream-response.ts +51 -0
- package/packages/proxy/src/http/client-identity.ts +51 -0
- package/packages/proxy/src/http/communication-graph.ts +139 -0
- package/packages/proxy/src/http/control-plane-guard.ts +56 -0
- package/packages/proxy/src/http/dashboard-assets.ts +115 -0
- package/packages/proxy/src/http/encoded-usage-meter.ts +32 -0
- package/packages/proxy/src/http/header-utils.ts +65 -0
- package/packages/proxy/src/http/identity-id.ts +11 -0
- package/packages/proxy/src/http/local-api-agent-actions.ts +17 -0
- package/packages/proxy/src/http/local-api-auth.ts +120 -0
- package/packages/proxy/src/http/local-api-consumer-actions.ts +20 -0
- package/packages/proxy/src/http/local-api-identity.ts +194 -0
- package/packages/proxy/src/http/local-api-io.ts +82 -0
- package/packages/proxy/src/http/local-api-keys.ts +126 -0
- package/packages/proxy/src/http/local-api-pipeline.ts +41 -0
- package/packages/proxy/src/http/local-api-plugin-actions.ts +31 -0
- package/packages/proxy/src/http/local-api-provider-actions.ts +181 -0
- package/packages/proxy/src/http/local-api-retention.ts +28 -0
- package/packages/proxy/src/http/local-api-runtime-auth.ts +119 -0
- package/packages/proxy/src/http/local-api-scope.ts +47 -0
- package/packages/proxy/src/http/local-api-state.ts +180 -0
- package/packages/proxy/src/http/local-api.ts +166 -0
- package/packages/proxy/src/http/password-policy.ts +5 -0
- package/packages/proxy/src/http/plugin-data.ts +38 -0
- package/packages/proxy/src/http/plugin-host.ts +87 -0
- package/packages/proxy/src/http/plugin-modules.ts +1 -0
- package/packages/proxy/src/http/plugin-page-loader.ts +24 -0
- package/packages/proxy/src/http/plugin-pipeline.ts +125 -0
- package/packages/proxy/src/http/provider-access.ts +33 -0
- package/packages/proxy/src/http/provider-http-test.ts +133 -0
- package/packages/proxy/src/http/provider-input.ts +39 -0
- package/packages/proxy/src/http/provider-routing-snapshot.ts +28 -0
- package/packages/proxy/src/http/provider-test.ts +149 -0
- package/packages/proxy/src/http/proxy-identity.ts +78 -0
- package/packages/proxy/src/http/public-bind.ts +8 -0
- package/packages/proxy/src/http/request-finish.ts +62 -0
- package/packages/proxy/src/http/request-path.ts +8 -0
- package/packages/proxy/src/http/request-policy.ts +46 -0
- package/packages/proxy/src/http/runtime-auth-proof.ts +55 -0
- package/packages/proxy/src/http/runtime-auth-registry.ts +105 -0
- package/packages/proxy/src/http/runtime-settings.ts +199 -0
- package/packages/proxy/src/http/runtime-state.ts +198 -0
- package/packages/proxy/src/http/server-io.ts +80 -0
- package/packages/proxy/src/http/server-types.ts +17 -0
- package/packages/proxy/src/http/server.ts +190 -0
- package/packages/proxy/src/http/session-secret.ts +19 -0
- package/packages/proxy/src/http/streaming-proxy.ts +88 -0
- package/packages/proxy/src/http/usage-accounting.ts +100 -0
- package/packages/proxy/src/http/usage-restore.ts +15 -0
- package/packages/proxy/src/runtime/cli-diagnostics.ts +64 -0
- package/packages/proxy/src/runtime/cli-env.ts +22 -0
- package/packages/proxy/src/runtime/cli-executor.ts +134 -0
- package/packages/proxy/src/runtime/cli-provider.ts +162 -0
- package/packages/proxy/src/runtime/cli-request.ts +79 -0
- package/packages/proxy/src/runtime/codex-runtime-config.ts +37 -0
- package/packages/proxy/src/runtime/runtime-profile.ts +170 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--bg:#f4f1ea;--ink:#1d1a17;--muted:#746c60;--line:#ded6ca;--card:#fbf9f4;--accent:#0f7668;--accent-2:#0a4a3f;--soft:#d9eee8;--danger:#b4520a;--shadow:0 1px 2px rgba(28,26,23,.05),0 8px 24px rgba(28,26,23,.06);font-family:Segoe UI,Arial,sans-serif;color:var(--ink);background:var(--bg)}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:linear-gradient(180deg,#f7f4ed,#eee8dd)}button,input,select,textarea{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.55}a{color:var(--accent-2)}section{margin-top:26px}.wrap{width:94%;max-width:1560px;margin:0 auto;padding:18px 36px 64px}.wrap.syncing:before{content:"";position:fixed;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:page-load 1.15s ease-in-out infinite;z-index:1000}@keyframes page-load{0%{transform:translate(-100%)}to{transform:translate(100%)}}.topbar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:6px}.brand-title{border:0;background:transparent;color:var(--ink);padding:0;display:inline-flex;align-items:center;gap:10px}.brand-mark{width:36px;height:36px;object-fit:contain;display:block}.brand-word{font:600 24px Georgia,serif}.status{display:flex;align-items:center;justify-content:flex-end;gap:10px;font-size:13px;flex-wrap:wrap}.conn-pill{display:inline-flex;gap:7px;align-items:center;border-radius:999px;background:var(--soft);color:var(--accent-2);font-weight:600;padding:5px 12px}.live{width:8px;height:8px;border-radius:50%;background:var(--accent)}.idle{width:8px;height:8px;border-radius:50%;background:var(--danger)}.toptabs{display:flex;gap:22px;border-bottom:1px solid var(--line);margin-bottom:18px;overflow:auto}.toptabs button{border:0;border-bottom:2px solid transparent;background:transparent;padding:8px 0;font-weight:650;color:var(--muted)}.toptabs button.on,.toptabs button[aria-selected=true]{color:var(--ink);border-bottom-color:var(--accent)}.label{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 12px;font:600 13px Georgia,serif;text-transform:uppercase;letter-spacing:.12em;color:var(--muted)}.rs,.hint{color:var(--muted);font-size:13px;margin:3px 0}.ctl{display:flex;gap:8px;align-items:center;justify-content:flex-end;flex-wrap:nowrap}.ghost,.ctl button,.tabs button{border:1px solid var(--line);background:#fff;border-radius:8px;padding:6px 12px}.primary{border:0;background:var(--accent);color:#fff;border-radius:8px;padding:8px 15px;font-weight:650}.icon-btn{width:34px;height:34px;display:inline-grid;place-items:center;padding:0!important}.icon-btn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}.danger,.danger-text{color:var(--danger)!important}.empty{border:1px dashed var(--line);border-radius:12px;padding:22px;text-align:center;color:var(--muted);background:#ffffff59}.pill{display:inline-flex;border-radius:999px;background:var(--soft);color:var(--accent-2);font-weight:700;font-size:11px;padding:3px 8px;white-space:nowrap}.pill.off{background:#ece6da;color:var(--muted)}code{font-family:ui-monospace,Menlo,Consolas,monospace}.meter{height:7px;border-radius:99px;background:#ebe4d8;overflow:hidden}.meter span{display:block;height:100%;background:var(--accent)}.meter.warn span{background:var(--danger)}.meter.over span{background:#b21f1f}@media(max-width:1100px){.ctl{justify-content:flex-start}}@media(max-width:900px){.wrap{padding-left:18px;padding-right:18px}}@media(max-width:520px){.topbar{align-items:flex-start}.brand-mark{width:32px;height:32px}.brand-word{font-size:21px}.status{justify-content:flex-start}}.action-group{gap:8px}.action-group:empty{display:none}.dtable-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.dtable{width:100%;border-collapse:separate;border-spacing:0;background:var(--card);min-width:560px}.dtable th,.dtable td{padding:11px 15px;text-align:left;border-bottom:1px solid var(--line);font-size:13px;vertical-align:middle}.dtable thead th{font:600 11px Georgia,serif;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);background:#0f76680d}.dtable tbody tr:last-child td{border-bottom:0}.dtable tbody tr:hover{background:#0f76680a}.dtable tbody tr[draggable=true]{cursor:grab}.dtable td.num,.dtable th.num,.dtable td.is-right,.dtable th.is-right{text-align:right;font-variant-numeric:tabular-nums}.dtable .name{font-weight:650}.dtable td,.dtable .rs,.dtable .name{overflow-wrap:anywhere}.dtable code{font-size:12px}.data-table{table-layout:fixed}.data-table th:last-child,.data-table td:last-child{text-align:right}.data-table .ctl{justify-content:flex-end}.key-table{min-width:760px}.key-table code{display:inline-block;margin-right:8px}.admin-table,.provider-table{min-width:760px}.plugin-table{min-width:860px}.plugin-table th,.plugin-table td{padding:8px 10px}.plugin-table .plugin-summary{display:grid;gap:3px;min-width:0}.plugin-table .plugin-desc,.plugin-table .plugin-reason{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.tree-table{min-width:760px}.metric-strip{display:grid;grid-template-columns:repeat(var(--metric-count,3),minmax(0,1fr));gap:14px;overflow:visible;padding-bottom:2px}.box{min-width:0;padding:16px 18px;position:relative;overflow:hidden;background:var(--card);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.box:before{content:"";position:absolute;left:0;right:0;top:0;height:3px;background:linear-gradient(90deg,var(--accent),var(--accent-2))}.n{font:700 28px Georgia,serif}.t{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}.dashboard-section{margin-top:26px}.weight-cell{display:grid;grid-template-columns:minmax(110px,1fr) 48px;gap:8px;align-items:center}.weight-cell b{font-variant-numeric:tabular-nums;text-align:right}.plugin-toggle{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-width:96px;min-height:34px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);font-weight:650;padding:6px 10px}.plugin-toggle.is-on{border-color:#b4520a47;color:var(--danger);background:#fff8f0}.plugin-toggle.is-off{border-color:#0f76684d;color:var(--accent-2);background:var(--soft)}.plugin-toggle-dot{width:8px;height:8px;border-radius:50%;background:currentColor}.collapsible-group{background:transparent;border:0;border-bottom:1px solid var(--line);border-radius:0;box-shadow:none;overflow:hidden}.collapsible-group:last-child{border-bottom:0}.collapsible-group summary{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:13px 16px;cursor:pointer;list-style:none}.collapsible-group summary::-webkit-details-marker{display:none}.collapsible-group summary:before{content:"+";width:22px;height:22px;display:grid;place-items:center;border:1px solid var(--line);border-radius:7px;background:#fff;color:var(--muted);font-weight:700;flex:0 0 auto}.collapsible-group[open] summary:before{content:"-";color:var(--accent-2);background:var(--soft)}.collapsible-group.is-drop-active summary{background:#0f766812;outline:1px dashed var(--accent);outline-offset:-5px}.collapsible-main{display:grid;gap:3px;min-width:0;flex:1}.collapsible-main strong{display:block}.collapsible-main span{color:var(--muted);font-size:12px}.collapsible-side{display:flex;gap:12px;align-items:center;justify-content:flex-end;flex-wrap:wrap}.collapsible-side span{color:var(--muted);font-size:12px}.collapsible-actions{display:inline-flex}@media(max-width:760px){.collapsible-group summary{align-items:flex-start}.collapsible-side{justify-content:flex-start}}.team-tree-panel{background:var(--card);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow);overflow:hidden}.tree-table-wrap{overflow-x:auto;border-top:1px solid var(--line)}.tree-table-wrap .dtable{border:0;border-radius:0;box-shadow:none}.tree-empty{border:0;border-top:1px dashed var(--line);border-radius:0;background:transparent}.auth-screen{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px}.auth-loading:before{content:"";position:fixed;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:page-load 1.15s ease-in-out infinite;z-index:1000}.auth-loading img{width:42px;height:42px;object-fit:contain}.auth-card{width:410px;padding:28px;background:var(--card);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.auth-card h1{font:700 28px Georgia,serif;margin:10px 0;text-align:center}.auth-card p{color:var(--muted);text-align:center}.auth-brand{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;margin:0 auto 16px;text-align:center}.auth-brand img{width:34px;height:34px;object-fit:contain;display:block}.brand-kicker{font-weight:800;color:var(--accent-2);letter-spacing:.04em}.auth-form{display:flex;flex-direction:column;gap:12px}.auth-form label{font-size:12px;color:var(--muted);display:grid;gap:4px}.auth-form input{width:100%;min-width:0;border:1px solid var(--line);border-radius:8px;background:#fff;padding:9px 10px}.dashboard-notice{display:flex;justify-content:space-between;gap:12px;align-items:center;border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin-bottom:12px}.dashboard-notice.success{border-color:#0f766847;color:var(--accent-2);background:#0f766814}.dashboard-notice.error{border-color:#b74f1847;color:var(--danger);background:#fff8f0}.dashboard-notice.info{border-color:#4a5c6f3d;color:#334155;background:#4a5c6f0f}.dashboard-notice button{border:1px solid var(--line);background:#fff;border-radius:6px;padding:5px 10px}.dashboard-notice span{overflow-wrap:anywhere}.modal{position:fixed;inset:0;background:#1c1a1775;display:flex;align-items:center;justify-content:center;z-index:50;padding:18px}.modal-card{width:430px;max-width:100%;max-height:calc(100dvh - 36px);overflow-y:auto;padding:24px;background:var(--card);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.modal-card.wide{width:760px;height:min(660px,calc(100dvh - 36px));display:flex;flex-direction:column;overflow:hidden}.modal-card h2{font:700 24px Georgia,serif;margin:0 0 14px}.stack{display:flex;flex-direction:column;gap:11px}.stack label{font-size:12px;color:var(--muted);display:grid;gap:4px}.modal-actions{position:sticky;bottom:-24px;background:var(--card);display:flex;justify-content:flex-end;gap:10px;margin:16px -24px -24px;padding:14px 24px 24px;border-top:1px solid var(--line)}.modal-actions button{border:1px solid var(--line);background:#fff;border-radius:8px;padding:6px 12px}.modal-actions button.primary{border:0;background:var(--accent);color:#fff;border-radius:8px;padding:8px 15px;font-weight:650}.reveal{background:#1d1a17;color:#fff;border-radius:8px;padding:12px;overflow-wrap:anywhere}.msg{color:var(--danger);font-size:13px}.stack input,.stack select{width:100%;min-width:0;border:1px solid var(--line);border-radius:8px;background:#fff;padding:9px 10px}.checkline{display:flex;align-items:center;gap:8px}.checkline input{width:auto}.provider-add-content{display:flex;flex-direction:column;gap:18px;flex:1;min-height:0;overflow:auto;padding-right:2px}.form-panel{display:flex;flex-direction:column;gap:14px;flex:1;min-height:0}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.form-field{display:flex;flex-direction:column;gap:5px;font-size:12px;color:var(--muted);min-width:0}.form-field.full{grid-column:1/-1}.form-field input,.form-field select{width:100%;min-width:0;min-height:38px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);padding:9px 10px}.form-field select.select-control{appearance:none;padding-right:34px;background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);background-position:calc(100% - 17px) 16px,calc(100% - 12px) 16px;background-size:5px 5px;background-repeat:no-repeat}.form-field textarea{min-height:96px;resize:vertical;border:1px solid var(--line);border-radius:8px;background:#fff;padding:10px;font-family:ui-monospace,Menlo,Consolas,monospace}.form-field input:focus,.form-field select:focus,.form-field textarea:focus,.file-picker:focus-within{outline:2px solid rgba(15,118,104,.22);border-color:var(--accent)}.form-actionbar{display:flex;justify-content:flex-end;gap:10px;margin-top:auto;padding-top:8px}.modal-card.wide .form-actionbar{position:sticky;bottom:0;background:var(--card);padding:14px 0 4px;z-index:1}.form-actionbar button{min-width:118px;min-height:38px;border:1px solid var(--line);background:#fff;border-radius:8px;padding:8px 15px;font-weight:650}.form-actionbar button.primary{border:0;background:var(--accent);color:#fff}.form-note{margin:0;color:var(--muted);font-size:13px;line-height:1.35}.file-picker{display:flex!important;align-items:center;justify-content:center;min-height:38px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);padding:8px 10px;cursor:pointer}.file-picker:hover{border-color:#b9aea0;background:#fffdfa}.file-picker input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px;min-height:0;border:0;padding:0;background:transparent}.file-picker span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.choice-group{border:0;margin:0;padding:0}.choice-group legend{font-size:12px;color:var(--muted);margin-bottom:7px}.radio-row{display:inline-flex;gap:0;border:1px solid var(--line);border-radius:9px;background:#fff;padding:3px;flex-wrap:wrap}.radio-choice{position:relative;display:inline-flex;align-items:center;justify-content:center;min-height:34px;border:0;border-radius:7px;background:transparent;padding:7px 13px;color:var(--muted);font-size:13px;font-weight:650;cursor:pointer}.radio-choice:has(input:checked){background:var(--accent);color:#fff;box-shadow:0 1px 3px #0f76682e}.radio-choice input{position:absolute;opacity:0;pointer-events:none;width:1px;height:1px}.checkbox-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px}.check-card{min-height:48px;border:1px solid var(--line);border-radius:8px;background:#fff;padding:8px 10px;display:grid;grid-template-columns:auto minmax(0,1fr);gap:4px 8px;align-items:center;color:var(--ink);font-size:13px}.check-card:has(input:checked){border-color:#0f766859;background:#0f76680f}.check-card input{width:auto}.check-card span,.check-card small{overflow-wrap:anywhere}.check-card small{grid-column:2;color:var(--muted);font-size:11px}@media(max-width:620px){.form-grid{grid-template-columns:1fr}.form-actionbar{justify-content:stretch}.form-actionbar button{flex:1}}.key-manager{display:flex;flex-direction:column;gap:14px;min-height:0;overflow:auto;padding-right:2px}.key-manager-table{min-width:820px}.key-manager-form{border-top:1px solid var(--line);padding-top:14px}.key-manager-form h3{margin:0;font-size:16px}.key-secret{border:1px solid rgba(15,118,104,.25);background:#0f766812;border-radius:10px;padding:10px 12px;display:grid;gap:7px}.key-secret span{color:var(--muted);font-size:13px}.key-secret code{display:block;background:#1d1a17;color:#fff;border-radius:8px;padding:10px;overflow-wrap:anywhere}.status-message{display:flex;align-items:flex-start;gap:10px;border-radius:8px;padding:10px 12px;font-size:13px;line-height:1.35;border:1px solid var(--line);background:#fff}.status-message.success{border-color:#0f76683d;background:#0f766814;color:var(--accent)}.status-message.error{border-color:#b74f1847;background:#b74f1812;color:var(--danger)}.status-message.warning{border-color:#9e6f2247;background:#9e6f2214;color:#7b5312}.status-message.info,.status-message.pending{border-color:#4a5c6f38;background:#4a5c6f0f;color:#334155}.status-icon{position:relative;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;flex:0 0 20px;border-radius:999px;border:1px solid currentColor;margin-top:1px}.status-message.success .status-icon:after{content:"";position:absolute;width:5px;height:9px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg);top:3px;left:7px}.status-message.error .status-icon:before,.status-message.error .status-icon:after{content:"";position:absolute;width:10px;height:2px;background:currentColor;border-radius:2px;top:8px;left:4px}.status-message.error .status-icon:before{transform:rotate(45deg)}.status-message.error .status-icon:after{transform:rotate(-45deg)}.status-message.warning .status-icon:before{content:"";position:absolute;width:2px;height:8px;background:currentColor;top:4px;left:8px;border-radius:2px}.status-message.warning .status-icon:after,.status-message.info .status-icon:after,.status-message.pending .status-icon:after{content:"";position:absolute;width:4px;height:4px;background:currentColor;border-radius:999px;bottom:4px;left:7px}.status-message.info .status-icon:before{content:"";position:absolute;width:2px;height:8px;background:currentColor;top:7px;left:8px;border-radius:2px}.status-message.pending .status-icon:before{content:"";position:absolute;inset:3px;border:2px solid currentColor;border-right-color:transparent;border-radius:999px}.status-copy{display:flex;flex-direction:column;gap:2px;min-width:0}.status-copy strong{font-weight:750;color:inherit}.status-copy span{color:var(--ink);overflow-wrap:anywhere}.copy-button{border:0;background:var(--accent);color:#fff;border-radius:8px;padding:7px 13px;font-weight:650}.copy-button.is-copied{background:var(--accent-2)}.copy-button.is-error{background:var(--danger)}.connect{padding:18px;background:var(--card);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.connect-head{display:flex;justify-content:space-between;gap:12px;margin-bottom:12px}.connect h2{margin:0;font-size:16px}.field-block p{font-weight:700;margin:12px 0 8px}.tabs{display:flex;gap:8px;flex-wrap:wrap}.tabs button.on{background:var(--accent);color:#fff;border-color:var(--accent)}.terminal{margin-top:14px;border-radius:12px;overflow:hidden;border:1px solid #14241f;background:#10201c;color:#eef7f3;display:grid;grid-template-columns:minmax(0,1fr) auto}.term-bar{grid-column:1/-1;display:flex;align-items:center;gap:7px;background:#182723;padding:8px 12px}.term-bar span{width:10px;height:10px;border-radius:50%;background:#d8a332}.terminal pre{margin:0;padding:14px;white-space:pre-wrap;overflow:auto;font:13px/1.6 ui-monospace,Menlo,Consolas,monospace}.terminal .copy-button{margin:12px;align-self:start}@media(max-width:900px){.connect-head{display:block}.terminal{grid-template-columns:1fr}}.overview-hero,.status-panel{background:var(--card);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow)}.overview-hero{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 18px}.overview-hero h2{margin:4px 0 0;font:700 24px Georgia,serif}.overview-hero p{margin:4px 0 0;color:var(--muted)}.scope-tags{display:flex;gap:8px;align-items:center;justify-content:flex-end;flex-wrap:wrap}.overview-panels{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:26px}.overview-panels .dashboard-section{margin-top:0}.status-panel{padding:16px 18px}.gauge-row{display:grid;grid-template-columns:112px minmax(0,1fr);gap:16px;align-items:center}.gauge{width:112px;height:112px;border-radius:50%;display:grid;place-items:center;background:conic-gradient(var(--accent) var(--pct),#e6dfd3 0);position:relative}.gauge:after{content:"";position:absolute;inset:12px;border-radius:50%;background:var(--card)}.gauge b{position:relative;z-index:1;font:700 22px Georgia,serif}.bar-list{display:grid;gap:12px}.bar-row{display:grid;grid-template-columns:70px minmax(0,1fr) 76px;gap:10px;align-items:center}.bar-row b{text-align:right}.overview-table{min-width:680px}.budget{display:flex;flex-direction:column;gap:5px;min-width:140px}@media(max-width:900px){.overview-panels{grid-template-columns:1fr}.overview-hero{align-items:flex-start;flex-direction:column}}
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" type="image/png" href="/__molenkopf/dashboard/favicon.png" />
|
|
7
|
+
<link rel="apple-touch-icon" href="/__molenkopf/dashboard/favicon.png" />
|
|
8
|
+
<title>Molenkopf</title>
|
|
9
|
+
<script type="module" crossorigin src="/__molenkopf/dashboard/assets/index-B_aSPgHx.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/__molenkopf/dashboard/assets/index-D6z2TEL2.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PluginDescriptor } from "../../core/src/plugins/plugin-descriptor.ts";
|
|
2
|
+
|
|
3
|
+
export const descriptor: PluginDescriptor = {
|
|
4
|
+
id: "context-compressor-plugin",
|
|
5
|
+
name: "context-compressor-plugin",
|
|
6
|
+
type: "transformer",
|
|
7
|
+
category: "compression",
|
|
8
|
+
description: "Compresses large safe context and keeps retrievable originals locally.",
|
|
9
|
+
traffic: { reads: ["redacted-body", "audit"], mutates: ["transform"] },
|
|
10
|
+
permissions: ["body:read", "body:write", "audit:read", "audit:write"],
|
|
11
|
+
hooks: ["request:body:rewrite", "audit:manifest", "workspace:local-page"],
|
|
12
|
+
toggle: { defaultEnabled: false, canDisable: true },
|
|
13
|
+
modulePath: "plugin.ts",
|
|
14
|
+
workspace: {
|
|
15
|
+
pagePath: "/__molenkopf/plugins/context-compressor-plugin/page",
|
|
16
|
+
dataPath: "/__molenkopf/plugins/context-compressor-plugin/data",
|
|
17
|
+
dataScopes: ["metrics", "audit-summary", "requests"]
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Context compression</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{--bg:#f4f1ea;--surface:#fbf9f4;--line:#e3ddd1;--ink:#1c1a17;--muted:#746d63;--accent:#087a68;--soft:#d9f3ed;--warn:#9b4a00;--font-sans:"Segoe UI",Arial,sans-serif;--font-serif:Georgia,"Times New Roman",serif}
|
|
9
|
+
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--font-sans);line-height:1.45}
|
|
10
|
+
.wrap{max-width:1440px;margin:0 auto;padding:34px 28px 48px}
|
|
11
|
+
header{display:flex;justify-content:space-between;gap:18px;align-items:flex-start;margin-bottom:22px}
|
|
12
|
+
h1{font-family:var(--font-serif);font-weight:650;font-size:30px;margin:0 0 4px}
|
|
13
|
+
h2{font-size:15px;margin:0 0 14px}.sub,.muted{color:var(--muted);font-size:13px}.state{border:1px solid var(--line);border-radius:8px;background:var(--surface);padding:10px 12px;min-width:260px}
|
|
14
|
+
.statusbox{display:grid;gap:8px}.actions{display:flex;justify-content:flex-end}button{border:1px solid var(--line);border-radius:8px;background:#fffdf8;color:var(--ink);padding:7px 10px;font-weight:700;cursor:pointer}button:disabled{opacity:.55;cursor:default}
|
|
15
|
+
.badge{display:inline-flex;align-items:center;border-radius:999px;background:var(--soft);color:#075f54;font-size:11px;font-weight:700;padding:3px 8px;margin-left:6px}
|
|
16
|
+
.grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.two{grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr)}.panel,.metric{min-width:0;background:var(--surface);border:1px solid var(--line);border-radius:10px;box-shadow:0 8px 22px rgba(28,26,23,.06)}
|
|
17
|
+
.metric{padding:16px}.metric .k,.label{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em}.metric .v{font-family:var(--font-serif);font-size:31px;font-weight:650;margin-top:7px}
|
|
18
|
+
.panel{padding:18px;margin-top:14px}.funnel{display:grid;grid-template-columns:1fr auto 1fr auto 1fr;gap:10px;align-items:center}.step{border:1px solid var(--line);border-radius:8px;padding:13px;background:#fffdf8}.arrow{color:var(--muted);font-weight:700}
|
|
19
|
+
.bar,.track{height:8px;background:#ebe5d9;border-radius:999px;overflow:hidden;margin-top:8px}.bar span,.track span{display:block;height:100%;background:var(--accent);border-radius:999px;animation:fill .45s ease-out}
|
|
20
|
+
.chart-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}.chart-row{display:grid;grid-template-columns:110px minmax(0,1fr) 88px;gap:10px;align-items:center;margin:10px 0}.chart-row b{font-size:13px}.chart-value{text-align:right;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px}
|
|
21
|
+
.stackbar{height:18px;border-radius:999px;background:#ebe5d9;overflow:hidden;display:flex}.stackbar span{display:block;height:100%;animation:fill .45s ease-out}.sent{background:#087a68}.saved{background:#e0ad45}.lost{background:#d7cfc1}
|
|
22
|
+
.break-list{display:grid;gap:9px}.break-card{border:1px solid var(--line);border-radius:8px;background:#fffdf8;padding:11px}.break-top{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.break-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:9px}.break-stats div{font-size:12px}.break-stats b{display:block;font-size:13px}.mini{height:5px;background:#ebe5d9;border-radius:999px;overflow:hidden;margin-top:9px}.mini span{display:block;height:100%;background:var(--accent);border-radius:999px;animation:fill .45s ease-out}
|
|
23
|
+
.timeline{width:100%;height:190px}.timeline text{font:11px ui-monospace,SFMono-Regular,Consolas,monospace;fill:var(--muted)}.timeline rect{rx:3;animation:rise .45s ease-out}.timeline .orig{fill:#d7cfc1}.timeline .fwd{fill:#087a68}.timeline .save{fill:#e0ad45}
|
|
24
|
+
table{width:100%;table-layout:fixed;border-collapse:collapse;font-size:13px}th,td{text-align:left;border-bottom:1px solid var(--line);padding:9px 8px;vertical-align:top;overflow-wrap:anywhere}th{color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
|
|
25
|
+
.right{text-align:right}.row-title{font-weight:700}.recent{display:grid;gap:9px}.request{display:grid;grid-template-columns:1.5fr 1fr 1fr auto;gap:10px;border:1px solid var(--line);border-radius:8px;background:#fffdf8;padding:10px}
|
|
26
|
+
.empty{border:1px dashed var(--line);border-radius:10px;padding:18px;color:var(--muted);background:#fffdf8}.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;overflow-wrap:anywhere}.status-error{color:var(--warn);font-weight:700}
|
|
27
|
+
@keyframes fill{from{width:0}to{width:var(--w,100%)}}@keyframes rise{from{transform:scaleY(.05);transform-origin:bottom}to{transform:scaleY(1);transform-origin:bottom}}
|
|
28
|
+
html[data-loaded="1"] .bar span,html[data-loaded="1"] .track span,html[data-loaded="1"] .stackbar span,html[data-loaded="1"] .mini span,html[data-loaded="1"] .timeline rect{animation:none}
|
|
29
|
+
@media(max-width:900px){.grid,.two,.funnel,.chart-grid,.break-stats{grid-template-columns:1fr}header{display:block}.state{margin-top:12px}.arrow{display:none}.request{grid-template-columns:1fr}.chart-row{grid-template-columns:1fr}
|
|
30
|
+
table,thead,tbody,tr,th,td{display:block}thead,th{display:none}tr{border:1px solid var(--line);border-radius:8px;background:#fffdf8;padding:8px;margin-bottom:9px}
|
|
31
|
+
td{border:0;padding:4px 0}.right{text-align:left}td[data-label]::before{content:attr(data-label);display:block;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em}
|
|
32
|
+
}
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<main class="wrap">
|
|
37
|
+
<header>
|
|
38
|
+
<div>
|
|
39
|
+
<h1>Context compression</h1>
|
|
40
|
+
<div class="sub">Real compression accounting for transferred payloads. Full prompts and responses stay out of this page.</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="statusbox"><div class="state" id="state">Loading workspace...</div><div class="actions"><button id="refresh" type="button">Refresh snapshot</button></div></div>
|
|
43
|
+
</header>
|
|
44
|
+
<section class="grid" id="metrics"></section>
|
|
45
|
+
<section class="chart-grid">
|
|
46
|
+
<div class="panel"><h2>Payload reduction chart</h2><div id="reduction"></div></div>
|
|
47
|
+
<div class="panel"><h2>Recent activity chart</h2><div id="timeline"></div></div>
|
|
48
|
+
</section>
|
|
49
|
+
<section class="panel" id="funnel"></section>
|
|
50
|
+
<section class="grid two">
|
|
51
|
+
<div class="panel"><h2>Projects / API keys</h2><div id="accounts"></div></div>
|
|
52
|
+
<div class="panel"><h2>Provider savings</h2><div id="providers"></div></div>
|
|
53
|
+
</section>
|
|
54
|
+
<section class="panel"><h2>Endpoint pressure</h2><div id="endpoints"></div></section>
|
|
55
|
+
<section class="panel"><h2>Recent grouped activity</h2><div id="recent" class="recent"></div></section>
|
|
56
|
+
</main>
|
|
57
|
+
<script>
|
|
58
|
+
const DATA_URL="/__molenkopf/plugins/context-compressor-plugin/data";
|
|
59
|
+
const METRICS=[
|
|
60
|
+
["Requests","requests"],["Observed payload","originalTokens"],["Forwarded payload","forwardedTokens"],
|
|
61
|
+
["Transform delta","payloadDelta","delta"],["Compression saved","savedTokens"],["Compressed items","compressedItems"],["Compression %","savedPercent","pct"],
|
|
62
|
+
["Input tokens","upstreamInputTokens"],["Output tokens","upstreamOutputTokens"],["Redacted secrets","redactedSecrets"]
|
|
63
|
+
];
|
|
64
|
+
const TABLE_COLS=[["Requests","requests"],["Compression saved","savedTokens"],["Compression %","savedPercent","pct"],["Original","originalTokens"],["Forwarded","forwardedTokens"]];
|
|
65
|
+
const $=(id)=>document.getElementById(id);
|
|
66
|
+
const n=(v)=>Number(v||0).toLocaleString("en-US");
|
|
67
|
+
const pct=(v)=>`${Number(v||0).toLocaleString("en-US")}%`;
|
|
68
|
+
const esc=(v)=>String(v??"").replace(/[&<>"]/g,(c)=>({"&":"&","<":"<",">":">",'"':"""}[c]));
|
|
69
|
+
const forwarded=(row)=>row.forwardedTokens??0;
|
|
70
|
+
const delta=(row)=>Math.max(0,(row.originalTokens||0)-forwarded(row));
|
|
71
|
+
const value=(row,key,kind)=>kind==="pct"?pct(row[key]):kind==="delta"?n(delta(row)):key==="upstream"?n((row.upstreamInputTokens||0)+(row.upstreamOutputTokens||0)):n(row[key]);
|
|
72
|
+
|
|
73
|
+
function stateText(plugin,m){
|
|
74
|
+
const enabled=plugin?.enabled!==false;
|
|
75
|
+
if(!m.requests) return `No payloads observed yet <span class="badge">${enabled?"enabled":"off"}</span><div class="muted">Send Claude, Codex, or API traffic through Molenkopf to populate this workspace.</div>`;
|
|
76
|
+
if(!enabled) return `Compression disabled <span class="badge">off</span><div class="muted">Existing rows are historical audit data; new requests pass through unchanged.</div>`;
|
|
77
|
+
if(m.compressedItems>0&&m.savedTokens>0) return `Compression active <span class="badge">saving</span><div class="muted">${n(m.savedTokens)} tokens saved across ${n(m.compressedItems)} compressed ${m.compressedItems===1?"item":"items"}.</div>`;
|
|
78
|
+
const d=delta(m);
|
|
79
|
+
return `Compressor enabled <span class="badge">watching</span><div class="muted">${d?n(d)+" tokens changed by other safe transforms; ":""}No compressible structured block was replaced yet. Prose, code, markdown and diffs stay untouched.</div>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function renderMetrics(m){
|
|
83
|
+
$("metrics").innerHTML=METRICS.map(([label,key,kind])=>`<div class="metric"><div class="k">${label}</div><div class="v">${value(m,key,kind)}</div></div>`).join("");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderFunnel(m){
|
|
87
|
+
const saved=Math.max(0,m.savedTokens||0), original=Math.max(0,m.originalTokens||0), sent=Math.max(0,forwarded(m));
|
|
88
|
+
const width=original?Math.max(2,Math.min(100,Math.round((saved/original)*100))):0;
|
|
89
|
+
$("funnel").innerHTML=`<h2>Token funnel</h2><div class="funnel">
|
|
90
|
+
${step("Before",original,"Estimated tokens observed before compression")}
|
|
91
|
+
<div class="arrow">-></div>${step("Forwarded",sent,"Estimated tokens after safe transforms")}
|
|
92
|
+
<div class="arrow">-></div>${step("Compression saved",saved,`${pct(m.savedPercent)} compression saved`)}
|
|
93
|
+
</div><div class="bar"><span style="--w:${width}%;width:${width}%"></span></div>
|
|
94
|
+
<div class="muted">Compression only targets large structured content such as logs, JSON, shell output and stacktraces.</div>`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderReduction(m){
|
|
98
|
+
const original=Math.max(0,m.originalTokens||0), sent=Math.max(0,forwarded(m)), saved=Math.max(0,m.savedTokens||0);
|
|
99
|
+
const changed=delta(m), max=Math.max(original,sent,changed,saved,1), sentPct=Math.min(100,Math.round((sent/max)*100)), changedPct=Math.min(100,Math.round((changed/max)*100)), savePct=Math.min(100,Math.round((saved/max)*100));
|
|
100
|
+
$("reduction").innerHTML=[
|
|
101
|
+
chartRow("Observed",original,100,"sent"),
|
|
102
|
+
chartRow("Forwarded",sent,sentPct,"sent"),
|
|
103
|
+
chartRow("Payload delta",changed,changedPct,"lost"),
|
|
104
|
+
chartRow("Compression saved",saved,savePct,"saved")
|
|
105
|
+
].join("")+`<div class="stackbar" title="Forwarded, transform delta, compression saved"><span class="sent" style="--w:${original?Math.max(1,Math.round((sent/original)*100)):0}%;width:${original?Math.max(1,Math.round((sent/original)*100)):0}%"></span><span class="lost" style="--w:${original?Math.round((Math.max(0,changed-saved)/original)*100):0}%;width:${original?Math.round((Math.max(0,changed-saved)/original)*100):0}%"></span><span class="saved" style="--w:${original?Math.round((saved/original)*100):0}%;width:${original?Math.round((saved/original)*100):0}%"></span></div><div class="muted">Green is forwarded context. Amber is confirmed context-compression savings; gray is other safe transform delta.</div>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function chartRow(label,count,width,cls){
|
|
109
|
+
return `<div class="chart-row"><b>${label}</b><div class="track"><span class="${cls}" style="--w:${Math.max(0,width)}%;width:${Math.max(0,width)}%"></span></div><div class="chart-value">${n(count)}</div></div>`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function renderTimeline(items){
|
|
113
|
+
const rows=(items||[]).slice(-14), max=Math.max(...rows.map((r)=>r.estimatedOriginalTokens??r.originalTokens??0),1);
|
|
114
|
+
if(!rows.length){$("timeline").innerHTML=`<div class="empty">No request timeline yet.</div>`;return}
|
|
115
|
+
const w=560,h=170,gap=7,bw=Math.max(14,Math.floor((w-gap*(rows.length-1))/rows.length));
|
|
116
|
+
const bars=rows.map((r,i)=>{
|
|
117
|
+
const original=r.estimatedOriginalTokens??r.originalTokens??0, compressed=r.estimatedCompressedTokens??forwarded(r), saved=r.estimatedSavedTokens??r.savedTokens??0;
|
|
118
|
+
const x=i*(bw+gap), orig=Math.max(2,Math.round((original/max)*130)), fwd=Math.max(2,Math.round((compressed/max)*130)), save=Math.max(0,Math.round((saved/max)*130));
|
|
119
|
+
return `<rect class="orig" x="${x}" y="${h-orig}" width="${bw}" height="${orig}"></rect><rect class="fwd" x="${x+3}" y="${h-fwd}" width="${Math.max(3,bw-6)}" height="${fwd}"></rect>${save?`<rect class="save" x="${x+6}" y="${h-fwd-save}" width="${Math.max(2,bw-12)}" height="${save}"></rect>`:""}`;
|
|
120
|
+
}).join("");
|
|
121
|
+
$("timeline").innerHTML=`<svg class="timeline" viewBox="0 0 ${w} 190" role="img" aria-label="Recent activity token chart">${bars}<text x="0" y="188">last ${rows.length} activity groups - gray observed - green forwarded - amber saved</text></svg>`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function step(label,count,caption){return `<div class="step"><div class="label">${label}</div><div class="metric v" style="box-shadow:none;border:0;padding:0;background:transparent">${n(count)}</div><div class="muted">${esc(caption)}</div></div>`}
|
|
125
|
+
|
|
126
|
+
function renderTable(id,rows,emptyLabel,withSource=false){
|
|
127
|
+
if(!rows?.length){$(id).innerHTML=`<div class="empty">${emptyLabel}</div>`;return}
|
|
128
|
+
const head=`<thead><tr><th>Name</th>${withSource?"<th>Source</th>":""}${TABLE_COLS.map(([l])=>`<th class="right">${l}</th>`).join("")}<th>Latest</th></tr></thead>`;
|
|
129
|
+
const body=rows.slice(0,8).map((row)=>`<tr><td data-label="Name"><div class="row-title">${esc(row.label)}</div><div class="mono">${esc(row.id)}</div></td>${withSource?`<td data-label="Source">${esc(row.source)}</td>`:""}${TABLE_COLS.map(([label,key,kind])=>`<td data-label="${label}" class="right">${value(row,key,kind)}</td>`).join("")}<td data-label="Latest">${esc(shortTime(row.latestAt))}</td></tr>`).join("");
|
|
130
|
+
$(id).innerHTML=`<table>${head}<tbody>${body}</tbody></table>`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderBreakdown(id,rows,emptyLabel,withSource=false){
|
|
134
|
+
if(!rows?.length){$(id).innerHTML=`<div class="empty">${emptyLabel}</div>`;return}
|
|
135
|
+
$(""+id).innerHTML=`<div class="break-list">`+rows.slice(0,8).map((row)=>{
|
|
136
|
+
const width=Math.max(2,Math.min(100,Math.round(row.savedPercent||0)));
|
|
137
|
+
const third=row.inputTokens!=null?["Input",n(row.inputTokens)]:["Observed",n(row.originalTokens)];
|
|
138
|
+
const fourth=row.outputTokens!=null?["Output",n(row.outputTokens)]:[withSource?"Source":"Forwarded",withSource?esc(row.source):n(forwarded(row))];
|
|
139
|
+
return `<div class="break-card"><div class="break-top"><div><div class="row-title">${esc(row.label)}</div><div class="mono">${esc(row.id)}</div></div><div class="muted">${esc(shortTime(row.latestAt))}</div></div>`+
|
|
140
|
+
`<div class="mini"><span style="--w:${width}%;width:${width}%"></span></div><div class="break-stats">`+
|
|
141
|
+
`<div><span class="label">Requests</span><b>${n(row.requests)}</b></div><div><span class="label">Compression saved</span><b>${n(row.savedTokens)}</b></div>`+
|
|
142
|
+
`<div><span class="label">${third[0]}</span><b>${third[1]}</b></div><div><span class="label">${fourth[0]}</span><b>${fourth[1]}</b></div>`+
|
|
143
|
+
`</div></div>`;
|
|
144
|
+
}).join("")+`</div>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renderRecent(groups){
|
|
148
|
+
if(!groups?.length){$("recent").innerHTML=`<div class="empty">No request activity recorded yet.</div>`;return}
|
|
149
|
+
$("recent").innerHTML=groups.map((item)=>{
|
|
150
|
+
const tokens=`${n(item.originalTokens)} -> ${n(forwarded(item))} compression saved ${n(item.savedTokens)}`;
|
|
151
|
+
const status=item.errors?`<span class="status-error">${esc(item.status)}</span>`:esc(item.status||"unknown");
|
|
152
|
+
return `<div class="request"><div><div class="row-title">${esc(item.clientLabel)}</div><div class="mono">${esc(item.endpoint)}</div><div class="muted">${item.project?`project ${esc(item.project)}`:"no project"}${item.keyId?` - key ${esc(item.keyId)}`:""}</div></div><div><div class="label">Provider</div><div class="mono">${esc(item.providerId)}</div></div><div><div class="label">Activity</div><div>${n(item.requests)} requests</div><div class="muted">${tokens}; ${n(item.compressedItems)} items, ${n(item.retrievalRefs)} retrieval refs</div></div><div><div class="label">Status</div><div>${status}</div><div class="muted">${esc(shortTime(item.latestAt))}</div></div></div>`;
|
|
153
|
+
}).join("");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function shortTime(value){
|
|
157
|
+
if(!value) return "n/a";
|
|
158
|
+
const date=new Date(value);
|
|
159
|
+
return Number.isNaN(date.getTime()) ? value : date.toLocaleString();
|
|
160
|
+
}
|
|
161
|
+
async function loadData(url){
|
|
162
|
+
try{const r=await fetch(url);if(!r.ok)return{loadError:`HTTP ${r.status}`};return await r.json()}catch(error){return{loadError:String(error.message||error||"request failed")}}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function load(){
|
|
166
|
+
const button=$("refresh");
|
|
167
|
+
if(button) button.disabled=true;
|
|
168
|
+
try{
|
|
169
|
+
const data=await loadData(DATA_URL);
|
|
170
|
+
if(data.loadError){$("state").innerHTML=`Data unavailable <span class="badge">error</span><div class="muted">${esc(data.loadError)}</div>`;["metrics","reduction","timeline","funnel","accounts","providers","endpoints","recent"].forEach((id)=>$(id).innerHTML=`<div class="empty status-error">Plugin data unavailable. Refresh after the proxy returns a valid data response.</div>`);return}
|
|
171
|
+
const m=data.metrics||{}, groups=data.requestGroups||[];
|
|
172
|
+
$("state").innerHTML=stateText(data.plugin,m);
|
|
173
|
+
renderMetrics(m);
|
|
174
|
+
renderReduction(m);
|
|
175
|
+
renderTimeline(groups);
|
|
176
|
+
renderFunnel(m);
|
|
177
|
+
const projectRows=m.projects&&m.projects.length?m.projects:m.buckets;
|
|
178
|
+
renderBreakdown("accounts",projectRows,"No project or API-key buckets observed yet.",!(m.projects&&m.projects.length));
|
|
179
|
+
renderBreakdown("providers",m.providers,"No provider traffic observed yet.");
|
|
180
|
+
renderTable("endpoints",m.endpoints,"No endpoint traffic observed yet.");
|
|
181
|
+
renderRecent(groups);
|
|
182
|
+
requestAnimationFrame(()=>{document.documentElement.dataset.loaded="1"});
|
|
183
|
+
}finally{
|
|
184
|
+
if(button) button.disabled=false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
$("refresh").addEventListener("click",load);
|
|
188
|
+
load();
|
|
189
|
+
</script>
|
|
190
|
+
</body>
|
|
191
|
+
</html>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { MolenkopfPluginModule, PluginRuntimeContext } from "../../core/src/plugins/plugin-api.ts";
|
|
2
|
+
import { compressJsonBody } from "../../core/src/pipeline/openai-request-rewriter.ts";
|
|
3
|
+
import { summarizeRecentActivity } from "../../core/src/manifest/audit-activity.ts";
|
|
4
|
+
import { summarizeAudit } from "../../core/src/manifest/audit-summary.ts";
|
|
5
|
+
import type { RetrievalStore } from "../../core/src/store/retrieval-store.ts";
|
|
6
|
+
import { projectMetrics } from "../shared/audit-projects.ts";
|
|
7
|
+
export { descriptor } from "./descriptor.ts";
|
|
8
|
+
|
|
9
|
+
export const plugin: MolenkopfPluginModule = {
|
|
10
|
+
async onRequest(ctx, runtime) {
|
|
11
|
+
const store = retrievalStore(runtime);
|
|
12
|
+
if (!store) return { notes: ["context_compressor_storage_unavailable"] };
|
|
13
|
+
const result = await compressJsonBody(ctx.body, store, ctx.requestId, true);
|
|
14
|
+
return {
|
|
15
|
+
body: result.body,
|
|
16
|
+
compressedItems: result.compressedItems,
|
|
17
|
+
retrievalIds: result.retrievalIds,
|
|
18
|
+
compressorsUsed: result.compressorsUsed,
|
|
19
|
+
savedTokens: result.savedTokens,
|
|
20
|
+
redactedSecrets: result.redactedSecrets
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
getData(ctx) {
|
|
24
|
+
const summary = summarizeAudit(ctx.manifests);
|
|
25
|
+
const projects = projectMetrics(ctx.manifests);
|
|
26
|
+
return {
|
|
27
|
+
plugin: ctx.plugin,
|
|
28
|
+
scopes: ctx.scopes,
|
|
29
|
+
metrics: { ...summary, projects },
|
|
30
|
+
latest: ctx.manifests.at(-1),
|
|
31
|
+
requests: ctx.manifests.slice(-25),
|
|
32
|
+
requestGroups: summarizeRecentActivity(ctx.manifests)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function retrievalStore(runtime: PluginRuntimeContext): RetrievalStore | undefined {
|
|
38
|
+
const candidate = runtime.storage as Partial<RetrievalStore> | undefined;
|
|
39
|
+
return candidate && typeof candidate.save === "function" ? candidate as RetrievalStore : undefined;
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PluginDescriptor } from "../../core/src/plugins/plugin-descriptor.ts";
|
|
2
|
+
|
|
3
|
+
export const descriptor: PluginDescriptor = {
|
|
4
|
+
id: "obsidian-graph-plugin",
|
|
5
|
+
name: "obsidian-graph-plugin",
|
|
6
|
+
type: "observer",
|
|
7
|
+
category: "visualization",
|
|
8
|
+
description: "Local workspace for memory graph rendering from compressed text decisions.",
|
|
9
|
+
traffic: { reads: ["audit"], mutates: ["none"] },
|
|
10
|
+
permissions: ["audit:read"],
|
|
11
|
+
hooks: ["workspace:local-page"],
|
|
12
|
+
toggle: { defaultEnabled: true, canDisable: true },
|
|
13
|
+
modulePath: "plugin.ts",
|
|
14
|
+
workspace: {
|
|
15
|
+
pagePath: "/__molenkopf/plugins/obsidian-graph-plugin/page",
|
|
16
|
+
dataPath: "/__molenkopf/plugins/obsidian-graph-plugin/data",
|
|
17
|
+
dataScopes: ["metrics", "memory-graph"]
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Memory graph</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root{--bg:#f4f1ea;--ink:#1c1a17;--muted:#7c756a;--line:#e3ddd1;--card:#fbf9f4;--accent:#0f6d5e;--accent-soft:#d7ebe5;--warn:#b4520a;--font-sans:"Segoe UI",Arial,sans-serif;--font-serif:Georgia,"Times New Roman",serif}
|
|
9
|
+
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--font-sans);line-height:1.5}
|
|
10
|
+
.wrap{width:min(92vw,1540px);margin:0 auto;padding:34px 18px 64px}
|
|
11
|
+
.top{display:flex;justify-content:space-between;gap:18px;align-items:flex-start;margin-bottom:18px}
|
|
12
|
+
h1{font-family:var(--font-serif);font-weight:600;font-size:30px;margin:0 0 4px}.sub{color:var(--muted);font-size:14px;max-width:680px}
|
|
13
|
+
button{border:1px solid var(--line);background:#fff;border-radius:8px;padding:7px 12px;font:inherit;font-size:13px;cursor:pointer}
|
|
14
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;margin:14px 0}
|
|
15
|
+
.card,.graph,.list{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px}
|
|
16
|
+
.k{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}.v{font-family:var(--font-serif);font-size:28px;font-weight:600;margin-top:6px}
|
|
17
|
+
.main{display:grid;grid-template-columns:minmax(0,1.35fr) minmax(320px,.65fr);gap:14px;align-items:start}.graph{min-height:520px}
|
|
18
|
+
svg{width:100%;height:520px}.empty{color:var(--muted);border:1px dashed var(--line);border-radius:12px;padding:42px;text-align:center}
|
|
19
|
+
text{font-size:11px;fill:var(--ink)}line{stroke:#cbd5c5}circle{fill:var(--accent)}.symbol circle{fill:var(--warn)}.error circle{fill:#9b2226}
|
|
20
|
+
.row{border-top:1px solid var(--line);padding:12px 0}.row:first-child{border-top:0}.name{font-weight:700;overflow-wrap:anywhere}
|
|
21
|
+
.meta{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:8px;font-size:12px}.meta b{display:block;font-size:14px;color:var(--ink)}
|
|
22
|
+
.bar{height:6px;background:#ece6da;border-radius:999px;margin-top:10px;overflow:hidden}.bar span{display:block;height:100%;background:var(--accent);border-radius:999px}
|
|
23
|
+
@media(max-width:980px){.main{grid-template-columns:1fr}.top{display:block}.top button{margin-top:10px}.meta{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<div class="wrap">
|
|
28
|
+
<div class="top"><div><h1>Memory graph</h1><div class="sub">Concepts, files, symbols, projects, and token flow derived from safe request metadata and transferred text. Full prompts and responses stay out of this page.</div></div><button id="refresh">Refresh snapshot</button></div>
|
|
29
|
+
<div class="grid" id="metrics"></div>
|
|
30
|
+
<div class="main"><div class="graph" id="graph"></div><div class="list"><div class="k">Projects</div><div id="projects"></div></div></div>
|
|
31
|
+
</div>
|
|
32
|
+
<script>
|
|
33
|
+
const fmt = (v) => Number(v || 0).toLocaleString("en-US");
|
|
34
|
+
const esc = (v) => String(v ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
35
|
+
document.getElementById("refresh").addEventListener("click", load);
|
|
36
|
+
load();
|
|
37
|
+
async function loadData() {
|
|
38
|
+
try { const r = await fetch("/__molenkopf/plugins/obsidian-graph-plugin/data"); if (!r.ok) return { loadError: "HTTP " + r.status }; return await r.json(); } catch (error) { return { loadError: String(error.message || error || "request failed") }; }
|
|
39
|
+
}
|
|
40
|
+
async function load() {
|
|
41
|
+
const d = await loadData();
|
|
42
|
+
if (d.loadError) { document.getElementById("metrics").innerHTML = ""; document.getElementById("projects").innerHTML = '<div class="empty">Plugin data unavailable. Refresh after the proxy returns a valid data response.</div>'; document.getElementById("graph").innerHTML = '<div class="empty">Data unavailable: ' + esc(d.loadError) + '</div>'; return; }
|
|
43
|
+
const m = d.metrics || {}, g = d.memoryGraph || { nodes: [], edges: [] };
|
|
44
|
+
renderMetrics(m);
|
|
45
|
+
renderProjects(m.projects || []);
|
|
46
|
+
document.getElementById("graph").innerHTML = g.nodes.length ? renderGraph(g) : '<div class="empty">No agent payloads observed yet.</div>';
|
|
47
|
+
}
|
|
48
|
+
function renderMetrics(m) {
|
|
49
|
+
const cards = [["Concepts", m.concepts], ["Links", m.links], ["Requests", m.requests], ["Input tokens", m.inputTokens], ["Output tokens", m.outputTokens], ["Saved tokens", m.savedTokens], ["Projects", (m.projects || []).length], ["Total tokens", m.totalTokens]];
|
|
50
|
+
document.getElementById("metrics").innerHTML = cards.map(([k, v]) => '<div class="card"><div class="k">' + k + '</div><div class="v">' + fmt(v) + '</div></div>').join("");
|
|
51
|
+
}
|
|
52
|
+
function renderProjects(items) {
|
|
53
|
+
const total = Math.max(1, ...items.map((p) => Number(p.inputTokens || 0) + Number(p.outputTokens || 0)));
|
|
54
|
+
document.getElementById("projects").innerHTML = items.length ? items.map((p) => {
|
|
55
|
+
const tokens = Number(p.inputTokens || 0) + Number(p.outputTokens || 0), pct = Math.max(2, Math.round(tokens / total * 100));
|
|
56
|
+
return '<div class="row"><div class="name">' + esc(p.label) + '</div><div class="bar"><span style="width:' + pct + '%"></span></div><div class="meta"><span><b>' + fmt(p.requests) + '</b>requests</span><span><b>' + fmt(p.inputTokens) + '</b>input</span><span><b>' + fmt(p.outputTokens) + '</b>output</span><span><b>' + fmt(p.clients) + '</b>clients</span></div></div>';
|
|
57
|
+
}).join("") : '<div class="empty">No project-bound traffic yet. Create or select an API key with a project.</div>';
|
|
58
|
+
}
|
|
59
|
+
function renderGraph(g) {
|
|
60
|
+
const W = 1000, H = 520, cx = W / 2, cy = H / 2, R = Math.min(cx, cy) - 70, pos = {};
|
|
61
|
+
g.nodes.forEach((node, i) => { const a = (i / g.nodes.length) * Math.PI * 2; pos[node.id] = { x: cx + R * Math.cos(a), y: cy + R * Math.sin(a) }; });
|
|
62
|
+
const lines = g.edges.filter((e) => pos[e.from] && pos[e.to]).map((e) => '<line x1="' + pos[e.from].x + '" y1="' + pos[e.from].y + '" x2="' + pos[e.to].x + '" y2="' + pos[e.to].y + '" stroke-width="' + Math.min(4, 1 + (e.count || 1)) + '"/>').join("");
|
|
63
|
+
const nodes = g.nodes.map((node) => { const p = pos[node.id], r = Math.min(16, 5 + (node.count || 1)); return '<g class="' + esc(node.kind || "") + '"><circle cx="' + p.x + '" cy="' + p.y + '" r="' + r + '"/><text x="' + (p.x + r + 4) + '" y="' + (p.y + 4) + '">' + esc(node.label) + '</text></g>'; }).join("");
|
|
64
|
+
return '<svg viewBox="0 0 ' + W + ' ' + H + '">' + lines + nodes + '</svg>';
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
</body>
|
|
68
|
+
</html>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { MolenkopfPluginModule } from "../../core/src/plugins/plugin-api.ts";
|
|
2
|
+
import { summarizeAudit } from "../../core/src/manifest/audit-summary.ts";
|
|
3
|
+
import { projectMetrics } from "../shared/audit-projects.ts";
|
|
4
|
+
export { descriptor } from "./descriptor.ts";
|
|
5
|
+
|
|
6
|
+
export const plugin: MolenkopfPluginModule = {
|
|
7
|
+
getData(ctx) {
|
|
8
|
+
const summary = summarizeAudit(ctx.manifests);
|
|
9
|
+
const projects = projectMetrics(ctx.manifests);
|
|
10
|
+
const graph = ctx.memoryGraph ?? { nodes: [], edges: [] };
|
|
11
|
+
return {
|
|
12
|
+
plugin: ctx.plugin,
|
|
13
|
+
scopes: ctx.scopes,
|
|
14
|
+
metrics: {
|
|
15
|
+
requests: summary.requests,
|
|
16
|
+
concepts: graph.nodes.length,
|
|
17
|
+
links: graph.edges.length,
|
|
18
|
+
savedTokens: summary.savedTokens,
|
|
19
|
+
inputTokens: summary.upstreamInputTokens,
|
|
20
|
+
outputTokens: summary.upstreamOutputTokens,
|
|
21
|
+
totalTokens: summary.upstreamInputTokens + summary.upstreamOutputTokens,
|
|
22
|
+
projects
|
|
23
|
+
},
|
|
24
|
+
memoryGraph: graph
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AuditManifest } from "../../core/src/manifest/audit-store.ts";
|
|
2
|
+
import { confirmedSavedTokens } from "../../core/src/manifest/audit-metrics.ts";
|
|
3
|
+
|
|
4
|
+
export function projectMetrics(manifests: AuditManifest[]) {
|
|
5
|
+
const map = new Map<string, { id: string; label: string; requests: number; originalTokens: number; forwardedTokens: number; inputTokens: number; outputTokens: number; savedTokens: number; clients: Set<string>; latestAt?: string }>();
|
|
6
|
+
for (const manifest of manifests) {
|
|
7
|
+
const id = manifest.client?.project?.trim() || "unassigned";
|
|
8
|
+
const item = map.get(id) ?? { id, label: id === "unassigned" ? "No project" : id, requests: 0, originalTokens: 0, forwardedTokens: 0, inputTokens: 0, outputTokens: 0, savedTokens: 0, clients: new Set(), latestAt: undefined };
|
|
9
|
+
item.requests++;
|
|
10
|
+
item.originalTokens += manifest.estimatedOriginalTokens;
|
|
11
|
+
item.forwardedTokens += manifest.estimatedCompressedTokens;
|
|
12
|
+
item.inputTokens += manifest.upstreamInputTokens ?? 0;
|
|
13
|
+
item.outputTokens += manifest.upstreamOutputTokens ?? 0;
|
|
14
|
+
item.savedTokens += confirmedSavedTokens(manifest);
|
|
15
|
+
item.clients.add(manifest.client?.id ?? "unattributed");
|
|
16
|
+
if (!item.latestAt || manifest.timestamp > item.latestAt) item.latestAt = manifest.timestamp;
|
|
17
|
+
map.set(id, item);
|
|
18
|
+
}
|
|
19
|
+
return [...map.values()].map((item) => ({
|
|
20
|
+
id: item.id,
|
|
21
|
+
label: item.label,
|
|
22
|
+
requests: item.requests,
|
|
23
|
+
originalTokens: item.originalTokens,
|
|
24
|
+
forwardedTokens: item.forwardedTokens,
|
|
25
|
+
inputTokens: item.inputTokens,
|
|
26
|
+
outputTokens: item.outputTokens,
|
|
27
|
+
savedTokens: item.savedTokens,
|
|
28
|
+
savedPercent: item.originalTokens > 0 && item.savedTokens > 0 ? Math.round((item.savedTokens / item.originalTokens) * 10000) / 100 : 0,
|
|
29
|
+
clients: item.clients.size,
|
|
30
|
+
latestAt: item.latestAt
|
|
31
|
+
})).sort((a, b) => (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens) || b.requests - a.requests || a.id.localeCompare(b.id));
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type CliArgs = { command?: string; values: string[]; flags: Map<string, string | boolean> };
|
|
2
|
+
|
|
3
|
+
export function parseArgs(input: string[]): CliArgs {
|
|
4
|
+
const [command, ...rest] = input;
|
|
5
|
+
const flags = new Map<string, string | boolean>();
|
|
6
|
+
const values: string[] = [];
|
|
7
|
+
let positionalOnly = false;
|
|
8
|
+
for (let i = 0; i < rest.length; i++) {
|
|
9
|
+
const item = rest[i];
|
|
10
|
+
if (positionalOnly || !item.startsWith("--") || item === "-") {
|
|
11
|
+
values.push(item);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (item === "--") {
|
|
15
|
+
positionalOnly = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const inline = item.indexOf("=");
|
|
19
|
+
if (inline > 2) {
|
|
20
|
+
flags.set(item.slice(2, inline), item.slice(inline + 1));
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const key = item.slice(2);
|
|
24
|
+
if (!key) throw new Error("invalid CLI flag");
|
|
25
|
+
const next = rest[i + 1];
|
|
26
|
+
if (next && !next.startsWith("--")) {
|
|
27
|
+
flags.set(key, next);
|
|
28
|
+
i++;
|
|
29
|
+
} else {
|
|
30
|
+
flags.set(key, true);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { command, values, flags };
|
|
34
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { parseMolenkopfConfigJson, type NormalizedMolenkopfConfig } from "../../../core/src/config/molenkopf-config.ts";
|
|
4
|
+
|
|
5
|
+
export type LoadedProxyConfig = {
|
|
6
|
+
source: "env" | "file";
|
|
7
|
+
configPath?: string;
|
|
8
|
+
config?: NormalizedMolenkopfConfig;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CONFIG_FILES = ["molenkopf.config.json", ".molenkopf/config.json"];
|
|
12
|
+
|
|
13
|
+
export async function loadProxyConfig(flags: Map<string, string | boolean>, env: Record<string, string | undefined> = process.env, cwd = process.cwd()): Promise<LoadedProxyConfig> {
|
|
14
|
+
const configPath = await resolveConfigPath(flags, env, cwd);
|
|
15
|
+
if (!configPath) return { source: "env" };
|
|
16
|
+
const text = await readFile(configPath, "utf8");
|
|
17
|
+
return { source: "file", configPath, config: parseMolenkopfConfigJson(text, configPath) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function resolveConfigPath(flags: Map<string, string | boolean>, env: Record<string, string | undefined> = process.env, cwd = process.cwd()): Promise<string | undefined> {
|
|
21
|
+
const explicit = stringFlag(flags, "config") ?? env.MOLENKOPF_CONFIG_FILE;
|
|
22
|
+
if (explicit) return requireFile(resolve(cwd, explicit), true);
|
|
23
|
+
for (const name of DEFAULT_CONFIG_FILES) {
|
|
24
|
+
const found = await requireFile(resolve(cwd, name), false);
|
|
25
|
+
if (found) return found;
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function stringFlag(flags: Map<string, string | boolean>, name: string): string | undefined {
|
|
31
|
+
const value = flags.get(name);
|
|
32
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function requireFile(path: string, explicit: boolean): Promise<string | undefined> {
|
|
36
|
+
try {
|
|
37
|
+
await access(path);
|
|
38
|
+
return path;
|
|
39
|
+
} catch {
|
|
40
|
+
if (explicit) throw new Error(`Molenkopf config file not found: ${path}`);
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
type LoadOptions = { overwrite?: boolean };
|
|
6
|
+
|
|
7
|
+
export async function loadEnvFile(file: string, env: Record<string, string | undefined> = process.env, options: LoadOptions = {}): Promise<void> {
|
|
8
|
+
applyEnv(parseEnvFile(await readFile(file, "utf8")), env, options);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function loadDefaultEnvFile(cwd = process.cwd(), env: Record<string, string | undefined> = process.env): boolean {
|
|
12
|
+
const file = join(cwd, ".env");
|
|
13
|
+
if (!existsSync(file)) return false;
|
|
14
|
+
applyEnv(parseEnvFile(readFileSync(file, "utf8")), env, { overwrite: false });
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseEnvFile(text: string): Record<string, string> {
|
|
19
|
+
const result: Record<string, string> = {};
|
|
20
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
21
|
+
const line = rawLine.trim();
|
|
22
|
+
if (!line || line.startsWith("#")) continue;
|
|
23
|
+
const index = line.indexOf("=");
|
|
24
|
+
if (index < 1) continue;
|
|
25
|
+
const key = line.slice(0, index).trim();
|
|
26
|
+
if (!/^[A-Z_][A-Z0-9_]*$/i.test(key)) continue;
|
|
27
|
+
result[key] = unquote(line.slice(index + 1).trim());
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applyEnv(values: Record<string, string>, env: Record<string, string | undefined>, options: LoadOptions): void {
|
|
33
|
+
for (const [key, value] of Object.entries(values)) {
|
|
34
|
+
if (options.overwrite === true || env[key] === undefined) env[key] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function unquote(value: string): string {
|
|
39
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
40
|
+
return value.slice(1, -1);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|