@phenx-inc/ctlsurf 0.13.6 → 0.14.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/out/headless/index.mjs +13663 -385
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +11665 -286
- package/out/main/node-CUnQxFha.js +868 -0
- package/out/preload/index.js +17 -0
- package/out/renderer/assets/{cssMode-CsSLnKZ-.js → cssMode-7adWSORr.js} +3 -3
- package/out/renderer/assets/{freemarker2-C1XYRbOq.js → freemarker2-BkLLWnzk.js} +1 -1
- package/out/renderer/assets/{handlebars-BSdbbnMW.js → handlebars-Cinfm9fZ.js} +1 -1
- package/out/renderer/assets/{html-96igtyOz.js → html-vl9fkHKL.js} +1 -1
- package/out/renderer/assets/{htmlMode-BRnCcIzp.js → htmlMode-BtGqkTNI.js} +3 -3
- package/out/renderer/assets/{index-Cc-jDHm3.js → index-BhrV6ld-.js} +359 -23
- package/out/renderer/assets/{index-BEekQe3-.css → index-BpCORklm.css} +217 -0
- package/out/renderer/assets/{javascript-7MdRNHk9.js → javascript-B7CCdfuW.js} +2 -2
- package/out/renderer/assets/{jsonMode-oGam2go_.js → jsonMode-ut9LWBMJ.js} +3 -3
- package/out/renderer/assets/{liquid-DoKC7ba2.js → liquid-DVDJ0lKt.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-C2h5qGCv.js → lspLanguageFeatures-BbMj8Wsc.js} +1 -1
- package/out/renderer/assets/{mdx-0w8n1HGo.js → mdx-CcQzYB0J.js} +1 -1
- package/out/renderer/assets/{python-BfY4HssB.js → python-Byy7wKMv.js} +1 -1
- package/out/renderer/assets/{razor-BoDk9Ula.js → razor-D1xi_wMM.js} +1 -1
- package/out/renderer/assets/{tsMode-CX0oVKRt.js → tsMode-DJPzbn5i.js} +1 -1
- package/out/renderer/assets/{typescript-BqIU7abP.js → typescript-BuVaU7i3.js} +1 -1
- package/out/renderer/assets/{xml-BlSDnxFw.js → xml-rzc91O3d.js} +1 -1
- package/out/renderer/assets/{yaml-bVMmGUtU.js → yaml-fAOzQMge.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +2 -1
- package/src/main/index.ts +36 -2
- package/src/main/observer/anthropic.ts +48 -0
- package/src/main/observer/budget.ts +79 -0
- package/src/main/observer/ledger.ts +61 -0
- package/src/main/observer/observer.ts +566 -0
- package/src/main/observer/spans.ts +170 -0
- package/src/main/observer/store.ts +200 -0
- package/src/main/observer/tier0.ts +234 -0
- package/src/main/observer/tier1.ts +306 -0
- package/src/main/observer/tier2.ts +125 -0
- package/src/main/observer/types.ts +224 -0
- package/src/main/orchestrator.ts +74 -0
- package/src/main/transcripts.ts +18 -0
- package/src/preload/index.ts +36 -0
- package/src/renderer/App.tsx +22 -0
- package/src/renderer/components/ObserverPanel.tsx +289 -0
- package/src/renderer/components/SettingsDialog.tsx +104 -0
- package/src/renderer/styles.css +217 -0
|
@@ -9768,3 +9768,220 @@ html, body, #root {
|
|
|
9768
9768
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
|
9769
9769
|
pointer-events: none;
|
|
9770
9770
|
}
|
|
9771
|
+
|
|
9772
|
+
/* ─── Observer pane ─────────────────────────────── */
|
|
9773
|
+
|
|
9774
|
+
/* Header on/off switch */
|
|
9775
|
+
.observer-switch { display: inline-flex; align-items: center; cursor: pointer; padding-top: 2px; }
|
|
9776
|
+
.observer-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
|
|
9777
|
+
.observer-switch-track {
|
|
9778
|
+
width: 34px; height: 19px; border-radius: 10px;
|
|
9779
|
+
background: #3b4261; position: relative; transition: background 0.15s ease;
|
|
9780
|
+
display: inline-block; flex-shrink: 0;
|
|
9781
|
+
}
|
|
9782
|
+
.observer-switch-thumb {
|
|
9783
|
+
position: absolute; top: 2px; left: 2px; width: 15px; height: 15px;
|
|
9784
|
+
border-radius: 50%; background: #c0caf5; transition: transform 0.15s ease;
|
|
9785
|
+
}
|
|
9786
|
+
.observer-switch input:checked + .observer-switch-track { background: #9ece6a; }
|
|
9787
|
+
.observer-switch input:checked + .observer-switch-track .observer-switch-thumb {
|
|
9788
|
+
transform: translateX(15px); background: #1a1b26;
|
|
9789
|
+
}
|
|
9790
|
+
.observer-switch input:disabled + .observer-switch-track { opacity: 0.5; }
|
|
9791
|
+
|
|
9792
|
+
.obs-feed { gap: 6px; }
|
|
9793
|
+
|
|
9794
|
+
/* Finding / digest cards */
|
|
9795
|
+
.obs-card {
|
|
9796
|
+
position: relative;
|
|
9797
|
+
display: flex;
|
|
9798
|
+
flex-direction: column;
|
|
9799
|
+
gap: 5px;
|
|
9800
|
+
width: 100%;
|
|
9801
|
+
text-align: left;
|
|
9802
|
+
appearance: none;
|
|
9803
|
+
font: inherit;
|
|
9804
|
+
color: inherit;
|
|
9805
|
+
cursor: pointer;
|
|
9806
|
+
background: #15161f;
|
|
9807
|
+
border: 1px solid #262a3f;
|
|
9808
|
+
border-left: 3px solid #565f89;
|
|
9809
|
+
border-radius: 9px;
|
|
9810
|
+
padding: 9px 26px 9px 12px;
|
|
9811
|
+
transition: border-color 0.12s ease, background 0.12s ease;
|
|
9812
|
+
}
|
|
9813
|
+
.obs-card:hover { background: #181a26; border-color: #323752; border-left-width: 3px; }
|
|
9814
|
+
.obs-card:active { transform: scale(0.995); }
|
|
9815
|
+
|
|
9816
|
+
.obs-card-top {
|
|
9817
|
+
display: flex;
|
|
9818
|
+
align-items: center;
|
|
9819
|
+
gap: 8px;
|
|
9820
|
+
min-width: 0;
|
|
9821
|
+
}
|
|
9822
|
+
.obs-chip {
|
|
9823
|
+
display: inline-flex;
|
|
9824
|
+
align-items: center;
|
|
9825
|
+
gap: 5px;
|
|
9826
|
+
flex-shrink: 0;
|
|
9827
|
+
font-size: 9.5px;
|
|
9828
|
+
font-weight: 600;
|
|
9829
|
+
letter-spacing: 0.06em;
|
|
9830
|
+
text-transform: uppercase;
|
|
9831
|
+
padding: 2px 7px 2px 6px;
|
|
9832
|
+
border: 1px solid;
|
|
9833
|
+
border-radius: 999px;
|
|
9834
|
+
line-height: 1.4;
|
|
9835
|
+
}
|
|
9836
|
+
.obs-chip-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; }
|
|
9837
|
+
.obs-card-cat {
|
|
9838
|
+
font-size: 10px;
|
|
9839
|
+
color: #565f89;
|
|
9840
|
+
letter-spacing: 0.05em;
|
|
9841
|
+
text-transform: uppercase;
|
|
9842
|
+
white-space: nowrap;
|
|
9843
|
+
overflow: hidden;
|
|
9844
|
+
text-overflow: ellipsis;
|
|
9845
|
+
}
|
|
9846
|
+
.obs-card-time {
|
|
9847
|
+
margin-left: auto;
|
|
9848
|
+
flex-shrink: 0;
|
|
9849
|
+
font-size: 10px;
|
|
9850
|
+
color: #414868;
|
|
9851
|
+
font-variant-numeric: tabular-nums;
|
|
9852
|
+
}
|
|
9853
|
+
.obs-card-title {
|
|
9854
|
+
font-size: 12.5px;
|
|
9855
|
+
font-weight: 600;
|
|
9856
|
+
color: #c0caf5;
|
|
9857
|
+
line-height: 1.4;
|
|
9858
|
+
word-break: break-word;
|
|
9859
|
+
}
|
|
9860
|
+
.obs-card-body {
|
|
9861
|
+
font-size: 11.5px;
|
|
9862
|
+
color: #9aa5ce;
|
|
9863
|
+
line-height: 1.5;
|
|
9864
|
+
word-break: break-word;
|
|
9865
|
+
}
|
|
9866
|
+
.obs-card-body p { margin: 0 0 6px; white-space: pre-wrap; }
|
|
9867
|
+
.obs-card-facts { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
9868
|
+
.obs-fact {
|
|
9869
|
+
font-size: 10px;
|
|
9870
|
+
color: #565f89;
|
|
9871
|
+
background: #1c1e2b;
|
|
9872
|
+
border: 1px solid #262a3f;
|
|
9873
|
+
border-radius: 5px;
|
|
9874
|
+
padding: 1px 6px;
|
|
9875
|
+
font-variant-numeric: tabular-nums;
|
|
9876
|
+
}
|
|
9877
|
+
.obs-card-caret {
|
|
9878
|
+
position: absolute;
|
|
9879
|
+
right: 10px;
|
|
9880
|
+
top: 10px;
|
|
9881
|
+
font-size: 10px;
|
|
9882
|
+
color: #414868;
|
|
9883
|
+
transition: transform 0.12s ease;
|
|
9884
|
+
}
|
|
9885
|
+
.obs-card-caret.open { transform: rotate(90deg); }
|
|
9886
|
+
|
|
9887
|
+
/* One-line heartbeats (reviews ran, attach/detach) and health warnings */
|
|
9888
|
+
.obs-line {
|
|
9889
|
+
display: flex;
|
|
9890
|
+
align-items: baseline;
|
|
9891
|
+
gap: 8px;
|
|
9892
|
+
padding: 3px 12px 3px 14px;
|
|
9893
|
+
font-size: 11px;
|
|
9894
|
+
color: #565f89;
|
|
9895
|
+
line-height: 1.45;
|
|
9896
|
+
}
|
|
9897
|
+
.obs-line-dot {
|
|
9898
|
+
flex-shrink: 0;
|
|
9899
|
+
width: 4px; height: 4px;
|
|
9900
|
+
border-radius: 50%;
|
|
9901
|
+
background: #3b4261;
|
|
9902
|
+
align-self: center;
|
|
9903
|
+
}
|
|
9904
|
+
.obs-line.health { color: #e0af68; }
|
|
9905
|
+
.obs-line.health .obs-line-dot { background: #e0af68; }
|
|
9906
|
+
.obs-line-text { min-width: 0; word-break: break-word; }
|
|
9907
|
+
.obs-line-time {
|
|
9908
|
+
margin-left: auto;
|
|
9909
|
+
flex-shrink: 0;
|
|
9910
|
+
font-size: 10px;
|
|
9911
|
+
color: #414868;
|
|
9912
|
+
font-variant-numeric: tabular-nums;
|
|
9913
|
+
}
|
|
9914
|
+
|
|
9915
|
+
/* Sensitivity lever + processing indicator row */
|
|
9916
|
+
.obs-toolbar {
|
|
9917
|
+
display: flex;
|
|
9918
|
+
align-items: center;
|
|
9919
|
+
justify-content: space-between;
|
|
9920
|
+
gap: 10px;
|
|
9921
|
+
padding: 8px 14px;
|
|
9922
|
+
border-bottom: 1px solid #23263a;
|
|
9923
|
+
}
|
|
9924
|
+
.obs-sensitivity {
|
|
9925
|
+
display: inline-flex;
|
|
9926
|
+
background: #15161f;
|
|
9927
|
+
border: 1px solid #262a3f;
|
|
9928
|
+
border-radius: 7px;
|
|
9929
|
+
padding: 2px;
|
|
9930
|
+
gap: 2px;
|
|
9931
|
+
}
|
|
9932
|
+
.obs-sens-btn {
|
|
9933
|
+
appearance: none;
|
|
9934
|
+
border: none;
|
|
9935
|
+
background: transparent;
|
|
9936
|
+
color: #565f89;
|
|
9937
|
+
font: inherit;
|
|
9938
|
+
font-size: 10.5px;
|
|
9939
|
+
font-weight: 600;
|
|
9940
|
+
padding: 3px 9px;
|
|
9941
|
+
border-radius: 5px;
|
|
9942
|
+
cursor: pointer;
|
|
9943
|
+
transition: background 0.12s ease, color 0.12s ease;
|
|
9944
|
+
white-space: nowrap;
|
|
9945
|
+
}
|
|
9946
|
+
.obs-sens-btn:hover { color: #9aa5ce; }
|
|
9947
|
+
.obs-sens-btn.active { background: #2a2f4a; color: #c0caf5; }
|
|
9948
|
+
|
|
9949
|
+
/* "reviewing" bouncing dots */
|
|
9950
|
+
.obs-reviewing {
|
|
9951
|
+
display: inline-flex;
|
|
9952
|
+
align-items: center;
|
|
9953
|
+
gap: 3px;
|
|
9954
|
+
font-size: 10.5px;
|
|
9955
|
+
color: #7aa2f7;
|
|
9956
|
+
letter-spacing: 0.03em;
|
|
9957
|
+
}
|
|
9958
|
+
.obs-reviewing-dot {
|
|
9959
|
+
width: 4px; height: 4px;
|
|
9960
|
+
border-radius: 50%;
|
|
9961
|
+
background: #7aa2f7;
|
|
9962
|
+
animation: obs-dot-bounce 1.1s ease-in-out infinite;
|
|
9963
|
+
}
|
|
9964
|
+
.obs-reviewing-dot:nth-child(2) { animation-delay: 0.15s; }
|
|
9965
|
+
.obs-reviewing-dot:nth-child(3) { animation-delay: 0.3s; margin-right: 4px; }
|
|
9966
|
+
@keyframes obs-dot-bounce {
|
|
9967
|
+
0%, 60%, 100% { transform: translateY(0); opacity: 0.5; }
|
|
9968
|
+
30% { transform: translateY(-3px); opacity: 1; }
|
|
9969
|
+
}
|
|
9970
|
+
|
|
9971
|
+
/* Pulsing eye badge while a model review is in flight */
|
|
9972
|
+
.obs-badge { position: relative; }
|
|
9973
|
+
.obs-badge.reviewing { color: #7aa2f7; }
|
|
9974
|
+
.obs-badge.reviewing::after {
|
|
9975
|
+
content: '';
|
|
9976
|
+
position: absolute;
|
|
9977
|
+
inset: -1px;
|
|
9978
|
+
border-radius: inherit;
|
|
9979
|
+
border: 1px solid #7aa2f7;
|
|
9980
|
+
animation: obs-badge-pulse 1.6s ease-out infinite;
|
|
9981
|
+
pointer-events: none;
|
|
9982
|
+
}
|
|
9983
|
+
@keyframes obs-badge-pulse {
|
|
9984
|
+
0% { opacity: 0.9; transform: scale(1); }
|
|
9985
|
+
70% { opacity: 0; transform: scale(1.35); }
|
|
9986
|
+
100% { opacity: 0; transform: scale(1.35); }
|
|
9987
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { conf as conf$1, language as language$1 } from "./typescript-
|
|
2
|
-
import "./index-
|
|
1
|
+
import { conf as conf$1, language as language$1 } from "./typescript-BuVaU7i3.js";
|
|
2
|
+
import "./index-BhrV6ld-.js";
|
|
3
3
|
const conf = conf$1;
|
|
4
4
|
const language = {
|
|
5
5
|
// Set defaultToken to invalid to see what you do not tokenize yet
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebWorker, l as languages, e as editor } from "./index-
|
|
2
|
-
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-
|
|
3
|
-
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-
|
|
1
|
+
import { c as createWebWorker, l as languages, e as editor } from "./index-BhrV6ld-.js";
|
|
2
|
+
import { f as DocumentFormattingEditProvider, g as DocumentRangeFormattingEditProvider, C as CompletionAdapter, H as HoverAdapter, b as DocumentSymbolAdapter, d as DocumentColorAdapter, F as FoldingRangeAdapter, S as SelectionRangeAdapter, e as DiagnosticsAdapter } from "./lspLanguageFeatures-BbMj8Wsc.js";
|
|
3
|
+
import { a, D, h, R, c, i, j, t, k } from "./lspLanguageFeatures-BbMj8Wsc.js";
|
|
4
4
|
const STOP_WHEN_IDLE_FOR = 2 * 60 * 1e3;
|
|
5
5
|
class WorkerManager {
|
|
6
6
|
constructor(defaults) {
|
package/out/renderer/assets/{lspLanguageFeatures-C2h5qGCv.js → lspLanguageFeatures-BbMj8Wsc.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-
|
|
1
|
+
import { R as Range$1, l as languages, e as editor, U as Uri, M as MarkerSeverity } from "./index-BhrV6ld-.js";
|
|
2
2
|
var DocumentUri;
|
|
3
3
|
(function(DocumentUri2) {
|
|
4
4
|
function is(value) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-
|
|
1
|
+
import { c as createWebWorker, e as editor, U as Uri, a as MarkerTag, M as MarkerSeverity, l as languages, t as typescriptDefaults, R as Range } from "./index-BhrV6ld-.js";
|
|
2
2
|
class WorkerManager {
|
|
3
3
|
constructor(_modeId, _defaults) {
|
|
4
4
|
this._modeId = _modeId;
|
package/out/renderer/index.html
CHANGED
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
color: #565f89;
|
|
50
50
|
}
|
|
51
51
|
</style>
|
|
52
|
-
<script type="module" crossorigin src="./assets/index-
|
|
53
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
52
|
+
<script type="module" crossorigin src="./assets/index-BhrV6ld-.js"></script>
|
|
53
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BpCORklm.css">
|
|
54
54
|
</head>
|
|
55
55
|
<body>
|
|
56
56
|
<div id="boot-splash">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phenx-inc/ctlsurf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
|
|
5
5
|
"main": "out/main/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"node": ">=18"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
+
"@anthropic-ai/sdk": "^0.110.0",
|
|
46
47
|
"@huggingface/transformers": "^4.2.0",
|
|
47
48
|
"@monaco-editor/react": "^4.7.0",
|
|
48
49
|
"@xterm/addon-fit": "^0.10.0",
|
package/src/main/index.ts
CHANGED
|
@@ -213,6 +213,8 @@ const orchestrator = new Orchestrator(
|
|
|
213
213
|
onWorkerRegistered: (data) => mainWindow?.webContents.send('worker:registered', data),
|
|
214
214
|
onProjectChanged: (name) => mainWindow?.webContents.send('app:projectChanged', name),
|
|
215
215
|
onAgentMessage: (text) => mainWindow?.webContents.send('agent:message', text),
|
|
216
|
+
onObserverUpdate: (item) => mainWindow?.webContents.send('observer:update', item),
|
|
217
|
+
onObserverActivity: (reviewing) => mainWindow?.webContents.send('observer:activity', reviewing),
|
|
216
218
|
onAgentStateChange: (tabId, state, anyAwaiting) => {
|
|
217
219
|
log(`[agent-state] tab=${tabId} state=${state} anyAwaiting=${anyAwaiting}`)
|
|
218
220
|
mainWindow?.webContents.send('agent:state', tabId, state, anyAwaiting)
|
|
@@ -619,6 +621,19 @@ ipcMain.handle('speak:set', (_event, enabled: boolean) => {
|
|
|
619
621
|
orchestrator.setSpeakReplies(!!enabled)
|
|
620
622
|
return { enabled: orchestrator.speakRepliesEnabled }
|
|
621
623
|
})
|
|
624
|
+
|
|
625
|
+
// ─── Observer IPC ─────────────────────────────────
|
|
626
|
+
|
|
627
|
+
ipcMain.handle('observer:get', () => orchestrator.getObserverSettingsSummary())
|
|
628
|
+
ipcMain.handle('observer:set', (_event, data: { enabled: boolean; anthropicApiKey?: string; dailyCapUsd?: number; sensitivity?: 'severe' | 'balanced' | 'all' }) => {
|
|
629
|
+
orchestrator.saveObserverSettings(data)
|
|
630
|
+
return orchestrator.getObserverSettingsSummary()
|
|
631
|
+
})
|
|
632
|
+
ipcMain.handle('observer:feed', () => orchestrator.observer.getFeed())
|
|
633
|
+
ipcMain.handle('observer:toggle', (_event, enabled: boolean) => {
|
|
634
|
+
orchestrator.setObserverEnabled(!!enabled)
|
|
635
|
+
return orchestrator.observer.getFeed()
|
|
636
|
+
})
|
|
622
637
|
// Latest assistant reply from the active tab's transcript, for on-demand TTS.
|
|
623
638
|
ipcMain.handle('agent:getLastReply', () => orchestrator.getLastReply())
|
|
624
639
|
|
|
@@ -709,9 +724,28 @@ app.whenReady().then(() => {
|
|
|
709
724
|
setInterval(() => { void checkForUpdate() }, UPDATE_CHECK_INTERVAL_MS)
|
|
710
725
|
})
|
|
711
726
|
|
|
727
|
+
// Shutdown is async (final Tier-1 flush, session digest, time-tracker
|
|
728
|
+
// writes). Hold the quit until it settles — capped so a hung network call
|
|
729
|
+
// can never trap the process — otherwise that work is lost. before-quit
|
|
730
|
+
// covers Cmd+Q; window-all-closed covers closing the last window.
|
|
731
|
+
let shutdownStarted = false
|
|
732
|
+
function shutdownThenQuit(): void {
|
|
733
|
+
if (shutdownStarted) return
|
|
734
|
+
shutdownStarted = true
|
|
735
|
+
void Promise.race([
|
|
736
|
+
orchestrator.shutdown(),
|
|
737
|
+
new Promise((resolve) => setTimeout(resolve, 10_000)),
|
|
738
|
+
]).finally(() => app.quit())
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
app.on('before-quit', (event) => {
|
|
742
|
+
if (shutdownStarted) return // second pass: let the quit proceed
|
|
743
|
+
event.preventDefault()
|
|
744
|
+
shutdownThenQuit()
|
|
745
|
+
})
|
|
746
|
+
|
|
712
747
|
app.on('window-all-closed', () => {
|
|
713
|
-
|
|
714
|
-
app.quit()
|
|
748
|
+
shutdownThenQuit()
|
|
715
749
|
})
|
|
716
750
|
|
|
717
751
|
app.on('activate', () => {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk'
|
|
2
|
+
import type { ModelCaller, ModelCallRequest, ModelCallResult } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Production ModelCaller backed by @anthropic-ai/sdk.
|
|
6
|
+
*
|
|
7
|
+
* The system block carries a cache_control marker so the 5-min prompt cache
|
|
8
|
+
* is refreshed by the Tier-1 call cadence (spec §3.2). Structured output
|
|
9
|
+
* (output_config.format json_schema) enforces the strict-JSON contract when
|
|
10
|
+
* a schema is provided.
|
|
11
|
+
*/
|
|
12
|
+
export function createModelCaller(apiKey: string): ModelCaller {
|
|
13
|
+
const client = new Anthropic({ apiKey })
|
|
14
|
+
|
|
15
|
+
return async (req: ModelCallRequest): Promise<ModelCallResult> => {
|
|
16
|
+
const params: Record<string, unknown> = {
|
|
17
|
+
model: req.model,
|
|
18
|
+
max_tokens: req.maxTokens,
|
|
19
|
+
system: [
|
|
20
|
+
{ type: 'text', text: req.system, cache_control: { type: 'ephemeral' } },
|
|
21
|
+
],
|
|
22
|
+
messages: [{ role: 'user', content: req.user }],
|
|
23
|
+
}
|
|
24
|
+
if (req.schema) {
|
|
25
|
+
params.output_config = { format: { type: 'json_schema', schema: req.schema } }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The SDK's create() is typed narrowly around output_config across
|
|
29
|
+
// versions; pass through as any to keep the wire shape authoritative.
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
+
const response: any = await (client.messages.create as any)(params)
|
|
32
|
+
|
|
33
|
+
const text = (response.content || [])
|
|
34
|
+
.filter((b: any) => b?.type === 'text' && typeof b.text === 'string')
|
|
35
|
+
.map((b: any) => b.text)
|
|
36
|
+
.join('')
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
text,
|
|
40
|
+
usage: {
|
|
41
|
+
inputTokens: response.usage?.input_tokens ?? 0,
|
|
42
|
+
outputTokens: response.usage?.output_tokens ?? 0,
|
|
43
|
+
cacheReadTokens: response.usage?.cache_read_input_tokens ?? 0,
|
|
44
|
+
cacheWriteTokens: response.usage?.cache_creation_input_tokens ?? 0,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Daily spend guard (spec §9). Tracks approximate model cost in a small
|
|
6
|
+
* JSON file; on cap breach Tier 1 degrades to Tier-0-tagged events only
|
|
7
|
+
* and an observer.health event is emitted by the caller.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface Usage {
|
|
11
|
+
inputTokens: number
|
|
12
|
+
outputTokens: number
|
|
13
|
+
cacheReadTokens: number
|
|
14
|
+
cacheWriteTokens: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** $/MTok — input, output. Cache reads 0.1x input, writes 1.25x input. */
|
|
18
|
+
const PRICES: Record<string, { in: number; out: number }> = {
|
|
19
|
+
'claude-haiku-4-5': { in: 1, out: 5 },
|
|
20
|
+
'claude-sonnet-4-6': { in: 3, out: 15 },
|
|
21
|
+
'claude-sonnet-5': { in: 3, out: 15 },
|
|
22
|
+
}
|
|
23
|
+
const DEFAULT_PRICE = { in: 3, out: 15 }
|
|
24
|
+
|
|
25
|
+
export function estimateCostUsd(model: string, usage: Usage): number {
|
|
26
|
+
const p = PRICES[model] || DEFAULT_PRICE
|
|
27
|
+
return (
|
|
28
|
+
(usage.inputTokens * p.in +
|
|
29
|
+
usage.outputTokens * p.out +
|
|
30
|
+
usage.cacheReadTokens * p.in * 0.1 +
|
|
31
|
+
usage.cacheWriteTokens * p.in * 1.25) /
|
|
32
|
+
1_000_000
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ObserverBudget {
|
|
37
|
+
private readonly filePath: string
|
|
38
|
+
private readonly capUsd: number
|
|
39
|
+
private date: string
|
|
40
|
+
private spentUsd = 0
|
|
41
|
+
|
|
42
|
+
constructor(dir: string, capUsd: number, now: () => Date = () => new Date()) {
|
|
43
|
+
this.filePath = path.join(dir, 'budget.json')
|
|
44
|
+
this.capUsd = capUsd
|
|
45
|
+
this.date = localDay(now())
|
|
46
|
+
try {
|
|
47
|
+
const raw = JSON.parse(fs.readFileSync(this.filePath, 'utf-8'))
|
|
48
|
+
if (raw?.date === this.date && typeof raw.spentUsd === 'number') {
|
|
49
|
+
this.spentUsd = raw.spentUsd
|
|
50
|
+
}
|
|
51
|
+
} catch { /* fresh day / first run */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
addUsage(model: string, usage: Usage, now: () => Date = () => new Date()): void {
|
|
55
|
+
const today = localDay(now())
|
|
56
|
+
if (today !== this.date) {
|
|
57
|
+
this.date = today
|
|
58
|
+
this.spentUsd = 0
|
|
59
|
+
}
|
|
60
|
+
this.spentUsd += estimateCostUsd(model, usage)
|
|
61
|
+
try {
|
|
62
|
+
fs.mkdirSync(path.dirname(this.filePath), { recursive: true })
|
|
63
|
+
fs.writeFileSync(this.filePath, JSON.stringify({ date: this.date, spentUsd: this.spentUsd }))
|
|
64
|
+
} catch { /* best-effort */ }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
overCap(now: () => Date = () => new Date()): boolean {
|
|
68
|
+
if (localDay(now()) !== this.date) return false // new day resets on next addUsage
|
|
69
|
+
return this.spentUsd >= this.capUsd
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get spent(): number {
|
|
73
|
+
return this.spentUsd
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function localDay(d: Date): string {
|
|
78
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
79
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import type { LedgerEvent, LedgerEventType } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Session Ledger
|
|
7
|
+
*
|
|
8
|
+
* Append-only JSONL file per observed session — the substrate everything
|
|
9
|
+
* else consumes (spec §1, §6). Events get monotonic `evt_N` ids; Observer
|
|
10
|
+
* outputs (findings, spans, digests, health) are appended to the same file
|
|
11
|
+
* with their own id prefixes so receipts always resolve within the file.
|
|
12
|
+
*
|
|
13
|
+
* A bounded in-memory tail is kept for ground-truth lookback (e.g. "was a
|
|
14
|
+
* test command actually run since the last user message?").
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const MEMORY_TAIL_EVENTS = 500
|
|
18
|
+
|
|
19
|
+
export class SessionLedger {
|
|
20
|
+
readonly sessionId: string
|
|
21
|
+
readonly filePath: string
|
|
22
|
+
|
|
23
|
+
private seq = 0
|
|
24
|
+
private tail: LedgerEvent[] = []
|
|
25
|
+
|
|
26
|
+
constructor(dir: string, sessionId: string) {
|
|
27
|
+
this.sessionId = sessionId
|
|
28
|
+
this.filePath = path.join(dir, `${sessionId}.jsonl`)
|
|
29
|
+
try { fs.mkdirSync(dir, { recursive: true }) } catch { /* ignore */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Append a normalized event; assigns id + session and persists it. */
|
|
33
|
+
append(partial: Omit<LedgerEvent, 'id' | 'session'> & { id?: string }): LedgerEvent {
|
|
34
|
+
const event: LedgerEvent = {
|
|
35
|
+
...partial,
|
|
36
|
+
id: partial.id || this.nextId(partial.type),
|
|
37
|
+
session: this.sessionId,
|
|
38
|
+
}
|
|
39
|
+
this.tail.push(event)
|
|
40
|
+
if (this.tail.length > MEMORY_TAIL_EVENTS) {
|
|
41
|
+
this.tail.splice(0, this.tail.length - MEMORY_TAIL_EVENTS)
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
fs.appendFileSync(this.filePath, JSON.stringify(event) + '\n')
|
|
45
|
+
} catch { /* best-effort: observation must never break the session */ }
|
|
46
|
+
return event
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Recent events, oldest first (bounded to the in-memory tail). */
|
|
50
|
+
recent(limit = MEMORY_TAIL_EVENTS): LedgerEvent[] {
|
|
51
|
+
return limit >= this.tail.length ? [...this.tail] : this.tail.slice(-limit)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private nextId(type: LedgerEventType): string {
|
|
55
|
+
if (type === 'observer.finding') return `fnd_${this.sessionId.slice(4, 10)}_${++this.seq}`
|
|
56
|
+
if (type === 'observer.span') return `spn_${this.sessionId.slice(4, 10)}_${++this.seq}`
|
|
57
|
+
if (type === 'observer.digest') return `dig_${this.sessionId.slice(4, 10)}_${++this.seq}`
|
|
58
|
+
if (type === 'observer.health') return `hlt_${this.sessionId.slice(4, 10)}_${++this.seq}`
|
|
59
|
+
return `evt_${++this.seq}`
|
|
60
|
+
}
|
|
61
|
+
}
|