@wrongstack/core 0.310.1 → 0.313.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chronicle/index.js +84 -6391
- package/dist/chronicle/project-server.js +84 -4868
- package/dist/chunk-2BUUIMQB.js +225 -0
- package/dist/chunk-2FK4WTFF.js +207 -0
- package/dist/chunk-2FQL4PDC.js +292 -0
- package/dist/chunk-2KKODIQU.js +2256 -0
- package/dist/chunk-2YU4WVBW.js +134 -0
- package/dist/chunk-3E244UDA.js +356 -0
- package/dist/chunk-3XPVB7BA.js +135 -0
- package/dist/chunk-4LJJYBAU.js +388 -0
- package/dist/chunk-4VWO3ZXL.js +342 -0
- package/dist/chunk-4XV6IR37.js +187 -0
- package/dist/chunk-4ZHTKJHM.js +183 -0
- package/dist/chunk-52RTVH4M.js +361 -0
- package/dist/chunk-57GEWK5X.js +26 -0
- package/dist/chunk-5DSMEVA4.js +931 -0
- package/dist/chunk-5RP3QTRL.js +1223 -0
- package/dist/chunk-5S437PRC.js +50 -0
- package/dist/chunk-5XDUGTKN.js +511 -0
- package/dist/chunk-62V2AFVG.js +422 -0
- package/dist/chunk-65RDIOR2.js +2470 -0
- package/dist/chunk-6AKOTGLM.js +597 -0
- package/dist/chunk-7ZHZE5EO.js +28 -0
- package/dist/chunk-A455ATZB.js +140 -0
- package/dist/chunk-AD7ZFZCS.js +46 -0
- package/dist/chunk-AI4LLQ2U.js +856 -0
- package/dist/chunk-ASX6AW36.js +789 -0
- package/dist/chunk-AT4V4XGI.js +3522 -0
- package/dist/chunk-AWNQ6ED6.js +311 -0
- package/dist/chunk-B4AMLZPB.js +9 -0
- package/dist/chunk-BAKVJLEV.js +417 -0
- package/dist/chunk-BJHVULHO.js +55 -0
- package/dist/chunk-CB2HFKTP.js +9 -0
- package/dist/chunk-CE4ZOCOI.js +1640 -0
- package/dist/chunk-CQQL4ZOQ.js +151 -0
- package/dist/chunk-DEIQMBD2.js +65 -0
- package/dist/chunk-E6I5RXVK.js +58 -0
- package/dist/chunk-EFEHDQGY.js +1589 -0
- package/dist/chunk-EODDVJBM.js +82 -0
- package/dist/chunk-EPC3GZD4.js +308 -0
- package/dist/chunk-FAYRX2AD.js +13 -0
- package/dist/chunk-FFSX2QDQ.js +362 -0
- package/dist/chunk-FHAMQS7Q.js +5856 -0
- package/dist/chunk-FIV4IJL7.js +2263 -0
- package/dist/chunk-FNXVT7HF.js +52 -0
- package/dist/chunk-FSWCWSTS.js +395 -0
- package/dist/chunk-FW5LDDMJ.js +16464 -0
- package/dist/chunk-FXQLIX3K.js +211 -0
- package/dist/chunk-G5AAKEEZ.js +228 -0
- package/dist/chunk-G5S3S5ZN.js +38 -0
- package/dist/chunk-GAZEWQEZ.js +231 -0
- package/dist/chunk-GFJOMSHG.js +125 -0
- package/dist/chunk-GFXZTHNI.js +158 -0
- package/dist/chunk-GM54GJBU.js +960 -0
- package/dist/chunk-GPQ3NJPR.js +900 -0
- package/dist/chunk-GTCU6DZ3.js +1 -0
- package/dist/chunk-H5E77KUJ.js +15 -0
- package/dist/chunk-H72H4XDU.js +1023 -0
- package/dist/chunk-HA4ZYA6T.js +178 -0
- package/dist/chunk-I45KAW2N.js +5020 -0
- package/dist/chunk-IQ35HMQL.js +1396 -0
- package/dist/chunk-IYFBFX34.js +86 -0
- package/dist/chunk-JJPMWPB6.js +109 -0
- package/dist/chunk-JTHKOYWU.js +663 -0
- package/dist/chunk-JVGPR3TS.js +365 -0
- package/dist/chunk-JVKB2TBL.js +48 -0
- package/dist/chunk-JX7VBZDK.js +31 -0
- package/dist/chunk-K3G6JBUC.js +5232 -0
- package/dist/chunk-K4YUNKU7.js +589 -0
- package/dist/chunk-KBZGE6LN.js +710 -0
- package/dist/chunk-KDQJCT2J.js +45 -0
- package/dist/chunk-KI4L2DUL.js +560 -0
- package/dist/chunk-KJ2ZTQSL.js +47 -0
- package/dist/chunk-KSJ5UTP7.js +154 -0
- package/dist/chunk-LHOA5DWA.js +28 -0
- package/dist/chunk-LX5QWLBL.js +27 -0
- package/dist/chunk-LZDDLNCZ.js +279 -0
- package/dist/chunk-M2WF6AXG.js +115 -0
- package/dist/chunk-M7BS2K6U.js +1 -0
- package/dist/chunk-NDBIPFNY.js +102 -0
- package/dist/chunk-NEMIWEXC.js +1199 -0
- package/dist/chunk-NFAYHPRH.js +323 -0
- package/dist/chunk-O37BQ65S.js +21 -0
- package/dist/chunk-O74MJHXU.js +187 -0
- package/dist/chunk-OJZEX73F.js +112 -0
- package/dist/chunk-PAAWLLZX.js +74 -0
- package/dist/chunk-PFXFQQRT.js +339 -0
- package/dist/chunk-PFZO3C6H.js +14 -0
- package/dist/chunk-PHH7LD45.js +113 -0
- package/dist/chunk-QRTG4XTI.js +188 -0
- package/dist/chunk-QVOVMXJR.js +1351 -0
- package/dist/chunk-RCQYT2I4.js +757 -0
- package/dist/chunk-RD6DEE36.js +1178 -0
- package/dist/chunk-RJIZEJ42.js +30 -0
- package/dist/chunk-RPOA7IAE.js +150 -0
- package/dist/chunk-SN2DQRL6.js +1172 -0
- package/dist/chunk-SQJVFEBV.js +41 -0
- package/dist/chunk-SQP64RCS.js +5391 -0
- package/dist/chunk-SVT4RNTR.js +49 -0
- package/dist/chunk-SWUFN7XM.js +43 -0
- package/dist/chunk-SXMVYC5H.js +174 -0
- package/dist/chunk-SZ6AYUIZ.js +23 -0
- package/dist/chunk-T3KGZBU5.js +3171 -0
- package/dist/chunk-TBRL2RCC.js +9 -0
- package/dist/chunk-TRT2HRWZ.js +18 -0
- package/dist/chunk-TYW2GI5I.js +1077 -0
- package/dist/chunk-UIWONWVG.js +4959 -0
- package/dist/chunk-VB3H5WWB.js +151 -0
- package/dist/chunk-W4A6QA3D.js +258 -0
- package/dist/chunk-WBPVKEEE.js +227 -0
- package/dist/chunk-WEBMSQP2.js +123 -0
- package/dist/chunk-WOEZV3Q4.js +333 -0
- package/dist/chunk-WQQON3SV.js +473 -0
- package/dist/chunk-WQTOVMGV.js +119 -0
- package/dist/chunk-WZO23AWO.js +2779 -0
- package/dist/chunk-XBDP4QPW.js +7 -0
- package/dist/chunk-XEBHSJLT.js +103 -0
- package/dist/chunk-XQ2ECNK3.js +1 -0
- package/dist/chunk-XQ2XGIBF.js +2086 -0
- package/dist/chunk-Y2GE5TSI.js +355 -0
- package/dist/chunk-YBUJCZ57.js +351 -0
- package/dist/chunk-YNCVSFGF.js +149 -0
- package/dist/chunk-YUPMXOGP.js +282 -0
- package/dist/chunk-YY6APYUM.js +49 -0
- package/dist/chunk-Z2IJUO45.js +691 -0
- package/dist/chunk-ZEHUCJHY.js +129 -0
- package/dist/chunk-ZYU2YOJ6.js +1496 -0
- package/dist/chunk-ZZ4V3WIZ.js +163 -0
- package/dist/coordination/agents/index.js +145 -6260
- package/dist/coordination/index.js +365 -34323
- package/dist/coordination/mailbox-project-server.js +48 -3220
- package/dist/coordination/mailbox-types.d.ts +13 -1
- package/dist/coordination/provider-status-tracker.d.ts +4 -0
- package/dist/coordination/sqlite-mailbox.d.ts +2 -1
- package/dist/core/agent.d.ts +14 -1
- package/dist/core/index.js +267 -12479
- package/dist/core/model-ref.js +5 -23
- package/dist/core/provider-runner.d.ts +36 -1
- package/dist/design/index.js +50 -1087
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +1599 -21887
- package/dist/execution/prompt-enhancer.js +16 -535
- package/dist/execution/retry-policy.d.ts +31 -1
- package/dist/extension/index.js +7 -311
- package/dist/goal/index.js +33 -3435
- package/dist/hooks/index.js +14 -1138
- package/dist/hq/index.js +112 -5139
- package/dist/hq/protocol.js +21 -668
- package/dist/index.js +1410 -92655
- package/dist/infrastructure/index.js +64 -4811
- package/dist/kernel/events/provider-events.d.ts +15 -0
- package/dist/kernel/events/wrongtrace-events.d.ts +26 -0
- package/dist/kernel/events.d.ts +16 -7
- package/dist/kernel/index.js +14 -992
- package/dist/models/index.js +40 -1117
- package/dist/notifications/index.js +3 -108
- package/dist/observability/index.js +17 -279
- package/dist/plugin/index.js +584 -14835
- package/dist/prompts/index.js +14 -948
- package/dist/registry/index.js +9 -822
- package/dist/registry-R23UBE7H.js +14 -0
- package/dist/replay/index.js +6 -84
- package/dist/review-finding-store-DKAV3AS6.js +21 -0
- package/dist/review-report-store-OWY3EC3N.js +19 -0
- package/dist/security/index.js +114 -2202
- package/dist/session-catalog/index.js +36 -2103
- package/dist/session-catalog/project-server.js +42 -1435
- package/dist/skills/index.js +35 -1578
- package/dist/storage/index.js +182 -15600
- package/dist/tasking/index.js +10 -676
- package/dist/tools/index.js +52 -9589
- package/dist/types/config/autonomy.d.ts +5 -0
- package/dist/types/config/tools.d.ts +2 -2
- package/dist/types/config/ui.d.ts +1 -1
- package/dist/types/errors.d.ts +18 -0
- package/dist/types/index.js +121 -1341
- package/dist/types/provider-runner.d.ts +8 -0
- package/dist/types/session-markers.js +8 -144
- package/dist/utils/error.js +3 -4
- package/dist/utils/expect-defined.js +3 -9
- package/dist/utils/heap-watchdog.js +6 -706
- package/dist/utils/index.js +269 -5685
- package/dist/utils/sage-output-block.js +5 -42
- package/dist/wiring/proxy-rewrite.d.ts +92 -4
- package/dist/wiring/proxy-rewrite.js +20 -61
- package/dist/worktree/index.js +6 -887
- package/package.json +4 -4
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
import {
|
|
2
|
+
atomicWrite,
|
|
3
|
+
withFileLock
|
|
4
|
+
} from "./chunk-7ZHZE5EO.js";
|
|
5
|
+
import {
|
|
6
|
+
ERROR_CODES,
|
|
7
|
+
FsError,
|
|
8
|
+
WrongStackError
|
|
9
|
+
} from "./chunk-WOEZV3Q4.js";
|
|
10
|
+
import {
|
|
11
|
+
expectDefined
|
|
12
|
+
} from "./chunk-PFZO3C6H.js";
|
|
13
|
+
|
|
14
|
+
// src/storage/cloud-config-sync/sanitize.ts
|
|
15
|
+
var SAGE_TREE = {
|
|
16
|
+
enabled: true,
|
|
17
|
+
storage: { projectLocal: true, directory: true },
|
|
18
|
+
inject: {
|
|
19
|
+
turnContext: true,
|
|
20
|
+
toolResults: true,
|
|
21
|
+
taskAware: true,
|
|
22
|
+
maxHintsPerTool: true,
|
|
23
|
+
maxCharsPerTool: true,
|
|
24
|
+
minScore: true,
|
|
25
|
+
repeatCooldownMs: true,
|
|
26
|
+
maxTurnMemories: true,
|
|
27
|
+
maxCharsPerTurn: true,
|
|
28
|
+
minImportance: true,
|
|
29
|
+
relationFloor: true
|
|
30
|
+
},
|
|
31
|
+
hygiene: {
|
|
32
|
+
autoAfterSession: true,
|
|
33
|
+
autoOnFileChange: true,
|
|
34
|
+
retentionDays: true,
|
|
35
|
+
archiveLowConfidenceAfterDays: true
|
|
36
|
+
},
|
|
37
|
+
embeddings: { enabled: true }
|
|
38
|
+
};
|
|
39
|
+
var CORE_RUNTIME_TREE = {
|
|
40
|
+
maxConcurrent: true,
|
|
41
|
+
context: {
|
|
42
|
+
mode: true,
|
|
43
|
+
autoCompact: true,
|
|
44
|
+
strategy: true,
|
|
45
|
+
warnThreshold: true,
|
|
46
|
+
softThreshold: true,
|
|
47
|
+
hardThreshold: true,
|
|
48
|
+
preserveK: true,
|
|
49
|
+
eliseThreshold: true,
|
|
50
|
+
targetLoad: true
|
|
51
|
+
},
|
|
52
|
+
tools: {
|
|
53
|
+
defaultExecutionStrategy: true,
|
|
54
|
+
maxIterations: true,
|
|
55
|
+
iterationTimeoutMs: true,
|
|
56
|
+
sessionTimeoutMs: true,
|
|
57
|
+
perIterationOutputCapBytes: true,
|
|
58
|
+
autoExtendLimit: true,
|
|
59
|
+
restrictToProjectRoot: true,
|
|
60
|
+
descriptionMode: true,
|
|
61
|
+
disabledTools: true,
|
|
62
|
+
maxToolTimeoutMs: true,
|
|
63
|
+
loopDetection: {
|
|
64
|
+
mode: true,
|
|
65
|
+
steerThreshold: true,
|
|
66
|
+
cutThreshold: true,
|
|
67
|
+
windowSize: true,
|
|
68
|
+
callRepeatThreshold: true
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
log: { level: true, format: true },
|
|
72
|
+
features: {
|
|
73
|
+
mcp: true,
|
|
74
|
+
plugins: true,
|
|
75
|
+
memory: true,
|
|
76
|
+
modelsRegistry: true,
|
|
77
|
+
skills: true,
|
|
78
|
+
prompts: true,
|
|
79
|
+
tokenSavingMode: true,
|
|
80
|
+
allowOutsideProjectRoot: true,
|
|
81
|
+
developerMode: true
|
|
82
|
+
},
|
|
83
|
+
session: { auditLevel: true, sampling: { toolProgress: { sampleRate: true } } },
|
|
84
|
+
indexing: { onSessionStart: true, onEdit: true, watchExternal: true, debounceMs: true },
|
|
85
|
+
circuitBreaker: { enabled: true, autoKillResetMs: true },
|
|
86
|
+
modelRuntime: {
|
|
87
|
+
reasoning: { mode: true, effort: true, preserve: true },
|
|
88
|
+
cache: { ttl: true }
|
|
89
|
+
},
|
|
90
|
+
systemPrompt: { variant: true },
|
|
91
|
+
skills: { readClaudeSkills: true },
|
|
92
|
+
Sage: SAGE_TREE,
|
|
93
|
+
fallbackAuto: true,
|
|
94
|
+
yolo: true,
|
|
95
|
+
nextPrediction: true,
|
|
96
|
+
hints: true,
|
|
97
|
+
debugStream: true
|
|
98
|
+
};
|
|
99
|
+
var UI_PREFERENCES_TREE = {
|
|
100
|
+
autonomy: {
|
|
101
|
+
defaultMode: true,
|
|
102
|
+
autoProceedDelayMs: true,
|
|
103
|
+
autoProceedMaxIterations: true,
|
|
104
|
+
autonomyNextPrompt: true,
|
|
105
|
+
terminalTitleAnimation: true,
|
|
106
|
+
yolo: true,
|
|
107
|
+
streamFleet: true,
|
|
108
|
+
chime: true,
|
|
109
|
+
confirmExit: true,
|
|
110
|
+
mouseMode: true,
|
|
111
|
+
enhance: true,
|
|
112
|
+
enhanceDelayMs: true,
|
|
113
|
+
enhanceCountdownMs: true,
|
|
114
|
+
enhanceLanguage: true,
|
|
115
|
+
statuslineMode: true,
|
|
116
|
+
thinkingWord: true,
|
|
117
|
+
midRunSendPicker: true,
|
|
118
|
+
animationStyle: true,
|
|
119
|
+
fleetChatVerbosity: true,
|
|
120
|
+
refinerFallbackProfile: true,
|
|
121
|
+
refinerProvider: true,
|
|
122
|
+
refinerModel: true,
|
|
123
|
+
showModelReasoning: true,
|
|
124
|
+
shellBangWarningDontShowAgain: true,
|
|
125
|
+
showAgentSwarmPanel: true,
|
|
126
|
+
showSageMemoryInject: true,
|
|
127
|
+
readAdvancedMode: true,
|
|
128
|
+
panelPositions: true
|
|
129
|
+
},
|
|
130
|
+
launch: { mode: true, autonomy: true },
|
|
131
|
+
uiLocale: true
|
|
132
|
+
};
|
|
133
|
+
var MODELS_ROUTING_TREE = {
|
|
134
|
+
provider: true,
|
|
135
|
+
model: true,
|
|
136
|
+
favoriteModels: true,
|
|
137
|
+
favoriteModelsOnly: true,
|
|
138
|
+
fallbackModels: true,
|
|
139
|
+
fallbackProfiles: true,
|
|
140
|
+
modelMatrix: { "*": { fallbackProfile: true } },
|
|
141
|
+
brain: {
|
|
142
|
+
mode: true,
|
|
143
|
+
maxAutoRisk: true,
|
|
144
|
+
models: true,
|
|
145
|
+
strategy: true,
|
|
146
|
+
decisionTimeoutMs: true,
|
|
147
|
+
humanTimeoutMs: true,
|
|
148
|
+
council: {
|
|
149
|
+
enabled: true,
|
|
150
|
+
minRisk: true,
|
|
151
|
+
voters: true
|
|
152
|
+
},
|
|
153
|
+
ledger: { enabled: true, autoDenyAfterFailures: true },
|
|
154
|
+
trace: { enabled: true }
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var PROVIDERS_CATALOG_TREE = {
|
|
158
|
+
providers: {
|
|
159
|
+
"*": {
|
|
160
|
+
type: true,
|
|
161
|
+
family: true,
|
|
162
|
+
baseUrl: true,
|
|
163
|
+
envVars: true,
|
|
164
|
+
models: true,
|
|
165
|
+
activeKey: true
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
var MCP_SERVERS_TREE = {
|
|
170
|
+
mcpServers: {
|
|
171
|
+
"*": {
|
|
172
|
+
name: true,
|
|
173
|
+
transport: true,
|
|
174
|
+
command: true,
|
|
175
|
+
args: true,
|
|
176
|
+
url: true,
|
|
177
|
+
description: true,
|
|
178
|
+
permission: true,
|
|
179
|
+
enabled: true,
|
|
180
|
+
lazy: true,
|
|
181
|
+
envVars: true
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var EXTENSIONS_PLUGINS_TREE = {
|
|
186
|
+
plugins: true,
|
|
187
|
+
extensions: true
|
|
188
|
+
};
|
|
189
|
+
var CLOUD_SYNC_CONTRACT = {
|
|
190
|
+
"core.runtime": CORE_RUNTIME_TREE,
|
|
191
|
+
"ui.preferences": UI_PREFERENCES_TREE,
|
|
192
|
+
"models.routing": MODELS_ROUTING_TREE,
|
|
193
|
+
"providers.catalog": PROVIDERS_CATALOG_TREE,
|
|
194
|
+
"mcp.servers": MCP_SERVERS_TREE,
|
|
195
|
+
"extensions.plugins": EXTENSIONS_PLUGINS_TREE
|
|
196
|
+
};
|
|
197
|
+
var CLOUD_SYNC_NAMESPACES = Object.keys(CLOUD_SYNC_CONTRACT);
|
|
198
|
+
var INBOUND_DENIED_PATHS = [
|
|
199
|
+
// ── Code execution ──────────────────────────────────────────────────────
|
|
200
|
+
{
|
|
201
|
+
namespace: "mcp.servers",
|
|
202
|
+
path: "mcpServers.*.command",
|
|
203
|
+
reason: "Executable spawned for a stdio MCP server."
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
namespace: "mcp.servers",
|
|
207
|
+
path: "mcpServers.*.args",
|
|
208
|
+
reason: "Argv for that executable."
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
namespace: "mcp.servers",
|
|
212
|
+
path: "mcpServers.*.transport",
|
|
213
|
+
reason: "Switching transport to stdio selects the spawning code path."
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
namespace: "extensions.plugins",
|
|
217
|
+
path: "plugins",
|
|
218
|
+
reason: "Plugin list is resolved and `await import`ed."
|
|
219
|
+
},
|
|
220
|
+
// `extensions` is deliberately NOT denied: it is per-plugin settings read via
|
|
221
|
+
// `ConfigStore.getExtension(name)`, not a loader list, so it grants no import.
|
|
222
|
+
// Syncing it is the feature (`extensions.telegram.notifyChatId` and friends).
|
|
223
|
+
// Residual risk accepted: a plugin whose own settings include a URL can have
|
|
224
|
+
// that URL rewritten by the portal. Constrain that in the plugin's
|
|
225
|
+
// `configSchema`, which is where the loader validates it.
|
|
226
|
+
// ── Credential redirection / exfiltration ───────────────────────────────
|
|
227
|
+
{
|
|
228
|
+
namespace: "providers.catalog",
|
|
229
|
+
path: "providers.*.baseUrl",
|
|
230
|
+
reason: "Repoints the provider endpoint; reinjectLocalSecrets keeps the local apiKey, so the real key follows the redirect."
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
namespace: "providers.catalog",
|
|
234
|
+
path: "providers.*.envVars",
|
|
235
|
+
reason: "Chooses which environment variable is read for the key."
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
namespace: "providers.catalog",
|
|
239
|
+
path: "providers.*.activeKey",
|
|
240
|
+
reason: "Selects which stored key is sent."
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
namespace: "mcp.servers",
|
|
244
|
+
path: "mcpServers.*.url",
|
|
245
|
+
reason: "Remote MCP endpoint; receives whatever the transport carries."
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
namespace: "mcp.servers",
|
|
249
|
+
path: "mcpServers.*.envVars",
|
|
250
|
+
reason: "Names of environment variables forwarded to the server process."
|
|
251
|
+
},
|
|
252
|
+
// ── Operator-owned safety switches ──────────────────────────────────────
|
|
253
|
+
{
|
|
254
|
+
namespace: "mcp.servers",
|
|
255
|
+
path: "mcpServers.*.permission",
|
|
256
|
+
reason: "Approval requirement for that server\u2019s tools."
|
|
257
|
+
},
|
|
258
|
+
{ namespace: "core.runtime", path: "yolo", reason: "Disables every permission prompt." },
|
|
259
|
+
{
|
|
260
|
+
namespace: "core.runtime",
|
|
261
|
+
path: "features.allowOutsideProjectRoot",
|
|
262
|
+
reason: "Short-circuits project-root containment and the symlink realpath check."
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
namespace: "core.runtime",
|
|
266
|
+
path: "tools.restrictToProjectRoot",
|
|
267
|
+
reason: "The other half of the filesystem confinement switch."
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
namespace: "core.runtime",
|
|
271
|
+
path: "features.developerMode",
|
|
272
|
+
reason: "Loosens guardrails; an operator opt-in, not a synced preference."
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
namespace: "core.runtime",
|
|
276
|
+
path: "tools.disabledTools",
|
|
277
|
+
reason: "Could re-enable a tool the operator deliberately switched off."
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
namespace: "ui.preferences",
|
|
281
|
+
path: "autonomy.defaultMode",
|
|
282
|
+
reason: "Autonomy is user-owned, never remote-owned."
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
namespace: "ui.preferences",
|
|
286
|
+
path: "autonomy.yolo",
|
|
287
|
+
reason: "Alias for the denied top-level `yolo`, and it wins over the user setting."
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
namespace: "ui.preferences",
|
|
291
|
+
path: "launch.autonomy",
|
|
292
|
+
reason: "Launch-time autonomy mode; same user-owned boundary."
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
namespace: "models.routing",
|
|
296
|
+
path: "brain.mode",
|
|
297
|
+
reason: "Selects the policy/LLM/human decision ladder."
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
namespace: "models.routing",
|
|
301
|
+
path: "brain.maxAutoRisk",
|
|
302
|
+
reason: "The risk ceiling below which actions proceed without asking."
|
|
303
|
+
}
|
|
304
|
+
];
|
|
305
|
+
function contractHasPath(tree, segments) {
|
|
306
|
+
if (segments.length === 0) return true;
|
|
307
|
+
if (tree === true) return false;
|
|
308
|
+
const [head, ...rest] = segments;
|
|
309
|
+
if (head === void 0 || !Object.hasOwn(tree, head)) return false;
|
|
310
|
+
const child = tree[head];
|
|
311
|
+
return child === void 0 ? false : contractHasPath(child, rest);
|
|
312
|
+
}
|
|
313
|
+
function pruneContractPath(tree, segments) {
|
|
314
|
+
if (tree === true || segments.length === 0) return tree;
|
|
315
|
+
const [head, ...rest] = segments;
|
|
316
|
+
if (head === void 0 || !Object.hasOwn(tree, head)) return tree;
|
|
317
|
+
const next = { ...tree };
|
|
318
|
+
if (rest.length === 0) {
|
|
319
|
+
delete next[head];
|
|
320
|
+
return next;
|
|
321
|
+
}
|
|
322
|
+
const child = next[head];
|
|
323
|
+
if (child === void 0) return tree;
|
|
324
|
+
next[head] = pruneContractPath(child, rest);
|
|
325
|
+
return next;
|
|
326
|
+
}
|
|
327
|
+
function assertInboundDenyListResolves() {
|
|
328
|
+
const unresolved = INBOUND_DENIED_PATHS.filter((entry) => {
|
|
329
|
+
const tree = CLOUD_SYNC_CONTRACT[entry.namespace];
|
|
330
|
+
return tree === void 0 || !contractHasPath(tree, entry.path.split("."));
|
|
331
|
+
});
|
|
332
|
+
if (unresolved.length > 0) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
"INBOUND_DENIED_PATHS entr(ies) no longer resolve against CLOUD_SYNC_CONTRACT \u2014 a rename would silently re-open them: " + unresolved.map((entry) => `${entry.namespace}:${entry.path}`).join(", ")
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
var INBOUND_CONTRACT = (() => {
|
|
339
|
+
assertInboundDenyListResolves();
|
|
340
|
+
const out = { ...CLOUD_SYNC_CONTRACT };
|
|
341
|
+
for (const entry of INBOUND_DENIED_PATHS) {
|
|
342
|
+
const tree = out[entry.namespace];
|
|
343
|
+
if (tree === void 0) continue;
|
|
344
|
+
out[entry.namespace] = pruneContractPath(tree, entry.path.split("."));
|
|
345
|
+
}
|
|
346
|
+
return out;
|
|
347
|
+
})();
|
|
348
|
+
var NAMESPACE_SCHEMA_VERSIONS = {
|
|
349
|
+
"core.runtime": 1,
|
|
350
|
+
"ui.preferences": 1,
|
|
351
|
+
"models.routing": 1,
|
|
352
|
+
"providers.catalog": 1,
|
|
353
|
+
"mcp.servers": 2,
|
|
354
|
+
"extensions.plugins": 1
|
|
355
|
+
};
|
|
356
|
+
var LOCAL_ONLY_TOP_LEVEL = [
|
|
357
|
+
"version",
|
|
358
|
+
"configScope",
|
|
359
|
+
"hq",
|
|
360
|
+
"cloudSync",
|
|
361
|
+
"sync",
|
|
362
|
+
"models",
|
|
363
|
+
"apiKey",
|
|
364
|
+
"baseUrl"
|
|
365
|
+
];
|
|
366
|
+
var ENCRYPTED_VALUE = /enc:v\d+:/;
|
|
367
|
+
var SECRET_KEY = /^(api[-_]?key|apikeys|.*token|secret.*|.*secret|password|passwd|credential.*|private[-_]?key|authorization)$/i;
|
|
368
|
+
function isPlainObject(value) {
|
|
369
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
370
|
+
}
|
|
371
|
+
function stripSecretMaterial(value) {
|
|
372
|
+
if (typeof value === "string") {
|
|
373
|
+
return ENCRYPTED_VALUE.test(value) ? void 0 : value;
|
|
374
|
+
}
|
|
375
|
+
if (Array.isArray(value)) {
|
|
376
|
+
const cleaned = value.map((entry) => stripSecretMaterial(entry));
|
|
377
|
+
return cleaned.filter((entry) => entry !== void 0);
|
|
378
|
+
}
|
|
379
|
+
if (isPlainObject(value)) {
|
|
380
|
+
const out = {};
|
|
381
|
+
for (const [key, child] of Object.entries(value)) {
|
|
382
|
+
if (SECRET_KEY.test(key)) continue;
|
|
383
|
+
const cleaned = stripSecretMaterial(child);
|
|
384
|
+
if (cleaned !== void 0) out[key] = cleaned;
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
return value;
|
|
389
|
+
}
|
|
390
|
+
function project(value, tree) {
|
|
391
|
+
if (tree === true) return value;
|
|
392
|
+
if (!isPlainObject(value)) return void 0;
|
|
393
|
+
const wildcard = tree["*"];
|
|
394
|
+
const out = {};
|
|
395
|
+
for (const [key, child] of Object.entries(value)) {
|
|
396
|
+
const node = Object.hasOwn(tree, key) && key !== "*" ? tree[key] : wildcard;
|
|
397
|
+
if (node === void 0) continue;
|
|
398
|
+
const projected = project(child, node);
|
|
399
|
+
if (projected !== void 0) out[key] = projected;
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
function buildNamespacePayloads(config) {
|
|
404
|
+
const payloads = {};
|
|
405
|
+
for (const [namespace, tree] of Object.entries(CLOUD_SYNC_CONTRACT)) {
|
|
406
|
+
if (tree === true) continue;
|
|
407
|
+
const payload = {};
|
|
408
|
+
for (const [key, node] of Object.entries(tree)) {
|
|
409
|
+
if (!(key in config) || config[key] === void 0) continue;
|
|
410
|
+
const projected = project(config[key], node);
|
|
411
|
+
if (projected === void 0) continue;
|
|
412
|
+
const cleaned = stripSecretMaterial(projected);
|
|
413
|
+
if (cleaned === void 0) continue;
|
|
414
|
+
if (isPlainObject(cleaned) && Object.keys(cleaned).length === 0 && node !== true) continue;
|
|
415
|
+
payload[key] = cleaned;
|
|
416
|
+
}
|
|
417
|
+
if (Object.keys(payload).length > 0) payloads[namespace] = payload;
|
|
418
|
+
}
|
|
419
|
+
return payloads;
|
|
420
|
+
}
|
|
421
|
+
function reinjectLocalSecrets(local, incoming) {
|
|
422
|
+
if (!isPlainObject(local) || !isPlainObject(incoming)) return incoming;
|
|
423
|
+
const out = { ...incoming };
|
|
424
|
+
for (const [key, value] of Object.entries(local)) {
|
|
425
|
+
const secretish = SECRET_KEY.test(key) || typeof value === "string" && ENCRYPTED_VALUE.test(value);
|
|
426
|
+
if (secretish && !(key in incoming)) {
|
|
427
|
+
out[key] = value;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (isPlainObject(value) && isPlainObject(incoming[key])) {
|
|
431
|
+
out[key] = reinjectLocalSecrets(value, incoming[key]);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return out;
|
|
435
|
+
}
|
|
436
|
+
function mergeAtContract(local, incoming, tree) {
|
|
437
|
+
if (tree === true) return reinjectLocalSecrets(local, incoming);
|
|
438
|
+
if (!isPlainObject(incoming)) return local;
|
|
439
|
+
const base = isPlainObject(local) ? { ...local } : {};
|
|
440
|
+
const wildcard = tree["*"];
|
|
441
|
+
for (const [key, child] of Object.entries(incoming)) {
|
|
442
|
+
const node = Object.hasOwn(tree, key) && key !== "*" ? tree[key] : wildcard;
|
|
443
|
+
if (node === void 0) continue;
|
|
444
|
+
base[key] = mergeAtContract(base[key], child, node);
|
|
445
|
+
}
|
|
446
|
+
return base;
|
|
447
|
+
}
|
|
448
|
+
function applyNamespacePayload(config, namespace, payload) {
|
|
449
|
+
const tree = INBOUND_CONTRACT[namespace];
|
|
450
|
+
if (!tree || tree === true) return config;
|
|
451
|
+
const next = { ...config };
|
|
452
|
+
for (const [key, incoming] of Object.entries(payload)) {
|
|
453
|
+
const node = Object.hasOwn(tree, key) ? tree[key] : void 0;
|
|
454
|
+
if (node === void 0) continue;
|
|
455
|
+
next[key] = mergeAtContract(next[key], incoming, node);
|
|
456
|
+
}
|
|
457
|
+
return next;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/storage/cloud-config-sync.ts
|
|
461
|
+
import { createHash, randomUUID as nodeRandomUUID } from "node:crypto";
|
|
462
|
+
import * as fs from "node:fs/promises";
|
|
463
|
+
import * as path from "node:path";
|
|
464
|
+
var EMPTY_STATE = {
|
|
465
|
+
version: 1,
|
|
466
|
+
cursor: 0,
|
|
467
|
+
namespaceRevisions: {},
|
|
468
|
+
syncedHashes: {},
|
|
469
|
+
syncedPayloads: {},
|
|
470
|
+
pendingMutations: {},
|
|
471
|
+
rejectedHashes: {}
|
|
472
|
+
};
|
|
473
|
+
function stableStringify(value) {
|
|
474
|
+
if (Array.isArray(value)) {
|
|
475
|
+
return `[${value.map((entry) => stableStringify(entry)).join(",")}]`;
|
|
476
|
+
}
|
|
477
|
+
if (typeof value === "object" && value !== null) {
|
|
478
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`);
|
|
479
|
+
return `{${entries.join(",")}}`;
|
|
480
|
+
}
|
|
481
|
+
return JSON.stringify(value) ?? "null";
|
|
482
|
+
}
|
|
483
|
+
function hashPayload(payload) {
|
|
484
|
+
return createHash("sha256").update(stableStringify(payload)).digest("hex");
|
|
485
|
+
}
|
|
486
|
+
function deepEquals(a, b) {
|
|
487
|
+
return stableStringify(a) === stableStringify(b);
|
|
488
|
+
}
|
|
489
|
+
var CloudConfigSync = class {
|
|
490
|
+
constructor(deps) {
|
|
491
|
+
this.deps = deps;
|
|
492
|
+
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
493
|
+
this.uuid = deps.randomUUID ?? (() => nodeRandomUUID());
|
|
494
|
+
this.timeoutMs = deps.requestTimeoutMs ?? 15e3;
|
|
495
|
+
}
|
|
496
|
+
deps;
|
|
497
|
+
state = null;
|
|
498
|
+
fetchImpl;
|
|
499
|
+
uuid;
|
|
500
|
+
timeoutMs;
|
|
501
|
+
/** One full pull-then-push-then-heartbeat pass. Never throws; failures are summarized. */
|
|
502
|
+
async syncOnce() {
|
|
503
|
+
const settings = this.deps.getSettings();
|
|
504
|
+
if (!settings?.enabled || !settings.url || !settings.token) {
|
|
505
|
+
return summary({ skipped: true, message: "Cloud sync is not configured." });
|
|
506
|
+
}
|
|
507
|
+
const state = await this.loadState();
|
|
508
|
+
const pulled = [];
|
|
509
|
+
const pushed = [];
|
|
510
|
+
const conflicted = [];
|
|
511
|
+
const rejected = [];
|
|
512
|
+
try {
|
|
513
|
+
await this.pullChanges(settings, state, pulled);
|
|
514
|
+
await this.pushDirty(settings, state, { pushed, conflicted, rejected });
|
|
515
|
+
await this.heartbeat(settings, state);
|
|
516
|
+
state.lastSyncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
517
|
+
await this.saveState(state);
|
|
518
|
+
return summary({
|
|
519
|
+
ok: true,
|
|
520
|
+
pulled,
|
|
521
|
+
pushed,
|
|
522
|
+
conflicted,
|
|
523
|
+
rejected,
|
|
524
|
+
message: pulled.length + pushed.length === 0 ? "Already in sync." : `Pulled ${pulled.length}, pushed ${pushed.length} namespace(s).`
|
|
525
|
+
});
|
|
526
|
+
} catch (error) {
|
|
527
|
+
await this.saveState(state).catch(() => {
|
|
528
|
+
});
|
|
529
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
530
|
+
this.deps.logger?.warn(`cloud-config-sync: pass failed: ${message}`);
|
|
531
|
+
return summary({ pulled, pushed, conflicted, rejected, message });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/** Human-readable status for the slash command. */
|
|
535
|
+
async statusText() {
|
|
536
|
+
const settings = this.deps.getSettings();
|
|
537
|
+
if (!settings?.enabled) {
|
|
538
|
+
return "Cloud config sync: disabled. Run `/cloudsync on` after setting url + token.";
|
|
539
|
+
}
|
|
540
|
+
const state = await this.loadState();
|
|
541
|
+
const dirty = await this.dirtyNamespaces(state);
|
|
542
|
+
const rejectedList = Object.keys(state.rejectedHashes);
|
|
543
|
+
const lines = [
|
|
544
|
+
"Cloud config sync: enabled",
|
|
545
|
+
` url: ${settings.url}`,
|
|
546
|
+
` cursor: ${state.cursor}`,
|
|
547
|
+
` last sync: ${state.lastSyncedAt ?? "never"}`,
|
|
548
|
+
` dirty: ${dirty.length > 0 ? dirty.join(", ") : "none"}`
|
|
549
|
+
];
|
|
550
|
+
if (rejectedList.length > 0) {
|
|
551
|
+
lines.push(` rejected: ${rejectedList.join(", ")} (kept local; edit to retry)`);
|
|
552
|
+
}
|
|
553
|
+
return lines.join("\n");
|
|
554
|
+
}
|
|
555
|
+
// ── Pull ────────────────────────────────────────────────────────────────
|
|
556
|
+
async pullChanges(settings, state, pulled) {
|
|
557
|
+
const delta = (await this.request(settings, "GET", `/api/sync/v1/config?since=${state.cursor}`)).json;
|
|
558
|
+
const toRead = delta.full ? CLOUD_SYNC_NAMESPACES.map((namespace) => ({ namespace })) : delta.changed.filter(
|
|
559
|
+
(entry) => CLOUD_SYNC_NAMESPACES.includes(entry.namespace) && entry.revision > (state.namespaceRevisions[entry.namespace] ?? 0)
|
|
560
|
+
);
|
|
561
|
+
const dirtyBefore = new Set(await this.dirtyNamespaces(state));
|
|
562
|
+
for (const entry of toRead) {
|
|
563
|
+
const snapshot = (await this.request(
|
|
564
|
+
settings,
|
|
565
|
+
"GET",
|
|
566
|
+
`/api/sync/v1/config/${encodeURIComponent(entry.namespace)}`
|
|
567
|
+
)).json;
|
|
568
|
+
if ((state.namespaceRevisions[entry.namespace] ?? 0) >= snapshot.revision) continue;
|
|
569
|
+
if (snapshot.revision === 0) {
|
|
570
|
+
state.namespaceRevisions[entry.namespace] = 0;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (dirtyBefore.has(entry.namespace)) {
|
|
574
|
+
const local = await this.deps.readLocalConfig();
|
|
575
|
+
const localPayload = buildNamespacePayloads(local)[entry.namespace] ?? {};
|
|
576
|
+
const base = state.syncedPayloads[entry.namespace] ?? {};
|
|
577
|
+
const merged = { ...snapshot.payload };
|
|
578
|
+
for (const [key, localValue] of Object.entries(localPayload)) {
|
|
579
|
+
if (!deepEquals(localValue, base[key])) merged[key] = localValue;
|
|
580
|
+
}
|
|
581
|
+
await this.deps.writeLocalConfig(
|
|
582
|
+
(config) => applyNamespacePayload(config, entry.namespace, merged)
|
|
583
|
+
);
|
|
584
|
+
state.namespaceRevisions[entry.namespace] = snapshot.revision;
|
|
585
|
+
state.syncedPayloads[entry.namespace] = snapshot.payload;
|
|
586
|
+
} else {
|
|
587
|
+
await this.applySnapshot(entry.namespace, snapshot, state);
|
|
588
|
+
}
|
|
589
|
+
pulled.push(entry.namespace);
|
|
590
|
+
}
|
|
591
|
+
state.cursor = Math.max(state.cursor, delta.cursor);
|
|
592
|
+
}
|
|
593
|
+
async applySnapshot(namespace, snapshot, state) {
|
|
594
|
+
await this.deps.writeLocalConfig(
|
|
595
|
+
(config) => applyNamespacePayload(config, namespace, snapshot.payload)
|
|
596
|
+
);
|
|
597
|
+
state.namespaceRevisions[namespace] = snapshot.revision;
|
|
598
|
+
state.syncedPayloads[namespace] = snapshot.payload;
|
|
599
|
+
const local = await this.deps.readLocalConfig();
|
|
600
|
+
const projected = buildNamespacePayloads(local)[namespace] ?? {};
|
|
601
|
+
state.syncedHashes[namespace] = hashPayload(projected);
|
|
602
|
+
delete state.pendingMutations[namespace];
|
|
603
|
+
delete state.rejectedHashes[namespace];
|
|
604
|
+
}
|
|
605
|
+
// ── Push ────────────────────────────────────────────────────────────────
|
|
606
|
+
async dirtyNamespaces(state) {
|
|
607
|
+
const local = await this.deps.readLocalConfig();
|
|
608
|
+
const payloads = buildNamespacePayloads(local);
|
|
609
|
+
return Object.entries(payloads).filter(([namespace, payload]) => hashPayload(payload) !== state.syncedHashes[namespace]).map(([namespace]) => namespace);
|
|
610
|
+
}
|
|
611
|
+
async pushDirty(settings, state, out) {
|
|
612
|
+
const local = await this.deps.readLocalConfig();
|
|
613
|
+
const payloads = buildNamespacePayloads(local);
|
|
614
|
+
for (const [namespace, payload] of Object.entries(payloads)) {
|
|
615
|
+
const hash = hashPayload(payload);
|
|
616
|
+
if (hash === state.syncedHashes[namespace]) continue;
|
|
617
|
+
if (state.rejectedHashes[namespace] === hash) continue;
|
|
618
|
+
const pending = state.pendingMutations[namespace];
|
|
619
|
+
const mutationId = pending?.hash === hash ? pending.mutationId : this.uuid();
|
|
620
|
+
state.pendingMutations[namespace] = { mutationId, hash };
|
|
621
|
+
await this.saveState(state);
|
|
622
|
+
const result = await this.putNamespace(settings, state, namespace, payload, mutationId);
|
|
623
|
+
if (result.kind === "applied") {
|
|
624
|
+
out.pushed.push(namespace);
|
|
625
|
+
} else if (result.kind === "conflict") {
|
|
626
|
+
out.conflicted.push(namespace);
|
|
627
|
+
await this.resolveConflict(settings, state, namespace, payload, result.server, out);
|
|
628
|
+
} else {
|
|
629
|
+
state.rejectedHashes[namespace] = hash;
|
|
630
|
+
delete state.pendingMutations[namespace];
|
|
631
|
+
out.rejected.push(namespace);
|
|
632
|
+
this.deps.logger?.warn(
|
|
633
|
+
`cloud-config-sync: server rejected ${namespace} (${result.code}); keeping it local until the config changes.`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async putNamespace(settings, state, namespace, payload, mutationId) {
|
|
639
|
+
const response = await this.request(
|
|
640
|
+
settings,
|
|
641
|
+
"PUT",
|
|
642
|
+
`/api/sync/v1/config/${encodeURIComponent(namespace)}`,
|
|
643
|
+
{
|
|
644
|
+
baseRevision: state.namespaceRevisions[namespace] ?? 0,
|
|
645
|
+
mutationId,
|
|
646
|
+
schemaVersion: NAMESPACE_SCHEMA_VERSIONS[namespace] ?? 1,
|
|
647
|
+
payload
|
|
648
|
+
},
|
|
649
|
+
// 409 (conflict) and 422 (contract mismatch) are protocol answers, not failures.
|
|
650
|
+
[409, 422]
|
|
651
|
+
);
|
|
652
|
+
if (response.status === 422) {
|
|
653
|
+
const code = response.json.error?.code;
|
|
654
|
+
return { kind: "rejected", code: code ?? "invalid_payload" };
|
|
655
|
+
}
|
|
656
|
+
const body = response.json;
|
|
657
|
+
if (response.status === 409) {
|
|
658
|
+
return { kind: "conflict", server: body };
|
|
659
|
+
}
|
|
660
|
+
state.namespaceRevisions[namespace] = body.revision;
|
|
661
|
+
state.syncedHashes[namespace] = hashPayload(payload);
|
|
662
|
+
state.syncedPayloads[namespace] = payload;
|
|
663
|
+
if (typeof body.cursor === "number") state.cursor = Math.max(state.cursor, body.cursor);
|
|
664
|
+
delete state.pendingMutations[namespace];
|
|
665
|
+
delete state.rejectedHashes[namespace];
|
|
666
|
+
return { kind: "applied" };
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Three-way merge at owned-top-level-key granularity: keys unchanged locally since the
|
|
670
|
+
* last sync adopt the server value; locally-changed keys win and are re-pushed on top
|
|
671
|
+
* of the server revision with a fresh mutation id.
|
|
672
|
+
*/
|
|
673
|
+
async resolveConflict(settings, state, namespace, localPayload, server, out) {
|
|
674
|
+
const base = state.syncedPayloads[namespace] ?? {};
|
|
675
|
+
const merged = { ...server.payload };
|
|
676
|
+
for (const [key, localValue] of Object.entries(localPayload)) {
|
|
677
|
+
if (!deepEquals(localValue, base[key])) merged[key] = localValue;
|
|
678
|
+
}
|
|
679
|
+
await this.deps.writeLocalConfig((config) => applyNamespacePayload(config, namespace, merged));
|
|
680
|
+
state.namespaceRevisions[namespace] = server.revision;
|
|
681
|
+
const mutationId = this.uuid();
|
|
682
|
+
const hash = hashPayload(merged);
|
|
683
|
+
state.pendingMutations[namespace] = { mutationId, hash };
|
|
684
|
+
await this.saveState(state);
|
|
685
|
+
const result = await this.putNamespace(settings, state, namespace, merged, mutationId);
|
|
686
|
+
if (result.kind === "applied") {
|
|
687
|
+
out.pushed.push(namespace);
|
|
688
|
+
} else {
|
|
689
|
+
this.deps.logger?.warn(
|
|
690
|
+
`cloud-config-sync: conflict resolution for ${namespace} did not converge (${result.kind}); will retry next pass.`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
// ── Heartbeat ───────────────────────────────────────────────────────────
|
|
695
|
+
async heartbeat(settings, state) {
|
|
696
|
+
await this.request(settings, "POST", "/api/sync/v1/heartbeat", {
|
|
697
|
+
transport: "poll",
|
|
698
|
+
syncCursor: state.cursor,
|
|
699
|
+
namespaceRevisions: state.namespaceRevisions
|
|
700
|
+
}).catch((error) => {
|
|
701
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
702
|
+
this.deps.logger?.warn(`cloud-config-sync: heartbeat failed: ${message}`);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
// ── HTTP + state plumbing ───────────────────────────────────────────────
|
|
706
|
+
async request(settings, method, pathname, body, acceptStatuses = []) {
|
|
707
|
+
const url = `${settings.url.replace(/\/+$/, "")}${pathname}`;
|
|
708
|
+
const headers = {
|
|
709
|
+
Authorization: `Bearer ${settings.token}`,
|
|
710
|
+
"X-WrongStack-Version": this.deps.appVersion ?? "unknown"
|
|
711
|
+
};
|
|
712
|
+
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
713
|
+
const init = {
|
|
714
|
+
method,
|
|
715
|
+
headers,
|
|
716
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
717
|
+
};
|
|
718
|
+
if (body !== void 0) init.body = JSON.stringify(body);
|
|
719
|
+
const response = await this.fetchImpl(url, init);
|
|
720
|
+
const ok = response.ok || acceptStatuses.includes(response.status);
|
|
721
|
+
if (!ok) {
|
|
722
|
+
if (response.status === 401) {
|
|
723
|
+
throw new Error("unauthorized: the machine token is invalid, expired, or revoked.");
|
|
724
|
+
}
|
|
725
|
+
if (response.status === 429) {
|
|
726
|
+
throw new Error("rate limited by the portal; backing off until the next pass.");
|
|
727
|
+
}
|
|
728
|
+
throw new Error(`${method} ${pathname} failed with HTTP ${response.status}.`);
|
|
729
|
+
}
|
|
730
|
+
const json = response.status === 204 ? null : await response.json().catch(() => null);
|
|
731
|
+
return { status: response.status, json };
|
|
732
|
+
}
|
|
733
|
+
async loadState() {
|
|
734
|
+
if (this.state) return this.state;
|
|
735
|
+
try {
|
|
736
|
+
const raw = await fs.readFile(this.deps.statePath, "utf8");
|
|
737
|
+
const parsed = JSON.parse(raw);
|
|
738
|
+
this.state = {
|
|
739
|
+
...structuredClone(EMPTY_STATE),
|
|
740
|
+
...parsed,
|
|
741
|
+
version: 1
|
|
742
|
+
};
|
|
743
|
+
} catch {
|
|
744
|
+
this.state = structuredClone(EMPTY_STATE);
|
|
745
|
+
}
|
|
746
|
+
return this.state;
|
|
747
|
+
}
|
|
748
|
+
async saveState(state) {
|
|
749
|
+
this.state = state;
|
|
750
|
+
await fs.mkdir(path.dirname(this.deps.statePath), { recursive: true });
|
|
751
|
+
await atomicWrite(this.deps.statePath, `${JSON.stringify(state, null, 2)}
|
|
752
|
+
`, {
|
|
753
|
+
mode: 384
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
function summary(partial) {
|
|
758
|
+
return {
|
|
759
|
+
ok: partial.ok ?? false,
|
|
760
|
+
skipped: partial.skipped ?? false,
|
|
761
|
+
pulled: partial.pulled ?? [],
|
|
762
|
+
pushed: partial.pushed ?? [],
|
|
763
|
+
conflicted: partial.conflicted ?? [],
|
|
764
|
+
rejected: partial.rejected ?? [],
|
|
765
|
+
message: partial.message
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// src/storage/prompt-usage-store.ts
|
|
770
|
+
import * as fs2 from "node:fs/promises";
|
|
771
|
+
var PromptUsageStore = class {
|
|
772
|
+
constructor(file) {
|
|
773
|
+
this.file = file;
|
|
774
|
+
}
|
|
775
|
+
file;
|
|
776
|
+
cachedUsage = null;
|
|
777
|
+
cachedSignature = null;
|
|
778
|
+
pendingRecords = [];
|
|
779
|
+
drainScheduled = false;
|
|
780
|
+
draining = false;
|
|
781
|
+
async load() {
|
|
782
|
+
return cloneUsage(await this.loadInternal());
|
|
783
|
+
}
|
|
784
|
+
async loadInternal() {
|
|
785
|
+
const signature = await this.fileSignature();
|
|
786
|
+
if (this.cachedUsage && sameSignature(signature, this.cachedSignature)) {
|
|
787
|
+
return this.cachedUsage;
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
const raw = JSON.parse(await fs2.readFile(this.file, "utf8"));
|
|
791
|
+
if (raw && typeof raw === "object" && raw.usage && typeof raw.usage === "object") {
|
|
792
|
+
this.cachedUsage = raw.usage;
|
|
793
|
+
this.cachedSignature = signature;
|
|
794
|
+
return this.cachedUsage;
|
|
795
|
+
}
|
|
796
|
+
} catch {
|
|
797
|
+
}
|
|
798
|
+
this.cachedUsage = {};
|
|
799
|
+
this.cachedSignature = signature;
|
|
800
|
+
return this.cachedUsage;
|
|
801
|
+
}
|
|
802
|
+
async record(slug, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
803
|
+
return await new Promise((resolve2, reject) => {
|
|
804
|
+
this.pendingRecords.push({ slug, at, resolve: resolve2, reject });
|
|
805
|
+
this.scheduleDrain();
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
async get(slug) {
|
|
809
|
+
return (await this.load())[slug];
|
|
810
|
+
}
|
|
811
|
+
/** Slugs ordered by most-recently-used (then by count), capped at `limit`. */
|
|
812
|
+
async recent(limit = 15) {
|
|
813
|
+
const usage = await this.load();
|
|
814
|
+
return Object.entries(usage).map(([slug, u]) => ({ slug, usage: u })).sort(
|
|
815
|
+
(a, b) => new Date(b.usage.lastUsedAt).getTime() - new Date(a.usage.lastUsedAt).getTime() || b.usage.count - a.usage.count
|
|
816
|
+
).slice(0, limit);
|
|
817
|
+
}
|
|
818
|
+
/** Slugs ordered by usage count (then recency), capped at `limit`. */
|
|
819
|
+
async top(limit = 15) {
|
|
820
|
+
const usage = await this.load();
|
|
821
|
+
return Object.entries(usage).map(([slug, u]) => ({ slug, usage: u })).sort(
|
|
822
|
+
(a, b) => b.usage.count - a.usage.count || new Date(b.usage.lastUsedAt).getTime() - new Date(a.usage.lastUsedAt).getTime()
|
|
823
|
+
).slice(0, limit);
|
|
824
|
+
}
|
|
825
|
+
scheduleDrain() {
|
|
826
|
+
if (this.drainScheduled || this.draining) return;
|
|
827
|
+
this.drainScheduled = true;
|
|
828
|
+
queueMicrotask(() => {
|
|
829
|
+
this.drainScheduled = false;
|
|
830
|
+
void this.drainPending();
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
async drainPending() {
|
|
834
|
+
if (this.draining || this.pendingRecords.length === 0) return;
|
|
835
|
+
this.draining = true;
|
|
836
|
+
const batch = this.pendingRecords.splice(0);
|
|
837
|
+
try {
|
|
838
|
+
const results = await withFileLock(this.file, async () => {
|
|
839
|
+
const usage = { ...await this.loadInternal() };
|
|
840
|
+
const nextValues = [];
|
|
841
|
+
for (const item of batch) {
|
|
842
|
+
const previous = usage[item.slug];
|
|
843
|
+
const next = {
|
|
844
|
+
count: (previous?.count ?? 0) + 1,
|
|
845
|
+
lastUsedAt: item.at
|
|
846
|
+
};
|
|
847
|
+
usage[item.slug] = next;
|
|
848
|
+
nextValues.push(next);
|
|
849
|
+
}
|
|
850
|
+
await atomicWrite(
|
|
851
|
+
this.file,
|
|
852
|
+
JSON.stringify({ version: 1, usage }, null, 2)
|
|
853
|
+
);
|
|
854
|
+
this.cachedUsage = usage;
|
|
855
|
+
this.cachedSignature = await this.fileSignature();
|
|
856
|
+
return nextValues;
|
|
857
|
+
});
|
|
858
|
+
for (let index = 0; index < batch.length; index++) {
|
|
859
|
+
batch[index].resolve(results[index]);
|
|
860
|
+
}
|
|
861
|
+
} catch (error) {
|
|
862
|
+
for (const item of batch) item.reject(error);
|
|
863
|
+
} finally {
|
|
864
|
+
this.draining = false;
|
|
865
|
+
if (this.pendingRecords.length > 0) this.scheduleDrain();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async fileSignature() {
|
|
869
|
+
try {
|
|
870
|
+
const stat2 = await fs2.stat(this.file);
|
|
871
|
+
return { size: stat2.size, mtimeMs: stat2.mtimeMs, ctimeMs: stat2.ctimeMs };
|
|
872
|
+
} catch {
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
function sameSignature(a, b) {
|
|
878
|
+
if (a === null || b === null) return a === b;
|
|
879
|
+
return a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
|
|
880
|
+
}
|
|
881
|
+
function cloneUsage(usage) {
|
|
882
|
+
return Object.fromEntries(Object.entries(usage).map(([slug, value]) => [slug, { ...value }]));
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/storage/cloud-sync.ts
|
|
886
|
+
import * as fs3 from "node:fs/promises";
|
|
887
|
+
import * as path2 from "node:path";
|
|
888
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
889
|
+
var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
|
|
890
|
+
var CloudSync = class {
|
|
891
|
+
constructor(paths, getConfig, setConfig, getSettingsConfigPath) {
|
|
892
|
+
this.paths = paths;
|
|
893
|
+
this.getConfig = getConfig;
|
|
894
|
+
this.setConfig = setConfig;
|
|
895
|
+
this.statePath = path2.join(paths.configDir, "sync-state.json");
|
|
896
|
+
this.getSettingsConfigPath = getSettingsConfigPath ?? (() => path2.join(this.paths.configDir, "config.json"));
|
|
897
|
+
}
|
|
898
|
+
paths;
|
|
899
|
+
getConfig;
|
|
900
|
+
setConfig;
|
|
901
|
+
statePath;
|
|
902
|
+
state = null;
|
|
903
|
+
getSettingsConfigPath;
|
|
904
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
905
|
+
async status() {
|
|
906
|
+
const cfg = this.getConfig();
|
|
907
|
+
if (!cfg?.enabled) {
|
|
908
|
+
return "CloudSync: disabled. Run `/sync enable` to activate.";
|
|
909
|
+
}
|
|
910
|
+
const last = this.state?.lastSyncedAt;
|
|
911
|
+
const since = last ? timeAgo(last) : "never";
|
|
912
|
+
return [
|
|
913
|
+
`CloudSync: enabled`,
|
|
914
|
+
` repo: ${cfg.repo}`,
|
|
915
|
+
` categories: ${cfg.categories.join(", ")}`,
|
|
916
|
+
` last sync: ${since}`
|
|
917
|
+
].join("\n");
|
|
918
|
+
}
|
|
919
|
+
async enable(_repo, _categories) {
|
|
920
|
+
return "Enable via /sync enable.";
|
|
921
|
+
}
|
|
922
|
+
async disable() {
|
|
923
|
+
const cfg = this.getConfig();
|
|
924
|
+
if (!cfg) return "CloudSync is not configured.";
|
|
925
|
+
const next = { ...cfg, enabled: false };
|
|
926
|
+
await this.setConfig(next);
|
|
927
|
+
return "CloudSync disabled. Local data kept.";
|
|
928
|
+
}
|
|
929
|
+
async push(token) {
|
|
930
|
+
const cfg = this.getConfig();
|
|
931
|
+
if (!cfg?.enabled) return { ok: false, action: "push", categories: [], message: "Not enabled." };
|
|
932
|
+
const parts = cfg.repo.split("/");
|
|
933
|
+
const owner = expectDefined(parts[0]);
|
|
934
|
+
const repoName = expectDefined(parts[1]);
|
|
935
|
+
const branch = "main";
|
|
936
|
+
const remoteHead = await this.getOptionalRef(token, owner, repoName, branch);
|
|
937
|
+
let parentCommitSha = remoteHead?.object?.sha;
|
|
938
|
+
let baseTreeSha;
|
|
939
|
+
let remoteEntries = [];
|
|
940
|
+
if (parentCommitSha) {
|
|
941
|
+
baseTreeSha = parentCommitSha === this.state?.sha && this.state.treeSha ? this.state.treeSha : (await this.getCommit(token, owner, repoName, parentCommitSha)).tree.sha;
|
|
942
|
+
remoteEntries = await this.getTreeEntries(token, owner, repoName, baseTreeSha);
|
|
943
|
+
}
|
|
944
|
+
const { treeEntries: localEntries, rev } = await this.buildLocalTree(cfg.categories);
|
|
945
|
+
let treeEntries = this.buildPushTree(localEntries, remoteEntries, cfg.categories);
|
|
946
|
+
let newTreeSha = await this.createGitTree(token, owner, repoName, treeEntries, baseTreeSha);
|
|
947
|
+
let commitSha = await this.createCommit(
|
|
948
|
+
token,
|
|
949
|
+
owner,
|
|
950
|
+
repoName,
|
|
951
|
+
newTreeSha,
|
|
952
|
+
parentCommitSha,
|
|
953
|
+
`Sync ${cfg.categories.join(", ")} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
954
|
+
);
|
|
955
|
+
try {
|
|
956
|
+
if (parentCommitSha) {
|
|
957
|
+
await this.updateRef(token, owner, repoName, branch, commitSha);
|
|
958
|
+
} else {
|
|
959
|
+
await this.createRef(token, owner, repoName, branch, commitSha);
|
|
960
|
+
}
|
|
961
|
+
} catch (err) {
|
|
962
|
+
if (err instanceof Error && err.message.includes("422")) {
|
|
963
|
+
const remote = await this.getRef(token, owner, repoName, branch);
|
|
964
|
+
parentCommitSha = remote.object.sha;
|
|
965
|
+
baseTreeSha = (await this.getCommit(token, owner, repoName, parentCommitSha)).tree.sha;
|
|
966
|
+
remoteEntries = await this.getTreeEntries(token, owner, repoName, baseTreeSha);
|
|
967
|
+
treeEntries = this.buildPushTree(localEntries, remoteEntries, cfg.categories);
|
|
968
|
+
newTreeSha = await this.createGitTree(token, owner, repoName, treeEntries, baseTreeSha);
|
|
969
|
+
commitSha = await this.createCommit(
|
|
970
|
+
token,
|
|
971
|
+
owner,
|
|
972
|
+
repoName,
|
|
973
|
+
newTreeSha,
|
|
974
|
+
parentCommitSha,
|
|
975
|
+
`Sync ${cfg.categories.join(", ")} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
976
|
+
);
|
|
977
|
+
await this.updateRef(token, owner, repoName, branch, commitSha);
|
|
978
|
+
} else {
|
|
979
|
+
throw err;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
const syncState = {
|
|
983
|
+
version: 1,
|
|
984
|
+
sha: commitSha,
|
|
985
|
+
treeSha: newTreeSha,
|
|
986
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
987
|
+
localRev: rev
|
|
988
|
+
};
|
|
989
|
+
await atomicWrite(this.statePath, JSON.stringify(syncState, null, 2));
|
|
990
|
+
this.state = syncState;
|
|
991
|
+
return {
|
|
992
|
+
ok: true,
|
|
993
|
+
action: "push",
|
|
994
|
+
categories: cfg.categories,
|
|
995
|
+
committedAt: commitSha,
|
|
996
|
+
message: `Pushed ${cfg.categories.join(", ")} to ${cfg.repo}. Commit: ${commitSha.slice(0, 7)}`
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
async pull(token) {
|
|
1000
|
+
const cfg = this.getConfig();
|
|
1001
|
+
if (!cfg?.enabled) return { ok: false, action: "pull", categories: [], message: "Not enabled." };
|
|
1002
|
+
const pullParts = cfg.repo.split("/");
|
|
1003
|
+
const owner = expectDefined(pullParts[0]);
|
|
1004
|
+
const repoName = expectDefined(pullParts[1]);
|
|
1005
|
+
const branchData = await this.getRef(token, owner, repoName, "main");
|
|
1006
|
+
const currentSha = branchData.object.sha;
|
|
1007
|
+
const commitData = await this.getCommit(token, owner, repoName, currentSha);
|
|
1008
|
+
const treeSha = commitData.tree.sha;
|
|
1009
|
+
const treeEntries = await this.getTreeEntries(token, owner, repoName, treeSha);
|
|
1010
|
+
for (const entry of treeEntries) {
|
|
1011
|
+
if (entry.type !== "blob") continue;
|
|
1012
|
+
const segments = entry.path.split("/");
|
|
1013
|
+
if (segments[0] !== "data" || !segments[1]) continue;
|
|
1014
|
+
const cat = segments[1];
|
|
1015
|
+
if (!ALL_SYNC_CATEGORIES.includes(cat) || !cfg.categories.includes(cat)) continue;
|
|
1016
|
+
const localPath = this.categoryToPath(cat);
|
|
1017
|
+
if (!localPath) continue;
|
|
1018
|
+
const rel = segments.slice(2).join("/");
|
|
1019
|
+
const destPath = resolvePulledCategoryPath(cat, localPath, rel, entry.path);
|
|
1020
|
+
await preparePulledDestination(cat, localPath, destPath, entry.path);
|
|
1021
|
+
const blobData = await this.getBlob(token, owner, repoName, entry.sha);
|
|
1022
|
+
await atomicWrite(destPath, Buffer.from(blobData, "base64"));
|
|
1023
|
+
}
|
|
1024
|
+
const localRev = await this.hashLocalCategories(cfg.categories);
|
|
1025
|
+
const syncState = {
|
|
1026
|
+
version: 1,
|
|
1027
|
+
sha: currentSha,
|
|
1028
|
+
treeSha,
|
|
1029
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1030
|
+
localRev
|
|
1031
|
+
};
|
|
1032
|
+
await atomicWrite(this.statePath, JSON.stringify(syncState, null, 2));
|
|
1033
|
+
this.state = syncState;
|
|
1034
|
+
return {
|
|
1035
|
+
ok: true,
|
|
1036
|
+
action: "pull",
|
|
1037
|
+
categories: cfg.categories,
|
|
1038
|
+
committedAt: currentSha,
|
|
1039
|
+
message: `Pulled ${cfg.categories.join(", ")} from ${cfg.repo}. Commit: ${currentSha.slice(0, 7)}`
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
async hasLocalChanges() {
|
|
1043
|
+
if (!this.state) return true;
|
|
1044
|
+
const cfg = this.getConfig();
|
|
1045
|
+
if (!cfg) return true;
|
|
1046
|
+
const current = await this.hashLocalCategories(cfg.categories);
|
|
1047
|
+
return current !== this.state.localRev;
|
|
1048
|
+
}
|
|
1049
|
+
async loadState() {
|
|
1050
|
+
try {
|
|
1051
|
+
const raw = await fs3.readFile(this.statePath, "utf8");
|
|
1052
|
+
this.state = JSON.parse(raw);
|
|
1053
|
+
} catch {
|
|
1054
|
+
this.state = null;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// ── GitHub API helpers ──────────────────────────────────────────────
|
|
1058
|
+
async githubFetch(token, owner, repo, method, pathSegment, body) {
|
|
1059
|
+
const url = `https://api.github.com/repos/${owner}/${repo}${pathSegment}`;
|
|
1060
|
+
const init = {
|
|
1061
|
+
signal: AbortSignal.timeout(15e3),
|
|
1062
|
+
method,
|
|
1063
|
+
headers: {
|
|
1064
|
+
Authorization: `Bearer ${token}`,
|
|
1065
|
+
Accept: "application/vnd.github+json",
|
|
1066
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
1067
|
+
"Content-Type": "application/json"
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
if (body !== void 0) init.body = JSON.stringify(body);
|
|
1071
|
+
const res = await fetch(url, init);
|
|
1072
|
+
if (!res.ok) {
|
|
1073
|
+
const errText = await res.text();
|
|
1074
|
+
throw new WrongStackError({
|
|
1075
|
+
message: `GitHub API ${method} ${pathSegment} failed (${res.status}): ${errText}`,
|
|
1076
|
+
code: ERROR_CODES.UNKNOWN,
|
|
1077
|
+
subsystem: "general",
|
|
1078
|
+
context: { method, pathSegment, status: res.status, repo: `${owner}/${repo}` }
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
const text = await res.text();
|
|
1082
|
+
return text ? JSON.parse(text) : {};
|
|
1083
|
+
}
|
|
1084
|
+
async getRef(token, owner, repo, ref) {
|
|
1085
|
+
return await this.githubFetch(token, owner, repo, "GET", `/git/refs/heads/${ref}`);
|
|
1086
|
+
}
|
|
1087
|
+
async getOptionalRef(token, owner, repo, ref) {
|
|
1088
|
+
try {
|
|
1089
|
+
return await this.getRef(token, owner, repo, ref);
|
|
1090
|
+
} catch (err) {
|
|
1091
|
+
if (err instanceof Error && err.message.includes("failed (404)")) return null;
|
|
1092
|
+
if (err instanceof Error && err.message.includes("failed (409)")) {
|
|
1093
|
+
throw new WrongStackError({
|
|
1094
|
+
message: "GitHub repository is empty. Initialize it with a README or first commit, then retry `/sync push`.",
|
|
1095
|
+
code: ERROR_CODES.UNKNOWN,
|
|
1096
|
+
subsystem: "general",
|
|
1097
|
+
context: { repo: `${owner}/${repo}`, ref },
|
|
1098
|
+
cause: err
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
throw err;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
async createRef(token, owner, repo, ref, sha) {
|
|
1105
|
+
await this.githubFetch(token, owner, repo, "POST", "/git/refs", {
|
|
1106
|
+
ref: `refs/heads/${ref}`,
|
|
1107
|
+
sha
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
async updateRef(token, owner, repo, ref, sha) {
|
|
1111
|
+
await this.githubFetch(token, owner, repo, "PATCH", `/git/refs/heads/${ref}`, {
|
|
1112
|
+
sha,
|
|
1113
|
+
force: false
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
async getCommit(token, owner, repo, sha) {
|
|
1117
|
+
return await this.githubFetch(token, owner, repo, "GET", `/git/commits/${sha}`);
|
|
1118
|
+
}
|
|
1119
|
+
async getTreeEntries(token, owner, repo, treeSha) {
|
|
1120
|
+
const res = await this.githubFetch(
|
|
1121
|
+
token,
|
|
1122
|
+
owner,
|
|
1123
|
+
repo,
|
|
1124
|
+
"GET",
|
|
1125
|
+
`/git/trees/${treeSha}?recursive=1`
|
|
1126
|
+
);
|
|
1127
|
+
if (res.truncated) {
|
|
1128
|
+
throw new WrongStackError({
|
|
1129
|
+
message: "Sync tree is too large \u2014 GitHub truncated the result; refusing a partial pull.",
|
|
1130
|
+
code: ERROR_CODES.UNKNOWN,
|
|
1131
|
+
subsystem: "general"
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
return res.tree ?? [];
|
|
1135
|
+
}
|
|
1136
|
+
async createCommit(token, owner, repo, treeSha, parentSha, message = "sync") {
|
|
1137
|
+
const body = { message, tree: treeSha };
|
|
1138
|
+
if (parentSha) body.parents = [parentSha];
|
|
1139
|
+
const result = await this.githubFetch(token, owner, repo, "POST", "/git/commits", body);
|
|
1140
|
+
return result.sha;
|
|
1141
|
+
}
|
|
1142
|
+
async createGitTree(token, owner, repo, entries, baseTreeSha) {
|
|
1143
|
+
const tree = entries.map(
|
|
1144
|
+
(entry) => "sha" in entry ? { path: entry.path, mode: entry.mode, type: "blob", sha: entry.sha } : { path: entry.path, mode: entry.mode, type: "blob", content: entry.content }
|
|
1145
|
+
);
|
|
1146
|
+
const body = { tree };
|
|
1147
|
+
if (baseTreeSha) body.base_tree = baseTreeSha;
|
|
1148
|
+
const result = await this.githubFetch(token, owner, repo, "POST", "/git/trees", body);
|
|
1149
|
+
return result.sha;
|
|
1150
|
+
}
|
|
1151
|
+
async getBlob(token, owner, repo, sha) {
|
|
1152
|
+
const result = await this.githubFetch(token, owner, repo, "GET", `/git/blobs/${sha}`);
|
|
1153
|
+
return result.content;
|
|
1154
|
+
}
|
|
1155
|
+
// ── Local file helpers ──────────────────────────────────────────────
|
|
1156
|
+
buildPushTree(localEntries, remoteEntries, categories) {
|
|
1157
|
+
const selected = new Set(categories.map((category) => `data/${category}`));
|
|
1158
|
+
const localPaths = new Set(localEntries.map((entry) => entry.path));
|
|
1159
|
+
const deletions = [];
|
|
1160
|
+
for (const entry of remoteEntries) {
|
|
1161
|
+
if (entry.type !== "blob" || localPaths.has(entry.path)) continue;
|
|
1162
|
+
const selectedCategory = [...selected].some(
|
|
1163
|
+
(prefix) => entry.path === prefix || entry.path.startsWith(`${prefix}/`)
|
|
1164
|
+
);
|
|
1165
|
+
if (selectedCategory) deletions.push({ path: entry.path, sha: null, mode: "100644" });
|
|
1166
|
+
}
|
|
1167
|
+
return [...localEntries, ...deletions];
|
|
1168
|
+
}
|
|
1169
|
+
async buildLocalTree(categories) {
|
|
1170
|
+
const entries = [];
|
|
1171
|
+
const hashes = [];
|
|
1172
|
+
for (const cat of categories) {
|
|
1173
|
+
const localPath = this.categoryToPath(cat);
|
|
1174
|
+
if (!localPath) continue;
|
|
1175
|
+
try {
|
|
1176
|
+
const stat2 = await fs3.lstat(localPath);
|
|
1177
|
+
if (stat2.isSymbolicLink()) continue;
|
|
1178
|
+
if (stat2.isDirectory()) {
|
|
1179
|
+
const files = await this.walkDir(localPath, localPath);
|
|
1180
|
+
for (const file of files) {
|
|
1181
|
+
const content = await fs3.readFile(file, "utf8");
|
|
1182
|
+
const rel = path2.relative(localPath, file).replace(/\\/g, "/");
|
|
1183
|
+
entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
|
|
1184
|
+
hashes.push(`${cat}/${rel}\0${content}`);
|
|
1185
|
+
}
|
|
1186
|
+
} else {
|
|
1187
|
+
const content = await fs3.readFile(localPath, "utf8");
|
|
1188
|
+
entries.push({ path: `data/${cat}`, content, mode: "100644" });
|
|
1189
|
+
hashes.push(`${cat}\0${content}`);
|
|
1190
|
+
}
|
|
1191
|
+
} catch {
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const rev = createHash2("sha256").update(hashes.join("")).digest("hex").slice(0, 12);
|
|
1195
|
+
return { treeEntries: entries, rev };
|
|
1196
|
+
}
|
|
1197
|
+
async hashLocalCategories(categories) {
|
|
1198
|
+
const hashes = [];
|
|
1199
|
+
for (const cat of categories) {
|
|
1200
|
+
const localPath = this.categoryToPath(cat);
|
|
1201
|
+
if (!localPath) continue;
|
|
1202
|
+
try {
|
|
1203
|
+
const stat2 = await fs3.lstat(localPath);
|
|
1204
|
+
if (stat2.isSymbolicLink()) continue;
|
|
1205
|
+
if (stat2.isDirectory()) {
|
|
1206
|
+
const files = await this.walkDir(localPath, localPath);
|
|
1207
|
+
for (const file of files) {
|
|
1208
|
+
const content = await fs3.readFile(file, "utf8");
|
|
1209
|
+
const rel = path2.relative(localPath, file).replace(/\\/g, "/");
|
|
1210
|
+
hashes.push(`${cat}/${rel}\0${content}`);
|
|
1211
|
+
}
|
|
1212
|
+
} else {
|
|
1213
|
+
const content = await fs3.readFile(localPath, "utf8");
|
|
1214
|
+
hashes.push(`${cat}\0${content}`);
|
|
1215
|
+
}
|
|
1216
|
+
} catch {
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return createHash2("sha256").update(hashes.join("")).digest("hex").slice(0, 12);
|
|
1220
|
+
}
|
|
1221
|
+
categoryToPath(cat) {
|
|
1222
|
+
switch (cat) {
|
|
1223
|
+
case "settings":
|
|
1224
|
+
return this.getSettingsConfigPath();
|
|
1225
|
+
case "skills":
|
|
1226
|
+
return this.paths.globalSkills;
|
|
1227
|
+
case "prompts":
|
|
1228
|
+
return this.paths.globalPrompts;
|
|
1229
|
+
case "memory":
|
|
1230
|
+
return this.paths.globalMemory;
|
|
1231
|
+
case "history":
|
|
1232
|
+
return this.paths.historyFile;
|
|
1233
|
+
/* v8 ignore next -- unreachable: SyncCategory is exhaustively matched above */
|
|
1234
|
+
default:
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async walkDir(dir, base) {
|
|
1239
|
+
const results = [];
|
|
1240
|
+
const entries = await fs3.readdir(dir, { withFileTypes: true });
|
|
1241
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1242
|
+
for (const entry of entries) {
|
|
1243
|
+
const full = path2.join(dir, entry.name);
|
|
1244
|
+
if (entry.isSymbolicLink()) continue;
|
|
1245
|
+
if (entry.isDirectory()) {
|
|
1246
|
+
results.push(...await this.walkDir(full, base));
|
|
1247
|
+
} else if (entry.isFile()) {
|
|
1248
|
+
results.push(full);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return results;
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
async function preparePulledDestination(cat, localPath, destPath, remotePath) {
|
|
1255
|
+
const directoryBacked = cat === "skills" || cat === "prompts";
|
|
1256
|
+
const rootPath = directoryBacked ? localPath : path2.dirname(localPath);
|
|
1257
|
+
const rootStat = await lstatIfExists(rootPath);
|
|
1258
|
+
if (rootStat?.isSymbolicLink()) {
|
|
1259
|
+
throw unsafePulledSymlinkError(remotePath, rootPath);
|
|
1260
|
+
}
|
|
1261
|
+
if (directoryBacked) {
|
|
1262
|
+
const relativeParent = path2.relative(rootPath, path2.dirname(destPath));
|
|
1263
|
+
let cursor = rootPath;
|
|
1264
|
+
if (relativeParent) {
|
|
1265
|
+
for (const segment of relativeParent.split(path2.sep)) {
|
|
1266
|
+
cursor = path2.join(cursor, segment);
|
|
1267
|
+
const stat2 = await lstatIfExists(cursor);
|
|
1268
|
+
if (stat2?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, cursor);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
const destStat = await lstatIfExists(destPath);
|
|
1273
|
+
if (destStat?.isSymbolicLink()) throw unsafePulledSymlinkError(remotePath, destPath);
|
|
1274
|
+
await fs3.mkdir(path2.dirname(destPath), { recursive: true });
|
|
1275
|
+
}
|
|
1276
|
+
async function lstatIfExists(filePath) {
|
|
1277
|
+
try {
|
|
1278
|
+
return await fs3.lstat(filePath);
|
|
1279
|
+
} catch (err) {
|
|
1280
|
+
if (err.code === "ENOENT") return null;
|
|
1281
|
+
throw err;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
function unsafePulledSymlinkError(remotePath, symlinkPath) {
|
|
1285
|
+
return new FsError({
|
|
1286
|
+
message: `Refusing CloudSync write through symlink: ${remotePath}`,
|
|
1287
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
1288
|
+
path: remotePath,
|
|
1289
|
+
context: { reason: "symlink_destination", symlinkPath }
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
function resolvePulledCategoryPath(cat, localPath, rel, remotePath) {
|
|
1293
|
+
const directoryBacked = cat === "skills" || cat === "prompts";
|
|
1294
|
+
if (!directoryBacked) {
|
|
1295
|
+
if (rel) throw new FsError({
|
|
1296
|
+
message: `Refusing nested CloudSync path for file category: ${remotePath}`,
|
|
1297
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
1298
|
+
path: remotePath,
|
|
1299
|
+
context: { reason: "nested_file_category", category: cat }
|
|
1300
|
+
});
|
|
1301
|
+
return localPath;
|
|
1302
|
+
}
|
|
1303
|
+
if (!rel) return localPath;
|
|
1304
|
+
const normalizedRel = path2.normalize(rel);
|
|
1305
|
+
const traversesUp = normalizedRel === ".." || normalizedRel.startsWith(`..${path2.sep}`);
|
|
1306
|
+
if (path2.isAbsolute(normalizedRel) || traversesUp) {
|
|
1307
|
+
throw new FsError({
|
|
1308
|
+
message: `Refusing CloudSync path traversal: ${remotePath}`,
|
|
1309
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
1310
|
+
path: remotePath,
|
|
1311
|
+
context: { reason: "path_traversal", normalizedRel }
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
const dest = path2.resolve(localPath, normalizedRel);
|
|
1315
|
+
const root = path2.resolve(localPath);
|
|
1316
|
+
const relative2 = path2.relative(root, dest);
|
|
1317
|
+
if (relative2.startsWith("..") || path2.isAbsolute(relative2)) {
|
|
1318
|
+
throw new FsError({
|
|
1319
|
+
message: `Refusing CloudSync path outside category root: ${remotePath}`,
|
|
1320
|
+
code: ERROR_CODES.FS_DELETE_FAILED,
|
|
1321
|
+
path: remotePath,
|
|
1322
|
+
context: { reason: "outside_category_root", category: cat }
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
return dest;
|
|
1326
|
+
}
|
|
1327
|
+
function timeAgo(iso) {
|
|
1328
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
1329
|
+
const mins = Math.floor(diff / 6e4);
|
|
1330
|
+
if (mins < 1) return "just now";
|
|
1331
|
+
if (mins < 60) return `${mins}m ago`;
|
|
1332
|
+
const hrs = Math.floor(mins / 60);
|
|
1333
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
1334
|
+
const days = Math.floor(hrs / 24);
|
|
1335
|
+
return `${days}d ago`;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
export {
|
|
1339
|
+
CLOUD_SYNC_CONTRACT,
|
|
1340
|
+
CLOUD_SYNC_NAMESPACES,
|
|
1341
|
+
NAMESPACE_SCHEMA_VERSIONS,
|
|
1342
|
+
LOCAL_ONLY_TOP_LEVEL,
|
|
1343
|
+
stripSecretMaterial,
|
|
1344
|
+
buildNamespacePayloads,
|
|
1345
|
+
applyNamespacePayload,
|
|
1346
|
+
CloudConfigSync,
|
|
1347
|
+
PromptUsageStore,
|
|
1348
|
+
ALL_SYNC_CATEGORIES,
|
|
1349
|
+
CloudSync
|
|
1350
|
+
};
|
|
1351
|
+
//# sourceMappingURL=chunk-QVOVMXJR.js.map
|