glimmervoid 0.35.1 → 0.37.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/README.md +86 -82
- package/dist/bin/glimmervoid.js +58 -114
- package/dist/chunks/{adapters-CADejSpq.js → adapters-BmfVPxlH.js} +29 -61
- package/dist/chunks/adapters-CqFFmM2R.js +1 -0
- package/dist/chunks/{agent-api-cli-CRj_FYbz.js → agent-api-cli-BTHEyJKS.js} +1 -1
- package/dist/chunks/{agent-api-cli-vGwstJ1j.js → agent-api-cli-CBgOKXoO.js} +41 -12
- package/dist/chunks/{agent-api-core-D1233u1U.js → agent-api-core-DzKhakCv.js} +94 -3
- package/dist/chunks/{agent-setup-cli-BTAFcckd.js → agent-setup-cli-C8DGxlA-.js} +1 -1
- package/dist/chunks/{config-rPbbL4nW.js → config-BYc2z1Oc.js} +60 -779
- package/dist/chunks/config-store-B7MpiYxG.js +4181 -0
- package/dist/chunks/config-store-CsUoy88Q.js +1 -0
- package/dist/chunks/{contracts-DjIgIuDk.js → contracts-C6ipN_zw.js} +100 -12
- package/dist/chunks/git-version-6QFnKbWa.js +139 -0
- package/dist/chunks/{grok-KKgdSe-7.js → grok-CZ6kP_vD.js} +7 -3
- package/dist/chunks/guardrails-deps-Cb-ki4EU.js +70 -0
- package/dist/chunks/{sandbox-proxy-core-hnQgKR0T.js → guardrails-proxy-core-BI3rdY-m.js} +4 -4
- package/dist/chunks/{hook-command-core-DclEoE9e.js → hook-command-core-qTNK-xMF.js} +4 -1
- package/dist/chunks/{hook-tools-UX2fmt81.js → hook-tools-BkxllUHE.js} +9 -16
- package/dist/chunks/{knowledge-graph-cli-Rj5nj87L.js → knowledge-graph-cli-DApeMzA8.js} +1 -1
- package/dist/chunks/{loopback-post-DepQfdcM.js → loopback-post-DJ6iYWuL.js} +9 -9
- package/dist/chunks/{main-C-T2Uk20.js → main-BUfjroXE.js} +16282 -12595
- package/dist/chunks/{pair-cli-Dcyzv3In.js → pair-cli-GZ9t1mIO.js} +2 -2
- package/dist/chunks/{remote-config-Dd1T-VJA.js → remote-config-D2Is89Kj.js} +1 -1
- package/dist/chunks/{rtk-hook-core-BmEz2aq9.js → rtk-hook-core-VrUafGAp.js} +15 -5
- package/dist/chunks/rtk-resolver-B8BPgN9L.js +1 -0
- package/dist/chunks/{rtk-resolver-B9pzjLsM.js → rtk-resolver-C_micGrL.js} +1 -1
- package/dist/chunks/session-BZbariv5.js +1032 -0
- package/dist/chunks/{session-routes-BYy3am9m.js → session-routes-C4t8O8yg.js} +9 -9
- package/dist/chunks/settings-cli-DQOjmjev.js +484 -0
- package/dist/chunks/{ssh-sign-cli-DPHpT22B.js → ssh-sign-cli-LIBP8ppO.js} +2 -2
- package/dist/client/assets/index-B_Qt7fbZ.css +1 -0
- package/dist/client/assets/index-D-upBor0.js +265 -0
- package/dist/client/index.html +5 -3
- package/dist/scripts/postinstall-path-check.js +4 -4
- package/dist/server/index.js +1 -1
- package/dist/session/command-hook-relay.js +1 -1
- package/dist/session/{sandbox-open-launcher.js → guardrails-open-launcher.js} +10 -10
- package/dist/session/hook-relay.js +1 -1
- package/dist/session/hook-tool-relay.js +3 -3
- package/dist/session/statusline-relay.js +1 -1
- package/package.json +6 -5
- package/dist/chunks/adapters-B37wqNti.js +0 -1
- package/dist/chunks/config-store-BeIxAZNn.js +0 -1
- package/dist/chunks/config-store-BtsYbNC3.js +0 -1713
- package/dist/chunks/git-version-CUcDtF5k.js +0 -58
- package/dist/chunks/rtk-resolver-SWU5VVM7.js +0 -1
- package/dist/chunks/sandbox-deps-CWiH1d1w.js +0 -70
- package/dist/chunks/session-D6xXN0UU.js +0 -232
- package/dist/client/assets/index-CUxRHbCS.js +0 -254
- package/dist/client/assets/index-DE2GUmqs.css +0 -1
|
@@ -1,1713 +0,0 @@
|
|
|
1
|
-
import { n as errorLabel, r as errorMessage } from "./text-DoirglWV.js";
|
|
2
|
-
import { n as equalsIgnoringCaseOnWindows } from "./paths-Bu4eH1Yd.js";
|
|
3
|
-
import { n as glimmervoidHomeDir$1, t as decideConfigPath } from "./config-path-core-DFt-TemS.js";
|
|
4
|
-
import { a as positiveIntOr, c as textOr, n as isRecord } from "./coerce-NSst8oRP.js";
|
|
5
|
-
import { d as writeJsonAtomicSync, f as writeTextAtomic, p as writeTextAtomicSync } from "./json-file-BaxGcB2R.js";
|
|
6
|
-
import { St as normalizeLegacyConfigNumbers, f as BranchGcFileSettings, i as Config, l as configIssueMessage, m as DEFAULT_FACTORY_PROTECTED_PATHS, n as BUILTIN_AGENT_IDS, p as DEFAULT_FACTORY_CHECKS, s as RUNTIME_CONFIG_SCALAR_KEYS, t as AGENT_ID_SHAPE_MESSAGE } from "./config-rPbbL4nW.js";
|
|
7
|
-
import { V as FeatureFlagsResponse, W as TELEMETRY_KILL_SWITCH_FLAG } from "./contracts-DjIgIuDk.js";
|
|
8
|
-
import fs from "node:fs";
|
|
9
|
-
import path from "node:path";
|
|
10
|
-
import os from "node:os";
|
|
11
|
-
import { promisify } from "node:util";
|
|
12
|
-
import crypto, { createHash } from "node:crypto";
|
|
13
|
-
//#region shared/canonical-paths.ts
|
|
14
|
-
var realpathNative = promisify(fs.realpath.native);
|
|
15
|
-
function canonicalizePath(p) {
|
|
16
|
-
try {
|
|
17
|
-
return fs.realpathSync.native(p);
|
|
18
|
-
} catch {
|
|
19
|
-
return p;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
function isSameDirectoryPath(a, b) {
|
|
23
|
-
const resolvedA = path.resolve(String(a || ""));
|
|
24
|
-
const resolvedB = path.resolve(String(b || ""));
|
|
25
|
-
if (equalsIgnoringCaseOnWindows(resolvedA, resolvedB)) return true;
|
|
26
|
-
if (process.platform !== "win32" && process.platform !== "darwin") return false;
|
|
27
|
-
return equalsIgnoringCaseOnWindows(canonicalizePath(resolvedA), canonicalizePath(resolvedB));
|
|
28
|
-
}
|
|
29
|
-
async function comparableDirectoryPath(candidatePath) {
|
|
30
|
-
const resolvedPath = path.resolve(String(candidatePath || ""));
|
|
31
|
-
let canonicalPath;
|
|
32
|
-
try {
|
|
33
|
-
canonicalPath = await realpathNative(resolvedPath);
|
|
34
|
-
} catch {
|
|
35
|
-
canonicalPath = resolvedPath;
|
|
36
|
-
}
|
|
37
|
-
if (process.platform !== "win32") return canonicalPath;
|
|
38
|
-
return canonicalPath.toLowerCase();
|
|
39
|
-
}
|
|
40
|
-
//#endregion
|
|
41
|
-
//#region server/core/branch-gc-core.ts
|
|
42
|
-
var SESSION_BRANCH_PREFIX = "glimmervoid/session/";
|
|
43
|
-
var DEFAULT_BRANCH_GC_PREFIXES = [SESSION_BRANCH_PREFIX, "worktree-agent-"];
|
|
44
|
-
var DAY_MS = 864e5;
|
|
45
|
-
var MERGED_DELETION_REASON = "merged-into-integration";
|
|
46
|
-
var STALE_DELETION_REASON = "stale-orphan";
|
|
47
|
-
var UNMERGED_KEPT_REASON = "not-merged-and-unmerged-deletion-off";
|
|
48
|
-
function sessionIdFromBranch(branchName) {
|
|
49
|
-
if (!branchName.startsWith(SESSION_BRANCH_PREFIX)) return null;
|
|
50
|
-
return branchName.slice(20);
|
|
51
|
-
}
|
|
52
|
-
function usablePrefixes(prefixes) {
|
|
53
|
-
return prefixes.filter((prefix) => prefix.length > 0);
|
|
54
|
-
}
|
|
55
|
-
function matchedPrefix(name, prefixes) {
|
|
56
|
-
for (const prefix of usablePrefixes(prefixes)) if (name.startsWith(prefix)) return prefix;
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
function protectedBranchNames(integrationTips) {
|
|
60
|
-
return /* @__PURE__ */ new Set([
|
|
61
|
-
"main",
|
|
62
|
-
"master",
|
|
63
|
-
...integrationTips.map((tip) => tip.branch)
|
|
64
|
-
]);
|
|
65
|
-
}
|
|
66
|
-
function planBranchGc({ remoteBranches = [], integrationTips = [], liveSessionIds = /* @__PURE__ */ new Set(), prefixes = DEFAULT_BRANCH_GC_PREFIXES, nowMs, staleDays = 14, deleteUnmerged = false }) {
|
|
67
|
-
const deletions = [];
|
|
68
|
-
const kept = [];
|
|
69
|
-
const protectedNames = protectedBranchNames(integrationTips);
|
|
70
|
-
const staleBeforeMs = nowMs - staleDays * DAY_MS;
|
|
71
|
-
for (const remoteBranch of remoteBranches) {
|
|
72
|
-
const { name, tipSha } = remoteBranch;
|
|
73
|
-
if (matchedPrefix(name, prefixes) === null) {
|
|
74
|
-
kept.push({
|
|
75
|
-
name,
|
|
76
|
-
reason: "foreign-prefix"
|
|
77
|
-
});
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (protectedNames.has(name)) {
|
|
81
|
-
kept.push({
|
|
82
|
-
name,
|
|
83
|
-
reason: "not-merged-and-fresh"
|
|
84
|
-
});
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
const sessionId = sessionIdFromBranch(name);
|
|
88
|
-
if (sessionId !== null && liveSessionIds.has(sessionId)) {
|
|
89
|
-
kept.push({
|
|
90
|
-
name,
|
|
91
|
-
reason: "live-session"
|
|
92
|
-
});
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (remoteBranch.mergedIntoIntegration === true) {
|
|
96
|
-
deletions.push({
|
|
97
|
-
name,
|
|
98
|
-
reason: remoteBranch.mergedReason ?? MERGED_DELETION_REASON,
|
|
99
|
-
tipSha
|
|
100
|
-
});
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (!deleteUnmerged) {
|
|
104
|
-
kept.push({
|
|
105
|
-
name,
|
|
106
|
-
reason: UNMERGED_KEPT_REASON
|
|
107
|
-
});
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
if (Number.isFinite(remoteBranch.tipCommitTimeMs) && remoteBranch.tipCommitTimeMs < staleBeforeMs) {
|
|
111
|
-
deletions.push({
|
|
112
|
-
name,
|
|
113
|
-
reason: STALE_DELETION_REASON,
|
|
114
|
-
tipSha
|
|
115
|
-
});
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
kept.push({
|
|
119
|
-
name,
|
|
120
|
-
reason: "not-merged-and-fresh"
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
deletions,
|
|
125
|
-
kept
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
function worktreeIntegrationTips({ projectTips, worktreeOwnTips }) {
|
|
129
|
-
const tipBySha = /* @__PURE__ */ new Map();
|
|
130
|
-
for (const tip of [...projectTips, ...worktreeOwnTips]) {
|
|
131
|
-
if (!tip.sha) continue;
|
|
132
|
-
if (tipBySha.has(tip.sha)) continue;
|
|
133
|
-
tipBySha.set(tip.sha, tip);
|
|
134
|
-
}
|
|
135
|
-
return [...tipBySha.values()];
|
|
136
|
-
}
|
|
137
|
-
function protectedWorktreeBranchNames(integrationTips, worktrees) {
|
|
138
|
-
const ownIntegrationBranches = worktrees.map((worktree) => worktree.integrationBranch).filter((branch) => Boolean(branch)).map((branch) => ({ branch }));
|
|
139
|
-
return protectedBranchNames([...integrationTips, ...ownIntegrationBranches]);
|
|
140
|
-
}
|
|
141
|
-
function planWorktreeGc({ worktrees, liveWorktreePaths, integrationTips, prefixes }) {
|
|
142
|
-
const removals = [];
|
|
143
|
-
const kept = [];
|
|
144
|
-
const protectedNames = protectedWorktreeBranchNames(integrationTips, worktrees);
|
|
145
|
-
for (const worktree of worktrees) {
|
|
146
|
-
const decision = (disposition, reason) => ({
|
|
147
|
-
cwd: worktree.cwd,
|
|
148
|
-
branch: worktree.branch,
|
|
149
|
-
disposition,
|
|
150
|
-
reason
|
|
151
|
-
});
|
|
152
|
-
if (worktree.prunable === true) {
|
|
153
|
-
kept.push(decision("keep", "missing-directory"));
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (worktree.locked) {
|
|
157
|
-
kept.push(decision("keep", "locked"));
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (liveWorktreePaths.has(worktree.cwd)) {
|
|
161
|
-
kept.push(decision("keep", "live-session"));
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
if (protectedNames.has(worktree.branch)) {
|
|
165
|
-
kept.push(decision("keep", "protected"));
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
if (matchedPrefix(worktree.branch, prefixes) === null) {
|
|
169
|
-
kept.push(decision("keep", "foreign-prefix"));
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if (worktree.dirty === null) {
|
|
173
|
-
kept.push(decision("keep", "status-probe-failed"));
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
if (worktree.dirty) {
|
|
177
|
-
kept.push(decision("keep", "uncommitted-changes"));
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (worktree.atIntegrationTip === true) {
|
|
181
|
-
kept.push(decision("keep", "no-commits"));
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (worktree.merged === null) {
|
|
185
|
-
kept.push(decision("keep", "merge-proof-failed"));
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if (worktree.merged !== true) {
|
|
189
|
-
kept.push(decision("keep", "unmerged"));
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
removals.push(decision("remove", "merged"));
|
|
193
|
-
}
|
|
194
|
-
return {
|
|
195
|
-
removals,
|
|
196
|
-
kept
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
//#endregion
|
|
200
|
-
//#region server/core/config-secrets-core.ts
|
|
201
|
-
var ENV_SECRET_BINDINGS = Object.freeze([{
|
|
202
|
-
blockName: "posthog",
|
|
203
|
-
secretKey: "apiKey",
|
|
204
|
-
environmentVariable: "GLIMMERVOID_POSTHOG_API_KEY"
|
|
205
|
-
}, {
|
|
206
|
-
blockName: "telegram",
|
|
207
|
-
secretKey: "botToken",
|
|
208
|
-
environmentVariable: "GLIMMERVOID_TELEGRAM_BOT_TOKEN"
|
|
209
|
-
}]);
|
|
210
|
-
function readEnvSecrets(env) {
|
|
211
|
-
const resolvedSecrets = [];
|
|
212
|
-
for (const binding of ENV_SECRET_BINDINGS) {
|
|
213
|
-
const value = env[binding.environmentVariable];
|
|
214
|
-
if (typeof value !== "string" || value.length === 0) continue;
|
|
215
|
-
resolvedSecrets.push({
|
|
216
|
-
...binding,
|
|
217
|
-
value
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
return resolvedSecrets;
|
|
221
|
-
}
|
|
222
|
-
function copyOfBlock(config, blockName) {
|
|
223
|
-
const block = config[blockName];
|
|
224
|
-
if (!isRecord(block)) return {};
|
|
225
|
-
return { ...block };
|
|
226
|
-
}
|
|
227
|
-
function withEnvSecrets(config, envSecrets) {
|
|
228
|
-
if (envSecrets.length === 0) return config;
|
|
229
|
-
const overlaid = { ...config };
|
|
230
|
-
for (const secret of envSecrets) overlaid[secret.blockName] = {
|
|
231
|
-
...copyOfBlock(overlaid, secret.blockName),
|
|
232
|
-
[secret.secretKey]: secret.value
|
|
233
|
-
};
|
|
234
|
-
return overlaid;
|
|
235
|
-
}
|
|
236
|
-
function withoutEnvSecrets(config, envSecrets) {
|
|
237
|
-
if (envSecrets.length === 0) return config;
|
|
238
|
-
const stripped = { ...config };
|
|
239
|
-
for (const secret of envSecrets) {
|
|
240
|
-
if (!isRecord(stripped[secret.blockName])) continue;
|
|
241
|
-
const remaining = copyOfBlock(stripped, secret.blockName);
|
|
242
|
-
delete remaining[secret.secretKey];
|
|
243
|
-
if (Object.keys(remaining).length === 0) {
|
|
244
|
-
delete stripped[secret.blockName];
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
stripped[secret.blockName] = remaining;
|
|
248
|
-
}
|
|
249
|
-
return stripped;
|
|
250
|
-
}
|
|
251
|
-
function withSortedKeys(value) {
|
|
252
|
-
if (Array.isArray(value)) return value.map(withSortedKeys);
|
|
253
|
-
if (!isRecord(value)) return value;
|
|
254
|
-
const sorted = {};
|
|
255
|
-
for (const key of Object.keys(value).sort()) sorted[key] = withSortedKeys(value[key]);
|
|
256
|
-
return sorted;
|
|
257
|
-
}
|
|
258
|
-
function stableConfigKey(value) {
|
|
259
|
-
return JSON.stringify(withSortedKeys(value));
|
|
260
|
-
}
|
|
261
|
-
//#endregion
|
|
262
|
-
//#region server/core/telemetry-core.ts
|
|
263
|
-
var POSTHOG_INGEST_HOST = "https://us.i.posthog.com";
|
|
264
|
-
var TELEMETRY_BATCH_URL = `${POSTHOG_INGEST_HOST}/batch/`;
|
|
265
|
-
var TELEMETRY_FLAGS_URL = `${POSTHOG_INGEST_HOST}/flags?v=2`;
|
|
266
|
-
var MAX_STACK_LENGTH = 16384;
|
|
267
|
-
var FIRST_RUN_NOTICE = ["Glimmervoid sends anonymous usage and error data to help improve it: app starts, daily activity, session starts and ends with the agent kind, exit kind and duration, errors as their type, error code and scrubbed stack frames, per session token counts, models and estimated cost, and factory outcomes as the kind of decision, how it came out, its duration and attempt number. No error messages, paths, repository names, prompts, objectives, model output or terminal output.", "Turn it off in Settings > Privacy, or set GLIMMERVOID_TELEMETRY=0 or DO_NOT_TRACK=1."].join("\n");
|
|
268
|
-
var TRUTHY_FLAGS = /* @__PURE__ */ new Set([
|
|
269
|
-
"1",
|
|
270
|
-
"true",
|
|
271
|
-
"yes"
|
|
272
|
-
]);
|
|
273
|
-
var FALSY_FLAGS = /* @__PURE__ */ new Set([
|
|
274
|
-
"0",
|
|
275
|
-
"false",
|
|
276
|
-
"no",
|
|
277
|
-
"off"
|
|
278
|
-
]);
|
|
279
|
-
function normalizedFlag(value) {
|
|
280
|
-
return (value ?? "").trim().toLowerCase();
|
|
281
|
-
}
|
|
282
|
-
function decideTelemetryConsent(env, config) {
|
|
283
|
-
if (TRUTHY_FLAGS.has(normalizedFlag(env.DO_NOT_TRACK))) return {
|
|
284
|
-
isEnabled: false,
|
|
285
|
-
source: "do-not-track"
|
|
286
|
-
};
|
|
287
|
-
if (FALSY_FLAGS.has(normalizedFlag(env.GLIMMERVOID_TELEMETRY))) return {
|
|
288
|
-
isEnabled: false,
|
|
289
|
-
source: "environment"
|
|
290
|
-
};
|
|
291
|
-
if (TRUTHY_FLAGS.has(normalizedFlag(env.CI))) return {
|
|
292
|
-
isEnabled: false,
|
|
293
|
-
source: "ci"
|
|
294
|
-
};
|
|
295
|
-
if (normalizedFlag(env.NODE_TEST_CONTEXT) !== "") return {
|
|
296
|
-
isEnabled: false,
|
|
297
|
-
source: "test-runner"
|
|
298
|
-
};
|
|
299
|
-
const configured = config.telemetry?.enabled;
|
|
300
|
-
if (typeof configured === "boolean") return {
|
|
301
|
-
isEnabled: configured,
|
|
302
|
-
source: "config"
|
|
303
|
-
};
|
|
304
|
-
return {
|
|
305
|
-
isEnabled: true,
|
|
306
|
-
source: "default"
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
function isTelemetryForcedOff(env) {
|
|
310
|
-
return decideTelemetryConsent(env, { telemetry: { enabled: true } }).isEnabled === false;
|
|
311
|
-
}
|
|
312
|
-
function resolveProjectToken(env) {
|
|
313
|
-
return env.GLIMMERVOID_TELEMETRY_PROJECT_TOKEN?.trim() || "phc_s68t9vWmeGcrWkx4NkLQaEiVZ9sMBxv8GjQBbAcix2Sf";
|
|
314
|
-
}
|
|
315
|
-
function toForwardSlashes(filePath) {
|
|
316
|
-
return filePath.replaceAll("\\", "/");
|
|
317
|
-
}
|
|
318
|
-
function isWindowsStylePath(filePath) {
|
|
319
|
-
return /^[A-Za-z]:\//.test(filePath);
|
|
320
|
-
}
|
|
321
|
-
function comparablePath(filePath) {
|
|
322
|
-
const forward = toForwardSlashes(filePath).replace(/\/+$/, "");
|
|
323
|
-
return isWindowsStylePath(forward) ? forward.toLowerCase() : forward;
|
|
324
|
-
}
|
|
325
|
-
function decodedFileUrlPath(fileUrl) {
|
|
326
|
-
const urlPath = fileUrl.slice(7).replace(/^\/([A-Za-z]:)/, "$1");
|
|
327
|
-
try {
|
|
328
|
-
return decodeURIComponent(urlPath);
|
|
329
|
-
} catch {
|
|
330
|
-
return urlPath;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
function stripFileScheme(rawPath) {
|
|
334
|
-
if (!rawPath.startsWith("file://")) return toForwardSlashes(rawPath);
|
|
335
|
-
return toForwardSlashes(decodedFileUrlPath(rawPath));
|
|
336
|
-
}
|
|
337
|
-
function relativeUnder(filePath, root) {
|
|
338
|
-
if (!root) return null;
|
|
339
|
-
const comparableRoot = comparablePath(root);
|
|
340
|
-
if (!comparableRoot) return null;
|
|
341
|
-
if (!comparablePath(filePath).startsWith(`${comparableRoot}/`)) return null;
|
|
342
|
-
return toForwardSlashes(filePath).slice(comparableRoot.length + 1);
|
|
343
|
-
}
|
|
344
|
-
function scrubLocalPath(rawPath, packageRoot) {
|
|
345
|
-
const filePath = stripFileScheme(rawPath);
|
|
346
|
-
if (filePath.startsWith("node:")) return filePath;
|
|
347
|
-
const underPackage = relativeUnder(filePath, packageRoot);
|
|
348
|
-
if (underPackage !== null) return underPackage;
|
|
349
|
-
const nodeModulesAt = filePath.lastIndexOf("/node_modules/");
|
|
350
|
-
if (nodeModulesAt >= 0) return filePath.slice(nodeModulesAt + 1);
|
|
351
|
-
return filePath.slice(filePath.lastIndexOf("/") + 1);
|
|
352
|
-
}
|
|
353
|
-
function isInApp(rawPath, packageRoot) {
|
|
354
|
-
const filePath = stripFileScheme(rawPath);
|
|
355
|
-
if (filePath.startsWith("node:")) return false;
|
|
356
|
-
const underPackage = relativeUnder(filePath, packageRoot);
|
|
357
|
-
if (underPackage === null) return false;
|
|
358
|
-
return !underPackage.split("/").includes("node_modules");
|
|
359
|
-
}
|
|
360
|
-
var V8_FRAME_WITH_FUNCTION = /^\s*at (.+?) \((.+?):(\d+):(\d+)\)$/;
|
|
361
|
-
var V8_FRAME_ANONYMOUS = /^\s*at (.+?):(\d+):(\d+)$/;
|
|
362
|
-
function matchV8Frame(stackLine) {
|
|
363
|
-
const named = V8_FRAME_WITH_FUNCTION.exec(stackLine);
|
|
364
|
-
if (named) return {
|
|
365
|
-
functionName: named[1].replace(/^async /, ""),
|
|
366
|
-
filePath: named[2],
|
|
367
|
-
line: named[3],
|
|
368
|
-
column: named[4]
|
|
369
|
-
};
|
|
370
|
-
const anonymous = V8_FRAME_ANONYMOUS.exec(stackLine);
|
|
371
|
-
if (!anonymous) return null;
|
|
372
|
-
return {
|
|
373
|
-
functionName: "?",
|
|
374
|
-
filePath: anonymous[1].replace(/^async /, ""),
|
|
375
|
-
line: anonymous[2],
|
|
376
|
-
column: anonymous[3]
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
function parseV8StackFrames(stack, packageRoot, platform = "node:javascript") {
|
|
380
|
-
const frames = [];
|
|
381
|
-
for (const stackLine of stack.slice(0, MAX_STACK_LENGTH).split("\n")) {
|
|
382
|
-
const rawFrame = matchV8Frame(stackLine);
|
|
383
|
-
if (!rawFrame) continue;
|
|
384
|
-
frames.push({
|
|
385
|
-
platform,
|
|
386
|
-
function: rawFrame.functionName.slice(0, 512),
|
|
387
|
-
filename: scrubLocalPath(rawFrame.filePath, packageRoot).slice(0, 512),
|
|
388
|
-
lineno: Number(rawFrame.line),
|
|
389
|
-
colno: Number(rawFrame.column),
|
|
390
|
-
in_app: isInApp(rawFrame.filePath, packageRoot)
|
|
391
|
-
});
|
|
392
|
-
if (frames.length >= 64) break;
|
|
393
|
-
}
|
|
394
|
-
return frames.reverse();
|
|
395
|
-
}
|
|
396
|
-
var ERROR_CODE_SHAPE = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
397
|
-
function safeErrorCode(error) {
|
|
398
|
-
const code = Reflect.get(error, "code");
|
|
399
|
-
if (typeof code !== "string") return "";
|
|
400
|
-
return ERROR_CODE_SHAPE.test(code) ? code : "";
|
|
401
|
-
}
|
|
402
|
-
function errorParts(error) {
|
|
403
|
-
if (error instanceof Error) return {
|
|
404
|
-
type: error.name || "Error",
|
|
405
|
-
code: safeErrorCode(error),
|
|
406
|
-
stack: error.stack ?? ""
|
|
407
|
-
};
|
|
408
|
-
return {
|
|
409
|
-
type: "NonError",
|
|
410
|
-
code: "",
|
|
411
|
-
stack: ""
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
function exceptionPropertiesOf({ type, code, handled, level, frames }) {
|
|
415
|
-
return {
|
|
416
|
-
$exception_list: [{
|
|
417
|
-
type: type.slice(0, 512),
|
|
418
|
-
value: code,
|
|
419
|
-
mechanism: {
|
|
420
|
-
handled,
|
|
421
|
-
synthetic: false,
|
|
422
|
-
type: "generic"
|
|
423
|
-
},
|
|
424
|
-
stacktrace: {
|
|
425
|
-
type: "raw",
|
|
426
|
-
frames
|
|
427
|
-
}
|
|
428
|
-
}],
|
|
429
|
-
$exception_level: level
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
function buildExceptionProperties(error, { handled, packageRoot }) {
|
|
433
|
-
const parts = errorParts(error);
|
|
434
|
-
return exceptionPropertiesOf({
|
|
435
|
-
type: parts.type,
|
|
436
|
-
code: parts.code,
|
|
437
|
-
handled,
|
|
438
|
-
level: handled ? "error" : "fatal",
|
|
439
|
-
frames: parseV8StackFrames(parts.stack, packageRoot)
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
var BROWSER_FRAME_GECKO = /^\s*([^@]*)@(.+?):(\d+):(\d+)$/;
|
|
443
|
-
var URL_ORIGIN = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/]*/;
|
|
444
|
-
var SAFE_ERROR_TYPE_NAME = /^[A-Za-z_$][\w$]{0,127}$/;
|
|
445
|
-
function isWebUrl(rawUrl) {
|
|
446
|
-
return /^https?:\/\//i.test(rawUrl);
|
|
447
|
-
}
|
|
448
|
-
function lastPathSegment(urlPath) {
|
|
449
|
-
return urlPath.slice(urlPath.lastIndexOf("/") + 1);
|
|
450
|
-
}
|
|
451
|
-
function scrubWebPath(webPath) {
|
|
452
|
-
const nodeModulesAt = webPath.lastIndexOf("/node_modules/");
|
|
453
|
-
if (nodeModulesAt >= 0) return webPath.slice(nodeModulesAt + 1);
|
|
454
|
-
if (webPath.startsWith("/@fs/")) return lastPathSegment(webPath);
|
|
455
|
-
return webPath;
|
|
456
|
-
}
|
|
457
|
-
function urlPathOnly(rawUrl) {
|
|
458
|
-
const [withoutQueryOrHash] = rawUrl.split(/[?#]/, 1);
|
|
459
|
-
if (isWebUrl(withoutQueryOrHash)) return scrubWebPath(withoutQueryOrHash.replace(URL_ORIGIN, "") || "/");
|
|
460
|
-
return lastPathSegment(withoutQueryOrHash);
|
|
461
|
-
}
|
|
462
|
-
function matchBrowserFrame(stackLine) {
|
|
463
|
-
const v8Frame = matchV8Frame(stackLine);
|
|
464
|
-
if (v8Frame) return v8Frame;
|
|
465
|
-
const gecko = BROWSER_FRAME_GECKO.exec(stackLine);
|
|
466
|
-
if (!gecko) return null;
|
|
467
|
-
return {
|
|
468
|
-
functionName: gecko[1] || "?",
|
|
469
|
-
filePath: gecko[2],
|
|
470
|
-
line: gecko[3],
|
|
471
|
-
column: gecko[4]
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
function parseBrowserStackFrames(stack) {
|
|
475
|
-
const frames = [];
|
|
476
|
-
for (const stackLine of stack.slice(0, MAX_STACK_LENGTH).split("\n")) {
|
|
477
|
-
const rawFrame = matchBrowserFrame(stackLine);
|
|
478
|
-
if (!rawFrame) continue;
|
|
479
|
-
frames.push({
|
|
480
|
-
platform: "web:javascript",
|
|
481
|
-
function: rawFrame.functionName.slice(0, 512),
|
|
482
|
-
filename: urlPathOnly(rawFrame.filePath).slice(0, 512),
|
|
483
|
-
lineno: Number(rawFrame.line),
|
|
484
|
-
colno: Number(rawFrame.column),
|
|
485
|
-
in_app: isWebUrl(rawFrame.filePath)
|
|
486
|
-
});
|
|
487
|
-
if (frames.length >= 64) break;
|
|
488
|
-
}
|
|
489
|
-
return frames.reverse();
|
|
490
|
-
}
|
|
491
|
-
function safeErrorTypeName(name) {
|
|
492
|
-
return SAFE_ERROR_TYPE_NAME.test(name) ? name : "Error";
|
|
493
|
-
}
|
|
494
|
-
function buildBrowserExceptionProperties({ name, stack }) {
|
|
495
|
-
return exceptionPropertiesOf({
|
|
496
|
-
type: safeErrorTypeName(name),
|
|
497
|
-
code: "",
|
|
498
|
-
handled: false,
|
|
499
|
-
level: "error",
|
|
500
|
-
frames: parseBrowserStackFrames(stack)
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
function exceptionFingerprint(properties) {
|
|
504
|
-
const [exception] = properties.$exception_list;
|
|
505
|
-
const inAppFrames = exception.stacktrace.frames.filter((frame) => frame.in_app).map((frame) => `${frame.function}@${frame.filename}`);
|
|
506
|
-
return [exception.type, ...inAppFrames].join("|");
|
|
507
|
-
}
|
|
508
|
-
function isBuiltinAgentId(agentId) {
|
|
509
|
-
return BUILTIN_AGENT_IDS.some((builtinAgentId) => builtinAgentId === agentId);
|
|
510
|
-
}
|
|
511
|
-
function adapterBucket(agentId) {
|
|
512
|
-
if (isBuiltinAgentId(agentId)) return agentId;
|
|
513
|
-
return "custom";
|
|
514
|
-
}
|
|
515
|
-
function classifySessionExit({ exitCode, signal, reason }) {
|
|
516
|
-
if (reason === "no_output_before_exit") return "no_output";
|
|
517
|
-
if (signal) return "signal";
|
|
518
|
-
if (exitCode === 0) return "clean";
|
|
519
|
-
return "error";
|
|
520
|
-
}
|
|
521
|
-
var AI_PROVIDER_BY_VENDOR = Object.freeze({
|
|
522
|
-
claude: "anthropic",
|
|
523
|
-
codex: "openai",
|
|
524
|
-
grok: "xai"
|
|
525
|
-
});
|
|
526
|
-
var AGENT_ID_BY_VENDOR = Object.freeze({
|
|
527
|
-
claude: "claude-code",
|
|
528
|
-
codex: "codex",
|
|
529
|
-
grok: "grok"
|
|
530
|
-
});
|
|
531
|
-
function aiTraceId(installId, sessionId) {
|
|
532
|
-
return createHash("sha256").update(`${installId}${sessionId}`).digest("hex");
|
|
533
|
-
}
|
|
534
|
-
function wholeTokenCount(tokens) {
|
|
535
|
-
return Math.max(0, Math.round(tokens));
|
|
536
|
-
}
|
|
537
|
-
function buildAiGenerationEvent(row, installId) {
|
|
538
|
-
const properties = {
|
|
539
|
-
$ai_trace_id: aiTraceId(installId, row.sessionId),
|
|
540
|
-
$ai_provider: (AI_PROVIDER_BY_VENDOR[row.vendor] ?? row.vendor).slice(0, 64),
|
|
541
|
-
$ai_model: row.isModelKnown ? row.model.slice(0, 128) : "unknown",
|
|
542
|
-
$ai_input_tokens: wholeTokenCount(row.input),
|
|
543
|
-
$ai_output_tokens: wholeTokenCount(row.output),
|
|
544
|
-
$ai_cache_read_input_tokens: wholeTokenCount(row.cacheRead),
|
|
545
|
-
$ai_cache_creation_input_tokens: wholeTokenCount(row.cacheCreate),
|
|
546
|
-
agent_adapter: adapterBucket(AGENT_ID_BY_VENDOR[row.vendor])
|
|
547
|
-
};
|
|
548
|
-
if (!row.hasKnownCost) return properties;
|
|
549
|
-
return {
|
|
550
|
-
...properties,
|
|
551
|
-
$ai_total_cost_usd: Math.max(0, row.costUSD)
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
function hasAnyTokens(row) {
|
|
555
|
-
return row.input + row.output + row.cacheRead + row.cacheCreate > 0;
|
|
556
|
-
}
|
|
557
|
-
function buildAiGenerationEvents(rows, installId) {
|
|
558
|
-
return rows.filter(hasAnyTokens).map((row) => buildAiGenerationEvent(row, installId));
|
|
559
|
-
}
|
|
560
|
-
function decideRemoteTelemetryState(previous, response) {
|
|
561
|
-
const parsed = FeatureFlagsResponse.safeParse(response);
|
|
562
|
-
if (!parsed.success || parsed.data.errorsWhileComputingFlags === true) return previous;
|
|
563
|
-
const killSwitchFlag = parsed.data.flags[TELEMETRY_KILL_SWITCH_FLAG];
|
|
564
|
-
if (!killSwitchFlag) return previous;
|
|
565
|
-
return killSwitchFlag.enabled ? "enabled" : "disabled";
|
|
566
|
-
}
|
|
567
|
-
var POSTHOG_RELEASE_ID_SHAPE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
568
|
-
function parsePosthogReleaseId(value) {
|
|
569
|
-
if (typeof value !== "string") return null;
|
|
570
|
-
return POSTHOG_RELEASE_ID_SHAPE.test(value) ? value : null;
|
|
571
|
-
}
|
|
572
|
-
function decideIsDevInstall({ installFlavor, isBundled, homeDir, defaultHomeDir }) {
|
|
573
|
-
if (installFlavor === "clone" || !isBundled) return true;
|
|
574
|
-
return comparablePath(homeDir) !== comparablePath(defaultHomeDir);
|
|
575
|
-
}
|
|
576
|
-
function buildFactoryDecisionEvent(decision) {
|
|
577
|
-
return {
|
|
578
|
-
kind: decision.kind,
|
|
579
|
-
outcome: decision.outcome,
|
|
580
|
-
failure_class: decision.failureClass,
|
|
581
|
-
duration_ms: decision.durationMs,
|
|
582
|
-
attempt: decision.attempt
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
function nodeMajorVersion(nodeVersion) {
|
|
586
|
-
const major = Number.parseInt(nodeVersion.replace(/^v/, ""), 10);
|
|
587
|
-
return Number.isFinite(major) ? major : 0;
|
|
588
|
-
}
|
|
589
|
-
//#endregion
|
|
590
|
-
//#region shared/display-text.ts
|
|
591
|
-
var SECONDS_PER_MINUTE = 60;
|
|
592
|
-
var HOURS_PER_DAY = 24;
|
|
593
|
-
var UNIT_WORDS = {
|
|
594
|
-
compact: {
|
|
595
|
-
second: (count) => `${count}s`,
|
|
596
|
-
minute: (count) => `${count}m`,
|
|
597
|
-
hour: (count) => `${count}h`,
|
|
598
|
-
day: (count) => `${count}d`
|
|
599
|
-
},
|
|
600
|
-
abbreviated: {
|
|
601
|
-
second: (count) => `${count} sec`,
|
|
602
|
-
minute: (count) => `${count} min`,
|
|
603
|
-
hour: (count) => `${count} hr`,
|
|
604
|
-
day: (count) => `${count} ${count === 1 ? "day" : "days"}`
|
|
605
|
-
},
|
|
606
|
-
spelled: {
|
|
607
|
-
second: (count) => `${count} ${count === 1 ? "second" : "seconds"}`,
|
|
608
|
-
minute: (count) => `${count} ${count === 1 ? "minute" : "minutes"}`,
|
|
609
|
-
hour: (count) => `${count} ${count === 1 ? "hour" : "hours"}`,
|
|
610
|
-
day: (count) => `${count} ${count === 1 ? "day" : "days"}`
|
|
611
|
-
}
|
|
612
|
-
};
|
|
613
|
-
var COMPACT_FLOORED_AGO = {
|
|
614
|
-
rounding: "floor",
|
|
615
|
-
wording: "compact",
|
|
616
|
-
suffix: " ago",
|
|
617
|
-
justNowBelowSeconds: 0
|
|
618
|
-
};
|
|
619
|
-
function pad2(value) {
|
|
620
|
-
return String(value).padStart(2, "0");
|
|
621
|
-
}
|
|
622
|
-
function wholeSeconds(elapsedMs, rounding) {
|
|
623
|
-
return Math.max(0, Math[rounding](elapsedMs / 1e3));
|
|
624
|
-
}
|
|
625
|
-
function largestUnitText(totalSeconds, wording) {
|
|
626
|
-
const words = UNIT_WORDS[wording];
|
|
627
|
-
if (totalSeconds < SECONDS_PER_MINUTE) return words.second(totalSeconds);
|
|
628
|
-
const minutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE);
|
|
629
|
-
if (minutes < 60) return words.minute(minutes);
|
|
630
|
-
const hours = Math.floor(minutes / 60);
|
|
631
|
-
if (hours < HOURS_PER_DAY) return words.hour(hours);
|
|
632
|
-
return words.day(Math.floor(hours / HOURS_PER_DAY));
|
|
633
|
-
}
|
|
634
|
-
function elapsedText(elapsedMs, options) {
|
|
635
|
-
const totalSeconds = wholeSeconds(elapsedMs, options.rounding);
|
|
636
|
-
if (totalSeconds < options.justNowBelowSeconds) return "just now";
|
|
637
|
-
return `${largestUnitText(totalSeconds, options.wording)}${options.suffix}`;
|
|
638
|
-
}
|
|
639
|
-
function localDayKey(moment) {
|
|
640
|
-
const date = new Date(moment);
|
|
641
|
-
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
|
642
|
-
}
|
|
643
|
-
//#endregion
|
|
644
|
-
//#region server/ingest/core/ingest-core.ts
|
|
645
|
-
var SOURCE_NAMES = [
|
|
646
|
-
"terminal",
|
|
647
|
-
"agentLogs",
|
|
648
|
-
"git",
|
|
649
|
-
"fs",
|
|
650
|
-
"shellHistory",
|
|
651
|
-
"editor"
|
|
652
|
-
];
|
|
653
|
-
var SOURCE_DEFAULTS = Object.freeze({
|
|
654
|
-
terminal: Object.freeze({
|
|
655
|
-
maxEntries: 200,
|
|
656
|
-
maxBytes: 262144,
|
|
657
|
-
digestQuota: 8,
|
|
658
|
-
flushMs: 500,
|
|
659
|
-
accumulatorBytes: 8192,
|
|
660
|
-
windowBytes: 65536
|
|
661
|
-
}),
|
|
662
|
-
agentLogs: Object.freeze({
|
|
663
|
-
maxEntries: 200,
|
|
664
|
-
maxBytes: 131072,
|
|
665
|
-
digestQuota: 8,
|
|
666
|
-
pollMs: 2e3
|
|
667
|
-
}),
|
|
668
|
-
git: Object.freeze({
|
|
669
|
-
maxEntries: 100,
|
|
670
|
-
maxBytes: 65536,
|
|
671
|
-
digestQuota: 6,
|
|
672
|
-
debounceMs: 1e3,
|
|
673
|
-
pollMs: 6e4
|
|
674
|
-
}),
|
|
675
|
-
fs: Object.freeze({
|
|
676
|
-
maxEntries: 500,
|
|
677
|
-
maxBytes: 131072,
|
|
678
|
-
digestQuota: 8,
|
|
679
|
-
batchMs: 500
|
|
680
|
-
}),
|
|
681
|
-
shellHistory: Object.freeze({
|
|
682
|
-
maxEntries: 100,
|
|
683
|
-
maxBytes: 32768,
|
|
684
|
-
digestQuota: 6,
|
|
685
|
-
pollMs: 2e3
|
|
686
|
-
}),
|
|
687
|
-
editor: Object.freeze({
|
|
688
|
-
maxEntries: 100,
|
|
689
|
-
maxBytes: 32768,
|
|
690
|
-
digestQuota: 6
|
|
691
|
-
})
|
|
692
|
-
});
|
|
693
|
-
var KINDS_BY_SOURCE = Object.freeze({
|
|
694
|
-
terminal: Object.freeze(["output"]),
|
|
695
|
-
agentLogs: Object.freeze(["agent-turn", "agent-tool"]),
|
|
696
|
-
git: Object.freeze([
|
|
697
|
-
"commit",
|
|
698
|
-
"status-change",
|
|
699
|
-
"branch-change"
|
|
700
|
-
]),
|
|
701
|
-
fs: Object.freeze(["file-change"]),
|
|
702
|
-
shellHistory: Object.freeze(["command"]),
|
|
703
|
-
editor: Object.freeze([
|
|
704
|
-
"doc-save",
|
|
705
|
-
"doc-open",
|
|
706
|
-
"doc-close"
|
|
707
|
-
])
|
|
708
|
-
});
|
|
709
|
-
var MAX_DETAIL_CHARS = 1e3;
|
|
710
|
-
var MAX_DETAIL_KEYS = 12;
|
|
711
|
-
var DEFAULT_DIGEST_BUDGET_CHARS = 2e3;
|
|
712
|
-
var SCRUB_PLACEHOLDER = "[scrubbed]";
|
|
713
|
-
var DIGEST_HEADER = "Recent activity on this machine, newest first:";
|
|
714
|
-
var LIST_KEYS = Object.freeze({
|
|
715
|
-
fs: "roots",
|
|
716
|
-
shellHistory: "shells"
|
|
717
|
-
});
|
|
718
|
-
function stringList(raw) {
|
|
719
|
-
if (!Array.isArray(raw)) return [];
|
|
720
|
-
const values = [];
|
|
721
|
-
for (const entry of raw) {
|
|
722
|
-
if (typeof entry !== "string" || !entry.trim() || values.includes(entry.trim())) continue;
|
|
723
|
-
values.push(entry.trim());
|
|
724
|
-
}
|
|
725
|
-
return values;
|
|
726
|
-
}
|
|
727
|
-
function disabledSource(name) {
|
|
728
|
-
const source = {
|
|
729
|
-
enabled: false,
|
|
730
|
-
...SOURCE_DEFAULTS[name]
|
|
731
|
-
};
|
|
732
|
-
const listKey = LIST_KEYS[name];
|
|
733
|
-
if (listKey) source[listKey] = [];
|
|
734
|
-
return source;
|
|
735
|
-
}
|
|
736
|
-
function disabledConfig() {
|
|
737
|
-
const sources = {};
|
|
738
|
-
for (const name of SOURCE_NAMES) sources[name] = disabledSource(name);
|
|
739
|
-
return {
|
|
740
|
-
enabled: false,
|
|
741
|
-
sources
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
function resolveSource(name, raw) {
|
|
745
|
-
const defaults = SOURCE_DEFAULTS[name];
|
|
746
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return disabledSource(name);
|
|
747
|
-
const rawSource = raw;
|
|
748
|
-
const resolved = { enabled: rawSource.enabled === true };
|
|
749
|
-
for (const [key, value] of Object.entries(defaults)) resolved[key] = positiveIntOr(rawSource[key], value);
|
|
750
|
-
const listKey = LIST_KEYS[name];
|
|
751
|
-
if (listKey) resolved[listKey] = stringList(rawSource[listKey]);
|
|
752
|
-
return resolved;
|
|
753
|
-
}
|
|
754
|
-
function resolveIngestConfig(raw) {
|
|
755
|
-
if (!isRecord(raw)) return disabledConfig();
|
|
756
|
-
if (raw.enabled !== true) return disabledConfig();
|
|
757
|
-
const rawSourcesValue = raw.sources;
|
|
758
|
-
const rawSources = isRecord(rawSourcesValue) ? rawSourcesValue : {};
|
|
759
|
-
const sources = {};
|
|
760
|
-
for (const name of SOURCE_NAMES) sources[name] = resolveSource(name, rawSources[name]);
|
|
761
|
-
return {
|
|
762
|
-
enabled: true,
|
|
763
|
-
sources
|
|
764
|
-
};
|
|
765
|
-
}
|
|
766
|
-
function enabledSourceNames(config) {
|
|
767
|
-
if (!config || config.enabled !== true) return [];
|
|
768
|
-
return SOURCE_NAMES.filter((name) => config.sources[name] && config.sources[name].enabled === true);
|
|
769
|
-
}
|
|
770
|
-
var SECRET_WORD = "(?:password|passwd|pwd|token|api[_-]?key|secret|credential|auth[_-]?token|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|connection[_-]?string)";
|
|
771
|
-
var QUOTED_OR_BARE = "(?:\"[^\"\\r\\n]*\"|'[^'\\r\\n]*'|[^\\s\\r\\n]+)";
|
|
772
|
-
var GAP = "[^\\S\\r\\n]";
|
|
773
|
-
var FLAG_REACH = "[^\\r\\n;&|]{0,200}?";
|
|
774
|
-
var SCRUB_PATTERNS = [
|
|
775
|
-
new RegExp(`(${SECRET_WORD}${GAP}*(?:=>|[:=])${GAP}*)${QUOTED_OR_BARE}`, "gi"),
|
|
776
|
-
new RegExp(`((?:^|\\s)--?${SECRET_WORD}${GAP}+)${QUOTED_OR_BARE}`, "gi"),
|
|
777
|
-
new RegExp(`((?:^|\\s)-(?:pwd|pw)(?![a-z])${GAP}*=?${GAP}*)${QUOTED_OR_BARE}`, "g"),
|
|
778
|
-
new RegExp(`(-AsPlainText${GAP}+)${QUOTED_OR_BARE}`, "gi"),
|
|
779
|
-
new RegExp(`((?:^|\\s)setx${GAP}+[^\\s\\r\\n]*${SECRET_WORD}[^\\s\\r\\n]*${GAP}+)${QUOTED_OR_BARE}`, "gi"),
|
|
780
|
-
new RegExp(`(\\bBearer${GAP}+)[A-Za-z0-9._~+/=-]{8,}`, "gi"),
|
|
781
|
-
new RegExp(`(\\bauthorization${GAP}*:${GAP}*Basic${GAP}+)[A-Za-z0-9+/=_-]{8,}`, "gi"),
|
|
782
|
-
new RegExp(`(\\bdocker${GAP}+login\\b${FLAG_REACH}${GAP}-p(?:${GAP}+|=))${QUOTED_OR_BARE}`, "gi"),
|
|
783
|
-
new RegExp(`(\\bcurl\\b${FLAG_REACH}${GAP}(?:--user|-u)(?:${GAP}+|=)[^\\s:\\r\\n]+:)${QUOTED_OR_BARE}`, "gi"),
|
|
784
|
-
new RegExp(`(\\b(?:mysql|mysqldump|mysqladmin|mysqlshow|mariadb|mariadb-dump)\\b${FLAG_REACH}${GAP}-p)${QUOTED_OR_BARE}`, "g"),
|
|
785
|
-
/\b([a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:)[^\s@/]+(?=@)/gi
|
|
786
|
-
];
|
|
787
|
-
var SECRET_SHAPE_PATTERNS = [
|
|
788
|
-
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
789
|
-
/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g,
|
|
790
|
-
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
791
|
-
/\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g,
|
|
792
|
-
/\beyJ[A-Za-z0-9_=-]{6,}\.[A-Za-z0-9_=-]{6,}\.[A-Za-z0-9_=-]{4,}/g,
|
|
793
|
-
/-----BEGIN(?:[A-Z0-9 ]+)? PRIVATE KEY-----/g
|
|
794
|
-
];
|
|
795
|
-
function scrubText(text) {
|
|
796
|
-
if (typeof text !== "string" || !text) return typeof text === "string" ? text : "";
|
|
797
|
-
let scrubbed = text;
|
|
798
|
-
for (const pattern of SCRUB_PATTERNS) {
|
|
799
|
-
pattern.lastIndex = 0;
|
|
800
|
-
scrubbed = scrubbed.replace(pattern, (_match, prefix) => `${prefix}${SCRUB_PLACEHOLDER}`);
|
|
801
|
-
}
|
|
802
|
-
for (const pattern of SECRET_SHAPE_PATTERNS) {
|
|
803
|
-
pattern.lastIndex = 0;
|
|
804
|
-
scrubbed = scrubbed.replace(pattern, SCRUB_PLACEHOLDER);
|
|
805
|
-
}
|
|
806
|
-
return scrubbed;
|
|
807
|
-
}
|
|
808
|
-
function scrubDetail(detail) {
|
|
809
|
-
if (!detail || typeof detail !== "object" || Array.isArray(detail)) return null;
|
|
810
|
-
const scrubbed = {};
|
|
811
|
-
let keys = 0;
|
|
812
|
-
for (const [key, value] of Object.entries(detail)) {
|
|
813
|
-
if (keys >= MAX_DETAIL_KEYS) break;
|
|
814
|
-
keys += 1;
|
|
815
|
-
if (typeof value === "string") {
|
|
816
|
-
scrubbed[key] = scrubText(value).slice(0, MAX_DETAIL_CHARS);
|
|
817
|
-
continue;
|
|
818
|
-
}
|
|
819
|
-
if (typeof value === "number" && Number.isFinite(value)) scrubbed[key] = value;
|
|
820
|
-
if (typeof value === "boolean") scrubbed[key] = value;
|
|
821
|
-
}
|
|
822
|
-
return scrubbed;
|
|
823
|
-
}
|
|
824
|
-
function normalizeScope(raw) {
|
|
825
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {
|
|
826
|
-
root: null,
|
|
827
|
-
sessionId: null
|
|
828
|
-
};
|
|
829
|
-
const rawScope = raw;
|
|
830
|
-
return {
|
|
831
|
-
root: textOr(rawScope.root, null),
|
|
832
|
-
sessionId: textOr(rawScope.sessionId, null)
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
function normalizeEvent(raw, { seq, now }) {
|
|
836
|
-
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
837
|
-
const rawEvent = raw;
|
|
838
|
-
const source = textOr(rawEvent.source, null);
|
|
839
|
-
if (!source || !KINDS_BY_SOURCE[source]) return null;
|
|
840
|
-
const kind = textOr(rawEvent.kind, null);
|
|
841
|
-
if (!kind || !KINDS_BY_SOURCE[source].includes(kind)) return null;
|
|
842
|
-
const summary = scrubText(typeof rawEvent.summary === "string" ? rawEvent.summary : "").replace(/\s+/g, " ").trim().slice(0, 400);
|
|
843
|
-
if (!summary) return null;
|
|
844
|
-
const ts = Number(rawEvent.ts);
|
|
845
|
-
return {
|
|
846
|
-
source,
|
|
847
|
-
kind,
|
|
848
|
-
ts: Number.isFinite(ts) && ts > 0 ? Math.floor(ts) : now,
|
|
849
|
-
seq,
|
|
850
|
-
scope: normalizeScope(rawEvent.scope),
|
|
851
|
-
summary,
|
|
852
|
-
detail: scrubDetail(rawEvent.detail)
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
function eventBytes(event) {
|
|
856
|
-
return Buffer.byteLength(JSON.stringify(event), "utf8");
|
|
857
|
-
}
|
|
858
|
-
function createRing({ maxEntries, maxBytes }) {
|
|
859
|
-
return {
|
|
860
|
-
entries: [],
|
|
861
|
-
totalBytes: 0,
|
|
862
|
-
maxEntries,
|
|
863
|
-
maxBytes
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
function pushToRing(ring, event) {
|
|
867
|
-
const bytes = eventBytes(event);
|
|
868
|
-
ring.entries.push({
|
|
869
|
-
event,
|
|
870
|
-
bytes
|
|
871
|
-
});
|
|
872
|
-
ring.totalBytes += bytes;
|
|
873
|
-
while (ring.entries.length > 1 && (ring.entries.length > ring.maxEntries || ring.totalBytes > ring.maxBytes)) {
|
|
874
|
-
const evicted = ring.entries.shift();
|
|
875
|
-
ring.totalBytes -= evicted ? evicted.bytes : 0;
|
|
876
|
-
}
|
|
877
|
-
return event;
|
|
878
|
-
}
|
|
879
|
-
function createIngestStore(config) {
|
|
880
|
-
const resolved = config?.sources ? config : disabledConfig();
|
|
881
|
-
const rings = /* @__PURE__ */ new Map();
|
|
882
|
-
for (const name of enabledSourceNames(resolved)) rings.set(name, createRing(resolved.sources[name]));
|
|
883
|
-
return {
|
|
884
|
-
config: resolved,
|
|
885
|
-
rings,
|
|
886
|
-
seq: 0
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
function publishEvent(store, raw, now) {
|
|
890
|
-
const source = textOr((raw ?? null)?.source, null);
|
|
891
|
-
if (!source) return null;
|
|
892
|
-
const ring = store.rings.get(source);
|
|
893
|
-
if (!ring) return null;
|
|
894
|
-
const event = normalizeEvent(raw, {
|
|
895
|
-
seq: store.seq + 1,
|
|
896
|
-
now
|
|
897
|
-
});
|
|
898
|
-
if (!event) return null;
|
|
899
|
-
store.seq += 1;
|
|
900
|
-
return pushToRing(ring, event);
|
|
901
|
-
}
|
|
902
|
-
function latestSeq(store) {
|
|
903
|
-
const seq = Number(store?.seq);
|
|
904
|
-
if (!Number.isFinite(seq)) return 0;
|
|
905
|
-
return seq;
|
|
906
|
-
}
|
|
907
|
-
function snapshotEvents(store, { limit = 200 } = {}) {
|
|
908
|
-
const all = [];
|
|
909
|
-
for (const ring of store.rings.values()) for (const entry of ring.entries) all.push(entry.event);
|
|
910
|
-
all.sort((left, right) => right.seq - left.seq);
|
|
911
|
-
return all.slice(0, Math.max(0, Math.floor(limit)));
|
|
912
|
-
}
|
|
913
|
-
function ringStats(store) {
|
|
914
|
-
const stats = [];
|
|
915
|
-
for (const [source, ring] of store.rings) stats.push({
|
|
916
|
-
source,
|
|
917
|
-
events: ring.entries.length,
|
|
918
|
-
bytes: ring.totalBytes
|
|
919
|
-
});
|
|
920
|
-
return stats;
|
|
921
|
-
}
|
|
922
|
-
var SOURCE_LABELS = Object.freeze({
|
|
923
|
-
terminal: "terminal",
|
|
924
|
-
agentLogs: "agent",
|
|
925
|
-
git: "git",
|
|
926
|
-
fs: "files",
|
|
927
|
-
shellHistory: "shell",
|
|
928
|
-
editor: "editor"
|
|
929
|
-
});
|
|
930
|
-
function digestLine(event, now) {
|
|
931
|
-
const label = SOURCE_LABELS[event.source] || event.source;
|
|
932
|
-
const scope = event.scope.root ? "" : " (machine scope)";
|
|
933
|
-
return `- ${label} ${elapsedText(now - event.ts, COMPACT_FLOORED_AGO)}${scope}: ${event.summary}`;
|
|
934
|
-
}
|
|
935
|
-
function matchesScopes(event, scopes) {
|
|
936
|
-
if (!Array.isArray(scopes) || scopes.length === 0) return true;
|
|
937
|
-
if (!event.scope.root) return true;
|
|
938
|
-
return scopes.includes(event.scope.root);
|
|
939
|
-
}
|
|
940
|
-
function buildContextDigest(store, { scopes = null, budgetChars = DEFAULT_DIGEST_BUDGET_CHARS, now = NaN } = {}) {
|
|
941
|
-
const budget = positiveIntOr(budgetChars, DEFAULT_DIGEST_BUDGET_CHARS);
|
|
942
|
-
const candidates = [];
|
|
943
|
-
const takenBySource = /* @__PURE__ */ new Map();
|
|
944
|
-
for (const event of snapshotEvents(store, { limit: Number.MAX_SAFE_INTEGER })) {
|
|
945
|
-
if (!matchesScopes(event, scopes)) continue;
|
|
946
|
-
const quota = store.config.sources[event.source].digestQuota;
|
|
947
|
-
const taken = takenBySource.get(event.source) || 0;
|
|
948
|
-
if (taken >= quota) continue;
|
|
949
|
-
takenBySource.set(event.source, taken + 1);
|
|
950
|
-
candidates.push(event);
|
|
951
|
-
}
|
|
952
|
-
if (candidates.length === 0) return "";
|
|
953
|
-
const lines = [DIGEST_HEADER];
|
|
954
|
-
let used = 46;
|
|
955
|
-
for (const event of candidates) {
|
|
956
|
-
const line = scrubText(digestLine(event, now));
|
|
957
|
-
if (used + line.length + 1 > budget) break;
|
|
958
|
-
lines.push(line);
|
|
959
|
-
used += line.length + 1;
|
|
960
|
-
}
|
|
961
|
-
if (lines.length === 1) return "";
|
|
962
|
-
return lines.join("\n");
|
|
963
|
-
}
|
|
964
|
-
//#endregion
|
|
965
|
-
//#region server/core/settings-block-core.ts
|
|
966
|
-
var NO_BLOCKS = Object.freeze({});
|
|
967
|
-
var INGEST_SOURCES_SPEC = Object.freeze({
|
|
968
|
-
name: "ingest.sources",
|
|
969
|
-
booleans: Object.freeze([]),
|
|
970
|
-
blocks: Object.freeze(Object.fromEntries(SOURCE_NAMES.map((source) => [source, Object.freeze({
|
|
971
|
-
name: `ingest.sources.${source}`,
|
|
972
|
-
booleans: Object.freeze(["enabled"]),
|
|
973
|
-
blocks: NO_BLOCKS
|
|
974
|
-
})])))
|
|
975
|
-
});
|
|
976
|
-
var INGEST_SPEC = Object.freeze({
|
|
977
|
-
name: "ingest",
|
|
978
|
-
booleans: Object.freeze(["enabled"]),
|
|
979
|
-
blocks: Object.freeze({ sources: INGEST_SOURCES_SPEC })
|
|
980
|
-
});
|
|
981
|
-
function validateSettingsBlock(block, spec) {
|
|
982
|
-
if (block == null) return null;
|
|
983
|
-
if (!isRecord(block)) return `${spec.name} must be an object`;
|
|
984
|
-
for (const [key, value] of Object.entries(block)) {
|
|
985
|
-
if (value == null) continue;
|
|
986
|
-
if (spec.booleans.includes(key)) {
|
|
987
|
-
if (typeof value !== "boolean") return `${spec.name}.${key} must be a boolean`;
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
if (Object.hasOwn(spec.blocks, key)) {
|
|
991
|
-
const error = validateSettingsBlock(value, spec.blocks[key]);
|
|
992
|
-
if (error) return error;
|
|
993
|
-
continue;
|
|
994
|
-
}
|
|
995
|
-
return `${spec.name}.${key} is not settable from the dashboard`;
|
|
996
|
-
}
|
|
997
|
-
return null;
|
|
998
|
-
}
|
|
999
|
-
function pickSettingsBlock(stored, spec) {
|
|
1000
|
-
if (!isRecord(stored)) return null;
|
|
1001
|
-
const fields = stored;
|
|
1002
|
-
const out = {};
|
|
1003
|
-
for (const key of spec.booleans) if (fields[key] != null) out[key] = !!fields[key];
|
|
1004
|
-
for (const [key, nested] of Object.entries(spec.blocks)) {
|
|
1005
|
-
const picked = pickSettingsBlock(fields[key], nested);
|
|
1006
|
-
if (picked) out[key] = picked;
|
|
1007
|
-
}
|
|
1008
|
-
return out;
|
|
1009
|
-
}
|
|
1010
|
-
function mergeSettingsBlock(stored, incoming, spec) {
|
|
1011
|
-
const out = isRecord(stored) ? { ...stored } : {};
|
|
1012
|
-
if (!isRecord(incoming)) return out;
|
|
1013
|
-
const fields = incoming;
|
|
1014
|
-
for (const key of spec.booleans) if (fields[key] != null) out[key] = !!fields[key];
|
|
1015
|
-
for (const [key, nested] of Object.entries(spec.blocks)) {
|
|
1016
|
-
if (fields[key] == null) continue;
|
|
1017
|
-
out[key] = mergeSettingsBlock(out[key], fields[key], nested);
|
|
1018
|
-
}
|
|
1019
|
-
return out;
|
|
1020
|
-
}
|
|
1021
|
-
//#endregion
|
|
1022
|
-
//#region server/core/rtk-install-core.ts
|
|
1023
|
-
var RTK_VERSION = "0.45.0";
|
|
1024
|
-
var RTK_RELEASE_BASE_URL = "https://github.com/rtk-ai/rtk/releases/download/v0.45.0/";
|
|
1025
|
-
var RTK_ASSETS = {
|
|
1026
|
-
"linux-x64": {
|
|
1027
|
-
file: "rtk-x86_64-unknown-linux-musl.tar.gz",
|
|
1028
|
-
sha256: "c4c036fbf181fc55ef329786c8c17e0d427972b053b825944d968a6aafef1ba4"
|
|
1029
|
-
},
|
|
1030
|
-
"linux-arm64": {
|
|
1031
|
-
file: "rtk-aarch64-unknown-linux-gnu.tar.gz",
|
|
1032
|
-
sha256: "80a746dd305ef944ff50ef011ae4ce3878dd5ba88dfe35d859d05498191637c3"
|
|
1033
|
-
},
|
|
1034
|
-
"win32-x64": {
|
|
1035
|
-
file: "rtk-x86_64-pc-windows-msvc.zip",
|
|
1036
|
-
sha256: "34cea9009a8099acdaf85147b971d95f65efabfa63fb3aea7d3e2b73e6f517c3"
|
|
1037
|
-
},
|
|
1038
|
-
"darwin-x64": {
|
|
1039
|
-
file: "rtk-x86_64-apple-darwin.tar.gz",
|
|
1040
|
-
sha256: "9ea02f889d5a2779e4fb700df4587824303c5a57cda22e903e30058079fca0ef"
|
|
1041
|
-
},
|
|
1042
|
-
"darwin-arm64": {
|
|
1043
|
-
file: "rtk-aarch64-apple-darwin.tar.gz",
|
|
1044
|
-
sha256: "064151cfc2d50b24d810b06a0af2e41b9c945e83534e4c438c3d3eae607fc3f4"
|
|
1045
|
-
}
|
|
1046
|
-
};
|
|
1047
|
-
var INSTALL_FAILURE_COOLDOWN_MS = 6e5;
|
|
1048
|
-
function assetForPlatform(platform, arch) {
|
|
1049
|
-
const entry = RTK_ASSETS[`${platform}-${arch}`];
|
|
1050
|
-
if (!entry) return null;
|
|
1051
|
-
return {
|
|
1052
|
-
...entry,
|
|
1053
|
-
version: RTK_VERSION,
|
|
1054
|
-
url: `${RTK_RELEASE_BASE_URL}${entry.file}`
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
function decideRtkInstall({ rtkEnabled, resolvedPath, platform, arch, inFlight = false, lastFailureAt = null, nowMs = 0, cooldownMs = INSTALL_FAILURE_COOLDOWN_MS } = {}) {
|
|
1058
|
-
if (!rtkEnabled) return {
|
|
1059
|
-
action: "skip",
|
|
1060
|
-
reason: "rtk-disabled"
|
|
1061
|
-
};
|
|
1062
|
-
if (resolvedPath) return {
|
|
1063
|
-
action: "skip",
|
|
1064
|
-
reason: "already-resolved"
|
|
1065
|
-
};
|
|
1066
|
-
if (inFlight) return {
|
|
1067
|
-
action: "skip",
|
|
1068
|
-
reason: "install-in-flight"
|
|
1069
|
-
};
|
|
1070
|
-
const asset = assetForPlatform(String(platform), String(arch));
|
|
1071
|
-
if (!asset) return {
|
|
1072
|
-
action: "skip",
|
|
1073
|
-
reason: `unsupported-platform:${platform}-${arch}`
|
|
1074
|
-
};
|
|
1075
|
-
if (typeof lastFailureAt === "number" && nowMs - lastFailureAt < cooldownMs) return {
|
|
1076
|
-
action: "skip",
|
|
1077
|
-
reason: "failure-cooldown"
|
|
1078
|
-
};
|
|
1079
|
-
return {
|
|
1080
|
-
action: "install",
|
|
1081
|
-
reason: "missing-binary",
|
|
1082
|
-
asset
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
function verifyDigest(expectedHex, actualHex) {
|
|
1086
|
-
if (typeof expectedHex !== "string" || typeof actualHex !== "string") return false;
|
|
1087
|
-
if (expectedHex.length !== actualHex.length) return false;
|
|
1088
|
-
return crypto.timingSafeEqual(Buffer.from(expectedHex, "utf8"), Buffer.from(actualHex, "utf8"));
|
|
1089
|
-
}
|
|
1090
|
-
function installTargetPath(glimmervoidHome, platform) {
|
|
1091
|
-
const binaryName = platform === "win32" ? "rtk.exe" : "rtk";
|
|
1092
|
-
return path.join(glimmervoidHome, "bin", binaryName);
|
|
1093
|
-
}
|
|
1094
|
-
function isRtkBinaryName(name, platform) {
|
|
1095
|
-
const wanted = platform === "win32" ? "rtk.exe" : "rtk";
|
|
1096
|
-
return name.toLowerCase() === wanted;
|
|
1097
|
-
}
|
|
1098
|
-
function findEscapingArchiveMember(listingText) {
|
|
1099
|
-
const members = String(listingText || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1100
|
-
for (const member of members) {
|
|
1101
|
-
if (member.split(/[\\/]+/).includes("..")) return member;
|
|
1102
|
-
if (member.startsWith("/") || member.startsWith("\\") || /^[A-Za-z]:/.test(member)) return member;
|
|
1103
|
-
}
|
|
1104
|
-
return null;
|
|
1105
|
-
}
|
|
1106
|
-
//#endregion
|
|
1107
|
-
//#region server/config-store.ts
|
|
1108
|
-
var DEFAULT_CONFIG = {
|
|
1109
|
-
port: 3e3,
|
|
1110
|
-
autoRecoverSeconds: 3,
|
|
1111
|
-
inputGraceSeconds: 5,
|
|
1112
|
-
promptDetectionMs: 1500,
|
|
1113
|
-
notifyDebounceMs: 3e3,
|
|
1114
|
-
phoneEscalationMs: 3e5,
|
|
1115
|
-
cursorBlink: false,
|
|
1116
|
-
debugMode: false,
|
|
1117
|
-
calmLayout: false,
|
|
1118
|
-
detectBackgroundAgents: true,
|
|
1119
|
-
detectScheduledWakeups: true,
|
|
1120
|
-
replayBufferKB: 512,
|
|
1121
|
-
recordSignals: true,
|
|
1122
|
-
trace: { enabled: true },
|
|
1123
|
-
planReview: { enabled: true },
|
|
1124
|
-
taskTitle: { refiner: {
|
|
1125
|
-
enabled: true,
|
|
1126
|
-
model: "haiku",
|
|
1127
|
-
minIntervalSeconds: 60,
|
|
1128
|
-
timeoutSeconds: 60
|
|
1129
|
-
} },
|
|
1130
|
-
changeMap: { narrator: {
|
|
1131
|
-
enabled: false,
|
|
1132
|
-
engine: "claude",
|
|
1133
|
-
model: "",
|
|
1134
|
-
timeoutSeconds: 90
|
|
1135
|
-
} },
|
|
1136
|
-
factory: {
|
|
1137
|
-
enabled: false,
|
|
1138
|
-
maxRisk: "medium",
|
|
1139
|
-
maxLiveWorkers: 2,
|
|
1140
|
-
checks: DEFAULT_FACTORY_CHECKS,
|
|
1141
|
-
protectedPaths: DEFAULT_FACTORY_PROTECTED_PATHS,
|
|
1142
|
-
reviewerModel: null,
|
|
1143
|
-
dailyBudgetUsd: null,
|
|
1144
|
-
verifierModel: null,
|
|
1145
|
-
mirrorHarnessRuns: false
|
|
1146
|
-
},
|
|
1147
|
-
guardrails: {
|
|
1148
|
-
unguardedRepos: [],
|
|
1149
|
-
blockedPaths: [],
|
|
1150
|
-
unreadablePaths: []
|
|
1151
|
-
},
|
|
1152
|
-
telemetry: { enabled: true },
|
|
1153
|
-
checkForUpdates: true,
|
|
1154
|
-
updateChannel: "release",
|
|
1155
|
-
autoResume: true,
|
|
1156
|
-
skipPermissionsByDefault: false,
|
|
1157
|
-
rtk: false,
|
|
1158
|
-
saneYolo: true,
|
|
1159
|
-
telegramNotifications: false,
|
|
1160
|
-
integrationBranch: null,
|
|
1161
|
-
worktreeRoot: "",
|
|
1162
|
-
worktreeShare: [
|
|
1163
|
-
"node_modules",
|
|
1164
|
-
".env",
|
|
1165
|
-
".env.local",
|
|
1166
|
-
".claude"
|
|
1167
|
-
],
|
|
1168
|
-
worktreeAutoRebase: true,
|
|
1169
|
-
worktreeSyncOnStart: true,
|
|
1170
|
-
worktreeRerere: true,
|
|
1171
|
-
branchGc: {
|
|
1172
|
-
enabled: true,
|
|
1173
|
-
worktrees: true,
|
|
1174
|
-
prefixes: [...DEFAULT_BRANCH_GC_PREFIXES],
|
|
1175
|
-
dryRun: false,
|
|
1176
|
-
staleDays: 14,
|
|
1177
|
-
deleteUnmerged: false,
|
|
1178
|
-
intervalMs: 216e5
|
|
1179
|
-
},
|
|
1180
|
-
repoRoots: [],
|
|
1181
|
-
customAgents: [],
|
|
1182
|
-
postTurnChecks: {
|
|
1183
|
-
enabled: true,
|
|
1184
|
-
mode: "report",
|
|
1185
|
-
rules: {
|
|
1186
|
-
trailingWs: true,
|
|
1187
|
-
finalNewline: true,
|
|
1188
|
-
bom: true,
|
|
1189
|
-
slop: false
|
|
1190
|
-
}
|
|
1191
|
-
},
|
|
1192
|
-
remote: {
|
|
1193
|
-
enabled: false,
|
|
1194
|
-
port: null,
|
|
1195
|
-
publicHost: "",
|
|
1196
|
-
allowedOrigins: []
|
|
1197
|
-
},
|
|
1198
|
-
workflows: {
|
|
1199
|
-
enabled: true,
|
|
1200
|
-
maxConcurrentSessions: 2,
|
|
1201
|
-
maxActionsPerPoll: 20,
|
|
1202
|
-
rules: []
|
|
1203
|
-
},
|
|
1204
|
-
projects: []
|
|
1205
|
-
};
|
|
1206
|
-
var DEFAULT_CONFIG_BY_KEY = DEFAULT_CONFIG;
|
|
1207
|
-
var ABORT_CONFIG_SAVE = "abort-config-save";
|
|
1208
|
-
var CONFIG_DIR_MODE = 448;
|
|
1209
|
-
function glimmervoidHomeDir() {
|
|
1210
|
-
return glimmervoidHomeDir$1(os.homedir(), process.env);
|
|
1211
|
-
}
|
|
1212
|
-
function restrictMode(target, mode) {
|
|
1213
|
-
try {
|
|
1214
|
-
fs.chmodSync(target, mode);
|
|
1215
|
-
} catch {}
|
|
1216
|
-
}
|
|
1217
|
-
function resolveConfigPath() {
|
|
1218
|
-
const homeDir = glimmervoidHomeDir();
|
|
1219
|
-
const decided = decideConfigPath({
|
|
1220
|
-
env: process.env,
|
|
1221
|
-
homeDir
|
|
1222
|
-
}, (candidate) => fs.existsSync(candidate));
|
|
1223
|
-
if (decided.path) return decided.path;
|
|
1224
|
-
if (decided.source === "env") {
|
|
1225
|
-
console.error(`Config file not found: ${decided.envPath}`);
|
|
1226
|
-
process.exit(1);
|
|
1227
|
-
}
|
|
1228
|
-
const homeConfig = decided.homePath;
|
|
1229
|
-
fs.mkdirSync(homeDir, {
|
|
1230
|
-
recursive: true,
|
|
1231
|
-
mode: CONFIG_DIR_MODE
|
|
1232
|
-
});
|
|
1233
|
-
restrictMode(homeDir, CONFIG_DIR_MODE);
|
|
1234
|
-
writeJsonAtomicSync(homeConfig, DEFAULT_CONFIG, {
|
|
1235
|
-
mode: 384,
|
|
1236
|
-
enforceMode: true
|
|
1237
|
-
});
|
|
1238
|
-
console.log(`Created default config at ${homeConfig}`);
|
|
1239
|
-
return homeConfig;
|
|
1240
|
-
}
|
|
1241
|
-
function generateProjectId() {
|
|
1242
|
-
return crypto.randomUUID();
|
|
1243
|
-
}
|
|
1244
|
-
function validateConfig(candidate) {
|
|
1245
|
-
if (!isRecord(candidate)) return {
|
|
1246
|
-
ok: false,
|
|
1247
|
-
errors: ["config must be a plain object"]
|
|
1248
|
-
};
|
|
1249
|
-
const parsedConfig = Config.safeParse(candidate);
|
|
1250
|
-
if (!parsedConfig.success) return {
|
|
1251
|
-
ok: false,
|
|
1252
|
-
errors: parsedConfig.error.issues.map((issue) => {
|
|
1253
|
-
if (issue.code === "custom") return issue.message;
|
|
1254
|
-
const [root, index, field] = issue.path;
|
|
1255
|
-
if (root === "port") return "port must be an integer from 0 to 65535";
|
|
1256
|
-
if (root === "replayBufferKB") return issue.message;
|
|
1257
|
-
if (root === "repoRoots" || root === "worktreeShare") return `${root} must be an array of strings`;
|
|
1258
|
-
if (root === "remote") return "remote must be a plain object";
|
|
1259
|
-
if (root !== "projects" && typeof DEFAULT_CONFIG_BY_KEY[String(root)] === "number") return `${String(root)} must be a finite number greater than or equal to 0`;
|
|
1260
|
-
if (root !== "projects") return issue.message;
|
|
1261
|
-
if (typeof index !== "number") return "projects must be an array";
|
|
1262
|
-
if (!field) return `projects[${index}] must be a plain object`;
|
|
1263
|
-
if (field === "agent") return `projects[${index}].agent must be ${AGENT_ID_SHAPE_MESSAGE}`;
|
|
1264
|
-
if (field === "codexBypassHookTrust") return `projects[${index}].codexBypassHookTrust must be a boolean`;
|
|
1265
|
-
return `projects[${index}].${String(field)} must be a string`;
|
|
1266
|
-
})
|
|
1267
|
-
};
|
|
1268
|
-
return {
|
|
1269
|
-
ok: true,
|
|
1270
|
-
config: parsedConfig.data
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
function normalizeConfigFile(candidate) {
|
|
1274
|
-
if (!isRecord(candidate)) throw new Error("config must be a plain object");
|
|
1275
|
-
const draft = normalizeLegacyConfigNumbers(candidate);
|
|
1276
|
-
for (const [key, fallback] of Object.entries(DEFAULT_CONFIG_BY_KEY)) {
|
|
1277
|
-
if (fallback === null || typeof fallback === "object") continue;
|
|
1278
|
-
if (!Object.hasOwn(draft, key)) continue;
|
|
1279
|
-
const fieldSchema = Config.shape[key];
|
|
1280
|
-
if (!fieldSchema || fieldSchema.safeParse(draft[key]).success) continue;
|
|
1281
|
-
console.warn(`[config] ${key} value ${JSON.stringify(draft[key])} is invalid; using ${JSON.stringify(fallback)}`);
|
|
1282
|
-
draft[key] = fallback;
|
|
1283
|
-
}
|
|
1284
|
-
const validation = validateConfig(draft);
|
|
1285
|
-
if (!validation.ok) throw new Error(`validation failed: ${validation.errors.join("; ")}`);
|
|
1286
|
-
return validation.config;
|
|
1287
|
-
}
|
|
1288
|
-
function writeBackupContent(backupPath, content) {
|
|
1289
|
-
try {
|
|
1290
|
-
writeTextAtomicSync(backupPath, content, {
|
|
1291
|
-
mode: 384,
|
|
1292
|
-
enforceMode: true
|
|
1293
|
-
});
|
|
1294
|
-
} catch (err) {
|
|
1295
|
-
console.warn(`[config] Failed to write backup ${backupPath}:`, errorLabel(err));
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
function loadConfigFile(configPath, { exitOnError = true } = {}) {
|
|
1299
|
-
const loadedContent = fs.readFileSync(configPath, "utf8");
|
|
1300
|
-
try {
|
|
1301
|
-
return {
|
|
1302
|
-
config: withEnvSecrets(normalizeConfigFile(JSON.parse(loadedContent)), readEnvSecrets(process.env)),
|
|
1303
|
-
loadedContent
|
|
1304
|
-
};
|
|
1305
|
-
} catch (err) {
|
|
1306
|
-
const invalidBackupPath = `${configPath}.invalid.bak`;
|
|
1307
|
-
try {
|
|
1308
|
-
writeTextAtomicSync(invalidBackupPath, loadedContent, {
|
|
1309
|
-
mode: 384,
|
|
1310
|
-
enforceMode: true
|
|
1311
|
-
});
|
|
1312
|
-
} catch (backupErr) {
|
|
1313
|
-
console.warn(`[config] Failed to save invalid config copy ${invalidBackupPath}:`, errorLabel(backupErr));
|
|
1314
|
-
}
|
|
1315
|
-
const message = `[config] Could not load ${configPath}: ${errorMessage(err)}. The broken file was copied to ${invalidBackupPath} when possible. Restore from ${configPath}.boot.bak or ${configPath}.bak, then restart Glimmervoid.`;
|
|
1316
|
-
if (!exitOnError) return {
|
|
1317
|
-
error: err,
|
|
1318
|
-
message,
|
|
1319
|
-
invalidBackupPath
|
|
1320
|
-
};
|
|
1321
|
-
console.error(message);
|
|
1322
|
-
process.exit(1);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
function topLevelKeyCount(candidate) {
|
|
1326
|
-
if (!isRecord(candidate)) return 0;
|
|
1327
|
-
return Object.keys(candidate).length;
|
|
1328
|
-
}
|
|
1329
|
-
function isSuspectedExternalWipe(candidate, currentConfig) {
|
|
1330
|
-
const currentKeyCount = topLevelKeyCount(currentConfig);
|
|
1331
|
-
if (currentKeyCount === 0) return false;
|
|
1332
|
-
return topLevelKeyCount({
|
|
1333
|
-
...candidate,
|
|
1334
|
-
branchGc: resolveBranchGc(candidate.branchGc)
|
|
1335
|
-
}) * 2 < currentKeyCount;
|
|
1336
|
-
}
|
|
1337
|
-
function warnInvalidConfig(action, validation) {
|
|
1338
|
-
console.warn(`[config] Refusing to ${action}; validation failed: ${validation.errors.join("; ")}. Recovery sources: config.json.bak and config.json.boot.bak.`);
|
|
1339
|
-
}
|
|
1340
|
-
function warnSuspectedWipe(action) {
|
|
1341
|
-
console.warn(`[config] Refusing to ${action}; config.json has fewer than half the top-level keys of the in-memory config. This looks like an external wipe. Recovery sources: config.json.bak and config.json.boot.bak.`);
|
|
1342
|
-
}
|
|
1343
|
-
function ensureProjectIds(projects) {
|
|
1344
|
-
let changed = false;
|
|
1345
|
-
for (const p of projects) if (!p.id) {
|
|
1346
|
-
p.id = generateProjectId();
|
|
1347
|
-
changed = true;
|
|
1348
|
-
}
|
|
1349
|
-
return changed;
|
|
1350
|
-
}
|
|
1351
|
-
function collectBranchGcIssues(branchGc) {
|
|
1352
|
-
if (!isRecord(branchGc)) return {
|
|
1353
|
-
block: { ...DEFAULT_CONFIG.branchGc },
|
|
1354
|
-
issues: []
|
|
1355
|
-
};
|
|
1356
|
-
const accepted = {};
|
|
1357
|
-
const issues = [];
|
|
1358
|
-
for (const [field, value] of Object.entries(branchGc)) {
|
|
1359
|
-
if (!Object.hasOwn(BranchGcFileSettings.shape, field)) {
|
|
1360
|
-
issues.push(`[config] Ignoring unknown branchGc.${field}; it is not a branchGc setting, so the default block applies.`);
|
|
1361
|
-
continue;
|
|
1362
|
-
}
|
|
1363
|
-
const parsed = BranchGcFileSettings.safeParse({ [field]: value });
|
|
1364
|
-
if (!parsed.success) {
|
|
1365
|
-
issues.push(`[config] Ignoring invalid branchGc.${field}; using the default instead: ${configIssueMessage(parsed.error)}`);
|
|
1366
|
-
continue;
|
|
1367
|
-
}
|
|
1368
|
-
accepted[field] = value;
|
|
1369
|
-
}
|
|
1370
|
-
return {
|
|
1371
|
-
block: {
|
|
1372
|
-
...DEFAULT_CONFIG.branchGc,
|
|
1373
|
-
...accepted
|
|
1374
|
-
},
|
|
1375
|
-
issues
|
|
1376
|
-
};
|
|
1377
|
-
}
|
|
1378
|
-
function resolveBranchGc(branchGc) {
|
|
1379
|
-
return collectBranchGcIssues(branchGc).block;
|
|
1380
|
-
}
|
|
1381
|
-
function resolveBranchGcAndWarn(branchGc) {
|
|
1382
|
-
const resolved = collectBranchGcIssues(branchGc);
|
|
1383
|
-
for (const issue of resolved.issues) console.warn(issue);
|
|
1384
|
-
return resolved.block;
|
|
1385
|
-
}
|
|
1386
|
-
var POSTHOG_SETTINGS_KEYS = Object.freeze([
|
|
1387
|
-
"enabled",
|
|
1388
|
-
"recurrenceDedupe",
|
|
1389
|
-
"trafficSpikeEnabled",
|
|
1390
|
-
"autoFix",
|
|
1391
|
-
"host",
|
|
1392
|
-
"repoPath",
|
|
1393
|
-
"projects",
|
|
1394
|
-
"projectMap",
|
|
1395
|
-
"intervalMinutes",
|
|
1396
|
-
"maxConcurrentInvestigations",
|
|
1397
|
-
"investigationTimeoutSeconds",
|
|
1398
|
-
"fixTimeoutSeconds",
|
|
1399
|
-
"minUsersToInvestigate",
|
|
1400
|
-
"userEscalationThreshold",
|
|
1401
|
-
"recurrenceWindowDays",
|
|
1402
|
-
"transientRecurrenceLimit",
|
|
1403
|
-
"trafficSpikeMultiplier",
|
|
1404
|
-
"trafficSpikeMinUsers",
|
|
1405
|
-
"trafficSpikeCooldownMinutes",
|
|
1406
|
-
"trafficSpikeBaselineDays"
|
|
1407
|
-
]);
|
|
1408
|
-
var POSTHOG_SECRET_KEYS = Object.freeze(["apiKey"]);
|
|
1409
|
-
var TELEGRAM_SETTINGS_KEYS = Object.freeze(["chatId"]);
|
|
1410
|
-
var TELEGRAM_SECRET_KEYS = Object.freeze(["botToken"]);
|
|
1411
|
-
var SECRET_PRESENCE_SUFFIX = "Configured";
|
|
1412
|
-
function pickRedactedBlock(stored, allowedKeys, secretKeys) {
|
|
1413
|
-
if (!isRecord(stored)) return null;
|
|
1414
|
-
const source = stored;
|
|
1415
|
-
const redacted = {};
|
|
1416
|
-
for (const key of allowedKeys) if (source[key] !== void 0) redacted[key] = source[key];
|
|
1417
|
-
for (const secretKey of secretKeys) {
|
|
1418
|
-
const secret = source[secretKey];
|
|
1419
|
-
redacted[`${secretKey}${SECRET_PRESENCE_SUFFIX}`] = typeof secret === "string" && secret.length > 0;
|
|
1420
|
-
}
|
|
1421
|
-
return redacted;
|
|
1422
|
-
}
|
|
1423
|
-
function retiredGuardrailKeysWarning(config) {
|
|
1424
|
-
const projects = Array.isArray(config.projects) ? config.projects : [];
|
|
1425
|
-
const retiredKeys = [...Object.hasOwn(config, "sandbox") ? ["sandbox (move it to guardrails, renaming unconfinedRepos to unguardedRepos)"] : [], ...projects.some((project) => isRecord(project) && Object.hasOwn(project, "unconfined")) ? ["projects[].unconfined (rename it to unguarded)"] : []];
|
|
1426
|
-
if (retiredKeys.length === 0) return null;
|
|
1427
|
-
return `[guardrails] config.json still has ${retiredKeys.join(" and ")}. These keys are ignored, so their unreadable paths and opt-outs do not apply until you move them.`;
|
|
1428
|
-
}
|
|
1429
|
-
function createConfigStore({ settingsDefaults } = {}) {
|
|
1430
|
-
const configPath = resolveConfigPath();
|
|
1431
|
-
const effectiveDefaults = {
|
|
1432
|
-
...DEFAULT_CONFIG,
|
|
1433
|
-
...settingsDefaults || {}
|
|
1434
|
-
};
|
|
1435
|
-
const launchDefaultKeys = new Set(Object.keys(settingsDefaults || {}));
|
|
1436
|
-
const envSecrets = readEnvSecrets(process.env);
|
|
1437
|
-
const loadedConfig = loadConfigFile(configPath);
|
|
1438
|
-
const config = loadedConfig.config;
|
|
1439
|
-
writeBackupContent(`${configPath}.boot.bak`, loadedConfig.loadedContent);
|
|
1440
|
-
config.repoRoots = config.repoRoots || [];
|
|
1441
|
-
config.branchGc = resolveBranchGcAndWarn(config.branchGc);
|
|
1442
|
-
const retiredGuardrailKeys = retiredGuardrailKeysWarning(config);
|
|
1443
|
-
if (retiredGuardrailKeys !== null) console.warn(retiredGuardrailKeys);
|
|
1444
|
-
if (Array.isArray(config.projects) && ensureProjectIds(config.projects)) try {
|
|
1445
|
-
writeJsonAtomicSync(configPath, withoutEnvSecrets(config, envSecrets), { mode: 384 });
|
|
1446
|
-
console.log("[config] Auto-assigned IDs to projects missing them");
|
|
1447
|
-
} catch (err) {
|
|
1448
|
-
console.warn("[config] Failed to persist auto-assigned project IDs:", errorMessage(err));
|
|
1449
|
-
}
|
|
1450
|
-
let _lastWrittenContent = null;
|
|
1451
|
-
let _lastAppliedContent = loadedConfig.loadedContent;
|
|
1452
|
-
let synchronousWriteRevision = 0;
|
|
1453
|
-
let lastSynchronousContent = null;
|
|
1454
|
-
let pendingSaves = Promise.resolve();
|
|
1455
|
-
function prepareSave(freshConfig, mutatorFn) {
|
|
1456
|
-
const freshValidation = validateConfig(freshConfig);
|
|
1457
|
-
if (!freshValidation.ok) {
|
|
1458
|
-
warnInvalidConfig("save config.json", freshValidation);
|
|
1459
|
-
return null;
|
|
1460
|
-
}
|
|
1461
|
-
if (isSuspectedExternalWipe(freshConfig, config)) {
|
|
1462
|
-
warnSuspectedWipe("save config.json");
|
|
1463
|
-
return null;
|
|
1464
|
-
}
|
|
1465
|
-
if (mutatorFn(freshConfig) === "abort-config-save") return null;
|
|
1466
|
-
const mutatedValidation = validateConfig(withEnvSecrets(freshConfig, envSecrets));
|
|
1467
|
-
if (!mutatedValidation.ok) {
|
|
1468
|
-
warnInvalidConfig("save config.json", mutatedValidation);
|
|
1469
|
-
return null;
|
|
1470
|
-
}
|
|
1471
|
-
return mutatedValidation.config;
|
|
1472
|
-
}
|
|
1473
|
-
function save(mutatorFn) {
|
|
1474
|
-
let loaded;
|
|
1475
|
-
try {
|
|
1476
|
-
loaded = loadConfigFile(configPath, { exitOnError: false });
|
|
1477
|
-
} catch (err) {
|
|
1478
|
-
console.warn("[config] Failed to read config.json for save:", errorLabel(err));
|
|
1479
|
-
return null;
|
|
1480
|
-
}
|
|
1481
|
-
if ("error" in loaded) {
|
|
1482
|
-
console.warn(loaded.message);
|
|
1483
|
-
return null;
|
|
1484
|
-
}
|
|
1485
|
-
const freshConfig = loaded.config;
|
|
1486
|
-
const freshContent = loaded.loadedContent;
|
|
1487
|
-
const effectiveConfig = prepareSave(freshConfig, mutatorFn);
|
|
1488
|
-
if (!effectiveConfig) return null;
|
|
1489
|
-
try {
|
|
1490
|
-
const nextContent = JSON.stringify(withoutEnvSecrets(effectiveConfig, envSecrets), null, 2);
|
|
1491
|
-
if (freshContent !== nextContent) writeBackupContent(`${configPath}.bak`, freshContent);
|
|
1492
|
-
_lastWrittenContent = nextContent;
|
|
1493
|
-
_lastAppliedContent = null;
|
|
1494
|
-
writeTextAtomicSync(configPath, nextContent, { mode: 384 });
|
|
1495
|
-
lastSynchronousContent = nextContent;
|
|
1496
|
-
synchronousWriteRevision++;
|
|
1497
|
-
} catch (err) {
|
|
1498
|
-
console.warn("[config] Failed to write config.json:", errorLabel(err));
|
|
1499
|
-
return null;
|
|
1500
|
-
}
|
|
1501
|
-
return effectiveConfig;
|
|
1502
|
-
}
|
|
1503
|
-
function saveAsync(mutatorFn) {
|
|
1504
|
-
const scheduledSave = pendingSaves.then(async () => {
|
|
1505
|
-
let concurrentContent = null;
|
|
1506
|
-
for (;;) {
|
|
1507
|
-
const revision = synchronousWriteRevision;
|
|
1508
|
-
try {
|
|
1509
|
-
const diskContent = await fs.promises.readFile(configPath, "utf8");
|
|
1510
|
-
if (revision !== synchronousWriteRevision) {
|
|
1511
|
-
concurrentContent = lastSynchronousContent;
|
|
1512
|
-
continue;
|
|
1513
|
-
}
|
|
1514
|
-
const freshContent = concurrentContent ?? diskContent;
|
|
1515
|
-
const effectiveConfig = prepareSave(withEnvSecrets(normalizeConfigFile(JSON.parse(freshContent)), envSecrets), mutatorFn);
|
|
1516
|
-
if (!effectiveConfig) return null;
|
|
1517
|
-
const nextContent = JSON.stringify(withoutEnvSecrets(effectiveConfig, envSecrets), null, 2);
|
|
1518
|
-
if (freshContent !== nextContent) try {
|
|
1519
|
-
await writeTextAtomic(`${configPath}.bak`, freshContent, {
|
|
1520
|
-
mode: 384,
|
|
1521
|
-
enforceMode: process.platform !== "win32"
|
|
1522
|
-
});
|
|
1523
|
-
} catch (error) {
|
|
1524
|
-
console.warn(`[config] Failed to write backup ${configPath}.bak:`, errorLabel(error));
|
|
1525
|
-
}
|
|
1526
|
-
if (revision !== synchronousWriteRevision) {
|
|
1527
|
-
concurrentContent = lastSynchronousContent;
|
|
1528
|
-
continue;
|
|
1529
|
-
}
|
|
1530
|
-
await writeTextAtomic(configPath, nextContent, { mode: 384 });
|
|
1531
|
-
if (revision !== synchronousWriteRevision) {
|
|
1532
|
-
concurrentContent = lastSynchronousContent;
|
|
1533
|
-
continue;
|
|
1534
|
-
}
|
|
1535
|
-
_lastWrittenContent = nextContent;
|
|
1536
|
-
_lastAppliedContent = null;
|
|
1537
|
-
return effectiveConfig;
|
|
1538
|
-
} catch (error) {
|
|
1539
|
-
console.warn("[config] Failed to save config.json asynchronously:", errorMessage(error));
|
|
1540
|
-
return null;
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
});
|
|
1544
|
-
pendingSaves = scheduledSave;
|
|
1545
|
-
return scheduledSave;
|
|
1546
|
-
}
|
|
1547
|
-
function getSettings() {
|
|
1548
|
-
return {
|
|
1549
|
-
port: config.port,
|
|
1550
|
-
autoRecoverSeconds: config.autoRecoverSeconds,
|
|
1551
|
-
inputGraceSeconds: config.inputGraceSeconds,
|
|
1552
|
-
promptDetectionMs: config.promptDetectionMs,
|
|
1553
|
-
notifyDebounceMs: config.notifyDebounceMs,
|
|
1554
|
-
phoneEscalationMs: config.phoneEscalationMs ?? DEFAULT_CONFIG.phoneEscalationMs,
|
|
1555
|
-
replayBufferKB: config.replayBufferKB,
|
|
1556
|
-
cursorBlink: config.cursorBlink ?? effectiveDefaults.cursorBlink,
|
|
1557
|
-
debugMode: config.debugMode ?? effectiveDefaults.debugMode,
|
|
1558
|
-
calmLayout: config.calmLayout ?? effectiveDefaults.calmLayout,
|
|
1559
|
-
detectBackgroundAgents: config.detectBackgroundAgents ?? effectiveDefaults.detectBackgroundAgents,
|
|
1560
|
-
recordSignals: config.recordSignals ?? effectiveDefaults.recordSignals,
|
|
1561
|
-
trace: { enabled: config.trace?.enabled ?? DEFAULT_CONFIG.trace.enabled },
|
|
1562
|
-
guardrails: {
|
|
1563
|
-
...DEFAULT_CONFIG.guardrails,
|
|
1564
|
-
...config.guardrails
|
|
1565
|
-
},
|
|
1566
|
-
telemetry: { enabled: config.telemetry?.enabled ?? DEFAULT_CONFIG.telemetry.enabled },
|
|
1567
|
-
telemetryForcedOff: isTelemetryForcedOff(process.env),
|
|
1568
|
-
rtk: config.rtk ?? effectiveDefaults.rtk,
|
|
1569
|
-
rtkInstallSupported: assetForPlatform(process.platform, process.arch) !== null,
|
|
1570
|
-
saneYolo: config.saneYolo ?? effectiveDefaults.saneYolo,
|
|
1571
|
-
checkForUpdates: config.checkForUpdates ?? effectiveDefaults.checkForUpdates,
|
|
1572
|
-
updateChannel: config.updateChannel ?? effectiveDefaults.updateChannel,
|
|
1573
|
-
autoResume: config.autoResume ?? effectiveDefaults.autoResume,
|
|
1574
|
-
skipPermissionsByDefault: config.skipPermissionsByDefault ?? effectiveDefaults.skipPermissionsByDefault,
|
|
1575
|
-
telegramNotifications: config.telegramNotifications ?? effectiveDefaults.telegramNotifications,
|
|
1576
|
-
integrationBranch: config.integrationBranch === void 0 ? effectiveDefaults.integrationBranch : config.integrationBranch,
|
|
1577
|
-
worktreeRoot: config.worktreeRoot ?? effectiveDefaults.worktreeRoot,
|
|
1578
|
-
worktreeShare: config.worktreeShare ?? effectiveDefaults.worktreeShare,
|
|
1579
|
-
repoRoots: config.repoRoots,
|
|
1580
|
-
repositoryRootCaseInsensitive: equalsIgnoringCaseOnWindows("root", "ROOT"),
|
|
1581
|
-
taskTitle: config.taskTitle ? { ...config.taskTitle } : { ...DEFAULT_CONFIG.taskTitle },
|
|
1582
|
-
changeMap: config.changeMap ? { ...config.changeMap } : null,
|
|
1583
|
-
branchGc: { ...config.branchGc },
|
|
1584
|
-
postTurnChecks: config.postTurnChecks ? { ...config.postTurnChecks } : { ...DEFAULT_CONFIG.postTurnChecks },
|
|
1585
|
-
visions: config.visions ? { ...config.visions } : null,
|
|
1586
|
-
github: config.github ? { ...config.github } : null,
|
|
1587
|
-
teamReview: config.teamReview ? { ...config.teamReview } : null,
|
|
1588
|
-
benchmarks: config.benchmarks ? { ...config.benchmarks } : null,
|
|
1589
|
-
factory: config.factory ? { ...config.factory } : null,
|
|
1590
|
-
knowledgeGraph: config.knowledgeGraph ? { ...config.knowledgeGraph } : null,
|
|
1591
|
-
workflows: config.workflows ?? null,
|
|
1592
|
-
posthog: pickRedactedBlock(config.posthog, POSTHOG_SETTINGS_KEYS, POSTHOG_SECRET_KEYS),
|
|
1593
|
-
secretSources: Object.fromEntries(readEnvSecrets(process.env).map((secret) => [`${secret.blockName}.${secret.secretKey}`, secret.environmentVariable])),
|
|
1594
|
-
usage: config.usage ? { ...config.usage } : null,
|
|
1595
|
-
telegram: pickRedactedBlock(config.telegram, TELEGRAM_SETTINGS_KEYS, TELEGRAM_SECRET_KEYS),
|
|
1596
|
-
ingest: pickSettingsBlock(config.ingest, INGEST_SPEC),
|
|
1597
|
-
projectChoices: (config.projects || []).map((p) => ({
|
|
1598
|
-
id: p.id,
|
|
1599
|
-
name: p.name,
|
|
1600
|
-
path: p.path
|
|
1601
|
-
}))
|
|
1602
|
-
};
|
|
1603
|
-
}
|
|
1604
|
-
function isUnchosenLaunchDefault(target, key, value) {
|
|
1605
|
-
if (!launchDefaultKeys.has(key)) return false;
|
|
1606
|
-
if (key in target) return false;
|
|
1607
|
-
return value === effectiveDefaults[key];
|
|
1608
|
-
}
|
|
1609
|
-
function applySettings(newConfig) {
|
|
1610
|
-
for (const key of RUNTIME_CONFIG_SCALAR_KEYS) {
|
|
1611
|
-
if (key === "integrationBranch") {
|
|
1612
|
-
if (newConfig[key] === void 0) continue;
|
|
1613
|
-
config[key] = newConfig[key] === null || String(newConfig[key]) === "" ? null : String(newConfig[key]);
|
|
1614
|
-
continue;
|
|
1615
|
-
}
|
|
1616
|
-
if (newConfig[key] == null) continue;
|
|
1617
|
-
let nextValue = newConfig[key];
|
|
1618
|
-
if (typeof DEFAULT_CONFIG_BY_KEY[key] === "boolean") nextValue = !!nextValue;
|
|
1619
|
-
if (typeof DEFAULT_CONFIG_BY_KEY[key] === "string") nextValue = String(nextValue);
|
|
1620
|
-
if (typeof nextValue === "boolean" && isUnchosenLaunchDefault(config, key, nextValue)) continue;
|
|
1621
|
-
config[key] = nextValue;
|
|
1622
|
-
}
|
|
1623
|
-
config.repoRoots = newConfig.repoRoots || [];
|
|
1624
|
-
if (newConfig.postTurnChecks != null) config.postTurnChecks = newConfig.postTurnChecks;
|
|
1625
|
-
if (newConfig.worktreeShare != null) config.worktreeShare = newConfig.worktreeShare;
|
|
1626
|
-
if (newConfig.taskTitle != null) config.taskTitle = newConfig.taskTitle;
|
|
1627
|
-
if (newConfig.changeMap != null) config.changeMap = newConfig.changeMap;
|
|
1628
|
-
if (newConfig.branchGc != null) config.branchGc = resolveBranchGc(newConfig.branchGc);
|
|
1629
|
-
if (newConfig.visions != null) config.visions = newConfig.visions;
|
|
1630
|
-
if (newConfig.github != null) config.github = newConfig.github;
|
|
1631
|
-
if (newConfig.teamReview != null) config.teamReview = newConfig.teamReview;
|
|
1632
|
-
if (newConfig.benchmarks != null) config.benchmarks = newConfig.benchmarks;
|
|
1633
|
-
if (newConfig.factory != null) config.factory = newConfig.factory;
|
|
1634
|
-
if (newConfig.knowledgeGraph != null) config.knowledgeGraph = newConfig.knowledgeGraph;
|
|
1635
|
-
if (newConfig.posthog != null) config.posthog = newConfig.posthog;
|
|
1636
|
-
if (newConfig.usage != null) config.usage = newConfig.usage;
|
|
1637
|
-
if (newConfig.telegram != null) config.telegram = newConfig.telegram;
|
|
1638
|
-
if (newConfig.ingest != null) config.ingest = newConfig.ingest;
|
|
1639
|
-
if (newConfig.guardrails != null) config.guardrails = newConfig.guardrails;
|
|
1640
|
-
if (newConfig.telemetry != null) config.telemetry = newConfig.telemetry;
|
|
1641
|
-
if (newConfig.workflows == null) delete config.workflows;
|
|
1642
|
-
if (newConfig.workflows != null) config.workflows = newConfig.workflows;
|
|
1643
|
-
if (newConfig.coder == null) delete config.coder;
|
|
1644
|
-
if (newConfig.coder != null) config.coder = newConfig.coder;
|
|
1645
|
-
config.customAgents = newConfig.customAgents ?? [];
|
|
1646
|
-
config.hooks = Array.isArray(newConfig.hooks) ? newConfig.hooks : [];
|
|
1647
|
-
if (newConfig.port != null && newConfig.port !== config.port) console.log(`[settings] Port changed to ${newConfig.port} - restart required to take effect`);
|
|
1648
|
-
}
|
|
1649
|
-
function watchForChanges(callback) {
|
|
1650
|
-
let reloadTimer = null;
|
|
1651
|
-
let watcher = null;
|
|
1652
|
-
function handleConfigChange(err, data) {
|
|
1653
|
-
if (err) {
|
|
1654
|
-
console.warn("[config] Failed to read config.json:", err.code);
|
|
1655
|
-
return;
|
|
1656
|
-
}
|
|
1657
|
-
if (_lastWrittenContent !== null && data === _lastWrittenContent) return;
|
|
1658
|
-
if (_lastAppliedContent !== null && data === _lastAppliedContent) return;
|
|
1659
|
-
let newConfig;
|
|
1660
|
-
try {
|
|
1661
|
-
newConfig = withEnvSecrets(normalizeConfigFile(JSON.parse(data)), envSecrets);
|
|
1662
|
-
} catch (parseErr) {
|
|
1663
|
-
console.warn("[config] Invalid config.json:", errorMessage(parseErr));
|
|
1664
|
-
return;
|
|
1665
|
-
}
|
|
1666
|
-
newConfig.branchGc = resolveBranchGcAndWarn(newConfig.branchGc);
|
|
1667
|
-
if (isSuspectedExternalWipe(newConfig, config)) {
|
|
1668
|
-
warnSuspectedWipe("reload config.json");
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
_lastAppliedContent = data;
|
|
1672
|
-
_lastWrittenContent = null;
|
|
1673
|
-
callback(newConfig);
|
|
1674
|
-
console.log("[config] Reloaded config.json");
|
|
1675
|
-
}
|
|
1676
|
-
try {
|
|
1677
|
-
const canonicalConfigPath = canonicalizePath(configPath);
|
|
1678
|
-
const watchDir = path.dirname(canonicalConfigPath);
|
|
1679
|
-
const targetName = path.basename(canonicalConfigPath);
|
|
1680
|
-
watcher = fs.watch(watchDir, (_event, filename) => {
|
|
1681
|
-
if (filename != null && !equalsIgnoringCaseOnWindows(path.basename(String(filename)), targetName)) return;
|
|
1682
|
-
if (reloadTimer) clearTimeout(reloadTimer);
|
|
1683
|
-
reloadTimer = setTimeout(() => {
|
|
1684
|
-
fs.readFile(configPath, "utf8", handleConfigChange);
|
|
1685
|
-
}, 500);
|
|
1686
|
-
});
|
|
1687
|
-
console.log("[config] Watching config.json for changes");
|
|
1688
|
-
} catch (watchErr) {
|
|
1689
|
-
console.warn("[config] Failed to watch config.json:", errorMessage(watchErr));
|
|
1690
|
-
}
|
|
1691
|
-
return function stop() {
|
|
1692
|
-
if (reloadTimer) clearTimeout(reloadTimer);
|
|
1693
|
-
if (watcher) try {
|
|
1694
|
-
watcher.close();
|
|
1695
|
-
} catch {}
|
|
1696
|
-
watcher = null;
|
|
1697
|
-
};
|
|
1698
|
-
}
|
|
1699
|
-
return {
|
|
1700
|
-
config,
|
|
1701
|
-
configPath,
|
|
1702
|
-
save,
|
|
1703
|
-
saveAsync,
|
|
1704
|
-
idle: () => pendingSaves,
|
|
1705
|
-
getSettings,
|
|
1706
|
-
applySettings,
|
|
1707
|
-
isUnchosenLaunchDefault,
|
|
1708
|
-
watchForChanges,
|
|
1709
|
-
DEFAULT_CONFIG
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
//#endregion
|
|
1713
|
-
export { sessionIdFromBranch as $, snapshotEvents as A, classifySessionExit as B, createIngestStore as C, resolveIngestConfig as D, publishEvent as E, adapterBucket as F, nodeMajorVersion as G, decideRemoteTelemetryState as H, buildAiGenerationEvents as I, stableConfigKey as J, parsePosthogReleaseId as K, buildBrowserExceptionProperties as L, FIRST_RUN_NOTICE as M, TELEMETRY_BATCH_URL as N, ringStats as O, TELEMETRY_FLAGS_URL as P, planWorktreeGc as Q, buildExceptionProperties as R, buildContextDigest as S, latestSeq as T, decideTelemetryConsent as U, decideIsDevInstall as V, exceptionFingerprint as W, matchedPrefix as X, DEFAULT_BRANCH_GC_PREFIXES as Y, planBranchGc as Z, INGEST_SPEC as _, ensureProjectIds as a, DEFAULT_DIGEST_BUDGET_CHARS as b, loadConfigFile as c, assetForPlatform as d, usablePrefixes as et, decideRtkInstall as f, verifyDigest as g, isRtkBinaryName as h, createConfigStore as i, isSameDirectoryPath as it, localDayKey as j, scrubText as k, resolveConfigPath as l, installTargetPath as m, DEFAULT_CONFIG as n, canonicalizePath as nt, generateProjectId as o, findEscapingArchiveMember as p, resolveProjectToken as q, SECRET_PRESENCE_SUFFIX as r, comparableDirectoryPath as rt, glimmervoidHomeDir as s, ABORT_CONFIG_SAVE as t, worktreeIntegrationTips as tt, validateConfig as u, mergeSettingsBlock as v, enabledSourceNames as w, SOURCE_DEFAULTS as x, validateSettingsBlock as y, buildFactoryDecisionEvent as z };
|