@mickey-kras/hindsight-memory-router-coding-agents 0.0.0-stage → 0.8.7
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/DEVIATIONS.md +166 -0
- package/LICENSE +23 -0
- package/README.md +83 -2
- package/UPSTREAM.json +263 -0
- package/cordis.patch.yml +18 -0
- package/dist/antigravity-hook.js +5666 -0
- package/dist/antigravity-statusline.js +1329 -0
- package/dist/antigravity-stop-hook.js +5859 -0
- package/dist/claude-hook.js +5666 -0
- package/dist/claude-sessionstart-hook.js +6349 -0
- package/dist/claude-stop-hook.js +5859 -0
- package/dist/cline.js +24976 -0
- package/dist/codebuddy-hook.js +5666 -0
- package/dist/codebuddy-sessionstart-hook.js +6349 -0
- package/dist/codebuddy-stop-hook.js +5859 -0
- package/dist/codex-hook.js +5666 -0
- package/dist/codex-sessionstart-hook.js +6349 -0
- package/dist/codex-stop-hook.js +5859 -0
- package/dist/copilot-hook.js +5666 -0
- package/dist/copilot-sessionstart-hook.js +6349 -0
- package/dist/copilot-stop-hook.js +5859 -0
- package/dist/cursor-hook.js +5666 -0
- package/dist/cursor-sessionstart-hook.js +6349 -0
- package/dist/cursor-stop-hook.js +5859 -0
- package/dist/daemon-start.js +13 -0
- package/dist/dcode-hook.js +5666 -0
- package/dist/dcode-sessionstart-hook.js +6349 -0
- package/dist/dcode-stop-hook.js +5859 -0
- package/dist/deepen.js +3119 -0
- package/dist/devin-hook.js +5666 -0
- package/dist/devin-sessionstart-hook.js +6349 -0
- package/dist/devin-stop-hook.js +5859 -0
- package/dist/droid-hook.js +5666 -0
- package/dist/droid-sessionstart-hook.js +6349 -0
- package/dist/droid-stop-hook.js +5859 -0
- package/dist/dsh.js +25098 -0
- package/dist/grok-hook.js +5666 -0
- package/dist/grok-sessionstart-hook.js +6349 -0
- package/dist/grok-stop-hook.js +5859 -0
- package/dist/hindsight-seed.js +1131 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +37505 -0
- package/dist/installer.js +9030 -0
- package/dist/kilo.js +37505 -0
- package/dist/kimi-hook.js +5666 -0
- package/dist/kimi-sessionstart-hook.js +6349 -0
- package/dist/kimi-stop-hook.js +5859 -0
- package/dist/mcp-server.js +39699 -0
- package/dist/opencode2.js +37464 -0
- package/dist/pi.js +25044 -0
- package/dist/prime-agent.js +25044 -0
- package/dist/qwen-hook.js +5666 -0
- package/dist/qwen-sessionstart-hook.js +6349 -0
- package/dist/qwen-stop-hook.js +5859 -0
- package/dist/status.js +2516 -0
- package/dist/survey-supervisor.js +81 -0
- package/dist/traecode-hook.js +5666 -0
- package/dist/traecode-sessionstart-hook.js +6349 -0
- package/dist/traecode-stop-hook.js +5859 -0
- package/dist/workbuddy-hook.js +5666 -0
- package/dist/workbuddy-sessionstart-hook.js +6349 -0
- package/dist/workbuddy-stop-hook.js +5859 -0
- package/dist/zcode-hook.js +5666 -0
- package/dist/zcode-sessionstart-hook.js +6349 -0
- package/dist/zcode-stop-hook.js +5859 -0
- package/hooks/hooks.json +37 -0
- package/index.js +10 -0
- package/npm-shrinkwrap.json +7756 -0
- package/package.json +124 -4
- package/plugin.json +16 -0
- package/skill/SKILL.md +141 -0
|
@@ -0,0 +1,1329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../../shared/patterns.ts
|
|
4
|
+
var PRINCIPAL_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
5
|
+
var TOKEN_FORMAT_PATTERN = /^mr_[A-Za-z0-9._-]{1,64}_[0-9a-f]{64}$/;
|
|
6
|
+
var BANK_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
7
|
+
|
|
8
|
+
// ../../shared/bank-access.ts
|
|
9
|
+
var AccessDeniedError = class extends Error {
|
|
10
|
+
statusCode = 403;
|
|
11
|
+
constructor() {
|
|
12
|
+
super("memory access denied");
|
|
13
|
+
this.name = "AccessDeniedError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
function visibleBanks(access) {
|
|
17
|
+
const banks = [.../* @__PURE__ */ new Set([...access.writeBank ? [access.writeBank] : [], ...access.additionalReadBanks])];
|
|
18
|
+
if (banks.some((bank) => !BANK_ID_PATTERN.test(bank) || bank === "." || bank === "..")) {
|
|
19
|
+
throw new AccessDeniedError();
|
|
20
|
+
}
|
|
21
|
+
return banks;
|
|
22
|
+
}
|
|
23
|
+
function requireBank(access, bank, operation) {
|
|
24
|
+
if (!visibleBanks(access).includes(bank) || operation === "write" && access.writeBank !== bank) {
|
|
25
|
+
throw new AccessDeniedError();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function classifyOperation(method, suffix) {
|
|
29
|
+
if (method === "POST" && ["/memories/recall", "/reflect"].includes(suffix)) return "read";
|
|
30
|
+
if (method === "GET" && /^\/(?:config|stats|tags|graph|documents|operations|memories|mental-models|knowledge-base)(?:\/[^/]+)*$/.test(
|
|
31
|
+
suffix
|
|
32
|
+
))
|
|
33
|
+
return "read";
|
|
34
|
+
if (method === "GET" && suffix === "") return "read";
|
|
35
|
+
if (["POST", "PUT", "PATCH", "DELETE"].includes(method) && /^(?:\/(?:config|import|memories|documents|mental-models|knowledge-base|consolidate|consolidation)(?:\/[^/]+)*)?$/.test(
|
|
36
|
+
suffix
|
|
37
|
+
))
|
|
38
|
+
return "write";
|
|
39
|
+
throw new AccessDeniedError();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ../../coding-agents/runtime.ts
|
|
43
|
+
import { realpathSync } from "fs";
|
|
44
|
+
import { isAbsolute as isAbsolute2, resolve, sep } from "path";
|
|
45
|
+
|
|
46
|
+
// ../../shared/managed-config.ts
|
|
47
|
+
import { readFileSync } from "fs";
|
|
48
|
+
import { isAbsolute } from "path";
|
|
49
|
+
|
|
50
|
+
// ../../shared/tool-prefix.ts
|
|
51
|
+
var ToolPrefixError = class extends Error {
|
|
52
|
+
};
|
|
53
|
+
var DEFAULT_TOOL_PREFIX = "hindsight_";
|
|
54
|
+
var PREFIX_PATTERN = /^[a-z][a-z0-9_]*_$/;
|
|
55
|
+
function validatedToolPrefix(value) {
|
|
56
|
+
if (value === void 0) return DEFAULT_TOOL_PREFIX;
|
|
57
|
+
if (typeof value !== "string" || value.length > 32 || !PREFIX_PATTERN.test(value)) {
|
|
58
|
+
throw new ToolPrefixError(
|
|
59
|
+
"invalid toolPrefix: expected 2-32 lowercase letters, digits or underscores, ending in underscore"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
function toolBrand(value = void 0) {
|
|
65
|
+
const prefix = validatedToolPrefix(value);
|
|
66
|
+
const id = prefix.slice(0, -1).replaceAll(/_+/g, "-").replace(/-$/, "");
|
|
67
|
+
return {
|
|
68
|
+
prefix,
|
|
69
|
+
id,
|
|
70
|
+
skillName: `${id}-coding-agent`,
|
|
71
|
+
displayName: prefix === DEFAULT_TOOL_PREFIX ? "Hindsight" : id
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ../../shared/managed-config.ts
|
|
76
|
+
var TOKEN_ENV_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
77
|
+
function validatedPrincipal(configured) {
|
|
78
|
+
const human = configured.human;
|
|
79
|
+
if ((human === void 0 ? typeof configured.tokenEnv !== "string" || !TOKEN_ENV_PATTERN.test(configured.tokenEnv) : typeof human !== "object" || human === null || Object.keys(human).length !== 1 || typeof human.credentialRef !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(human.credentialRef) || Object.hasOwn(configured, "tokenEnv")) || "token" in configured || "apiToken" in configured) {
|
|
80
|
+
throw new AccessDeniedError();
|
|
81
|
+
}
|
|
82
|
+
if (configured.source !== void 0 && (typeof configured.source !== "string" || configured.source.trim() === "")) {
|
|
83
|
+
throw new AccessDeniedError();
|
|
84
|
+
}
|
|
85
|
+
if (typeof configured.writeBank === "string" && configured.writeBank.trim() === "") {
|
|
86
|
+
throw new AccessDeniedError();
|
|
87
|
+
}
|
|
88
|
+
if (configured.additionalReadBanks !== void 0 && !Array.isArray(configured.additionalReadBanks)) {
|
|
89
|
+
throw new AccessDeniedError();
|
|
90
|
+
}
|
|
91
|
+
const principal = { ...configured, additionalReadBanks: configured.additionalReadBanks ?? [] };
|
|
92
|
+
visibleBanks(principal);
|
|
93
|
+
return principal;
|
|
94
|
+
}
|
|
95
|
+
function validatePositiveInteger(value) {
|
|
96
|
+
if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) throw new AccessDeniedError();
|
|
97
|
+
}
|
|
98
|
+
function validateQueueOptions(config) {
|
|
99
|
+
for (const value of [
|
|
100
|
+
config.recallTimeoutMs,
|
|
101
|
+
config.recallMaxTokens,
|
|
102
|
+
config.retainQueueFlushIntervalMs,
|
|
103
|
+
config.queueMaxItems,
|
|
104
|
+
config.queueMaxBytes
|
|
105
|
+
]) {
|
|
106
|
+
validatePositiveInteger(value);
|
|
107
|
+
}
|
|
108
|
+
if (config.queueMaxAgeMs !== void 0 && (!Number.isSafeInteger(config.queueMaxAgeMs) || config.queueMaxAgeMs !== -1 && config.queueMaxAgeMs <= 0)) {
|
|
109
|
+
throw new AccessDeniedError();
|
|
110
|
+
}
|
|
111
|
+
if (config.queueDir !== void 0 && (typeof config.queueDir !== "string" || !isAbsolute(config.queueDir))) {
|
|
112
|
+
throw new AccessDeniedError();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function loadValidatedManagedConfig(env) {
|
|
116
|
+
const path = env.HINDSIGHT_ROUTER_CONFIG;
|
|
117
|
+
if (!path || !isAbsolute(path)) throw new AccessDeniedError();
|
|
118
|
+
const config = JSON.parse(readFileSync(path, "utf8"));
|
|
119
|
+
const toolPrefix = validatedToolPrefix(config.toolPrefix);
|
|
120
|
+
validateQueueOptions(config);
|
|
121
|
+
return { ...config, toolPrefix };
|
|
122
|
+
}
|
|
123
|
+
function loadManagedConfig(env, principalId) {
|
|
124
|
+
try {
|
|
125
|
+
if (!principalId) throw new AccessDeniedError();
|
|
126
|
+
const config = loadValidatedManagedConfig(env);
|
|
127
|
+
if (!Object.hasOwn(config.principals ?? {}, principalId)) throw new AccessDeniedError();
|
|
128
|
+
const principal = validatedPrincipal(config.principals[principalId]);
|
|
129
|
+
validateQueueOptions(config);
|
|
130
|
+
return { config, principalId, principal };
|
|
131
|
+
} catch {
|
|
132
|
+
throw new AccessDeniedError();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ../../shared/router-transport.ts
|
|
137
|
+
import { createHash } from "crypto";
|
|
138
|
+
|
|
139
|
+
// ../../shared/router-url.ts
|
|
140
|
+
var RouterUrlError = class extends Error {
|
|
141
|
+
constructor(reason) {
|
|
142
|
+
super(`routerUrl rejected: ${reason}`);
|
|
143
|
+
this.name = "RouterUrlError";
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
function validateRouterUrl(value) {
|
|
147
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
148
|
+
throw new RouterUrlError("missing");
|
|
149
|
+
}
|
|
150
|
+
let url;
|
|
151
|
+
try {
|
|
152
|
+
url = new URL(value.trim());
|
|
153
|
+
} catch {
|
|
154
|
+
throw new RouterUrlError("invalid");
|
|
155
|
+
}
|
|
156
|
+
if (url.protocol !== "https:") {
|
|
157
|
+
throw new RouterUrlError("not-https");
|
|
158
|
+
}
|
|
159
|
+
const hostname = url.hostname.replace(/\.$/, "");
|
|
160
|
+
if (hostname === "hindsight.vectorize.io" || hostname.endsWith(".hindsight.vectorize.io")) {
|
|
161
|
+
throw new RouterUrlError("upstream");
|
|
162
|
+
}
|
|
163
|
+
if (url.username !== "" || url.password !== "") {
|
|
164
|
+
throw new RouterUrlError("userinfo");
|
|
165
|
+
}
|
|
166
|
+
return url.toString().replace(/\/$/, "");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ../../shared/router-transport.ts
|
|
170
|
+
var RouterRequestError = class extends Error {
|
|
171
|
+
constructor(statusCode, retryAfterMs = 0) {
|
|
172
|
+
super(`memory request failed (${statusCode})`);
|
|
173
|
+
this.statusCode = statusCode;
|
|
174
|
+
this.retryAfterMs = retryAfterMs;
|
|
175
|
+
this.name = "RouterRequestError";
|
|
176
|
+
}
|
|
177
|
+
statusCode;
|
|
178
|
+
retryAfterMs;
|
|
179
|
+
};
|
|
180
|
+
function boundedRetryAfterMs(header) {
|
|
181
|
+
if (!header?.trim()) return 0;
|
|
182
|
+
const value = header.trim();
|
|
183
|
+
const delay = /^\d+(?:\.\d+)?$/.test(value) ? Number(value) * 1e3 : Date.parse(value) - Date.now();
|
|
184
|
+
return Number.isFinite(delay) ? Math.min(6e4, Math.max(0, delay)) : 0;
|
|
185
|
+
}
|
|
186
|
+
var ROUTING_KEYS = ["bankId", "bank_id", "bank_ids"];
|
|
187
|
+
function assertAuthorizedUrl(url, method, baseUrl, access) {
|
|
188
|
+
const listing = url === `${baseUrl}/v1/default/banks` && method === "GET";
|
|
189
|
+
if (listing || [`${baseUrl}/version`, `${baseUrl}/v1/version`].includes(url) && method === "GET") {
|
|
190
|
+
return listing;
|
|
191
|
+
}
|
|
192
|
+
const prefix = `${baseUrl}/v1/default/banks/`;
|
|
193
|
+
if (!url.startsWith(prefix)) throw new AccessDeniedError();
|
|
194
|
+
const path = url.slice(prefix.length).split("?")[0];
|
|
195
|
+
const split = path.indexOf("/");
|
|
196
|
+
const bank = decodeURIComponent(split < 0 ? path : path.slice(0, split));
|
|
197
|
+
const suffix = split < 0 ? "" : path.slice(split);
|
|
198
|
+
requireBank(access, bank, classifyOperation(method, suffix));
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
async function filterVisibleBanks(response, access) {
|
|
202
|
+
const data = await response.json();
|
|
203
|
+
if (!Array.isArray(data.banks)) throw new RouterRequestError(502);
|
|
204
|
+
const allowed = new Set(visibleBanks(access));
|
|
205
|
+
const banks = data.banks.filter(
|
|
206
|
+
(bank) => bank !== null && typeof bank === "object" && allowed.has(bank.bank_id ?? "")
|
|
207
|
+
);
|
|
208
|
+
return Response.json({ banks, total: banks.length });
|
|
209
|
+
}
|
|
210
|
+
function assertSafeRequest(url, body) {
|
|
211
|
+
const rawPath = /^[a-z][a-z\d+.-]*:\/\/[^/?#]*([^?#]*)/i.exec(url)?.[1] ?? "";
|
|
212
|
+
if (url.includes("#") || /\\/.test(rawPath) || /%(?:2e|2f|5c|25)/i.test(rawPath) || /\/\.\.?(?:\/|$)/.test(rawPath)) {
|
|
213
|
+
throw new AccessDeniedError();
|
|
214
|
+
}
|
|
215
|
+
const parsed = new URL(url);
|
|
216
|
+
if (ROUTING_KEYS.some((key) => parsed.searchParams.has(key))) throw new AccessDeniedError();
|
|
217
|
+
if (typeof body === "string") {
|
|
218
|
+
let parsedBody;
|
|
219
|
+
try {
|
|
220
|
+
parsedBody = JSON.parse(body);
|
|
221
|
+
} catch {
|
|
222
|
+
throw new AccessDeniedError();
|
|
223
|
+
}
|
|
224
|
+
if (parsedBody && typeof parsedBody === "object" && ROUTING_KEYS.some((key) => Object.hasOwn(parsedBody, key))) {
|
|
225
|
+
throw new AccessDeniedError();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (parsed.username || parsed.password) throw new AccessDeniedError();
|
|
229
|
+
return parsed;
|
|
230
|
+
}
|
|
231
|
+
var GLOBAL_DISPATCHER = /* @__PURE__ */ Symbol.for("undici.globalDispatcher.1");
|
|
232
|
+
function signalOnlyDispatcher() {
|
|
233
|
+
Reflect.get(globalThis, "Headers");
|
|
234
|
+
const d = globalThis[GLOBAL_DISPATCHER];
|
|
235
|
+
if (typeof d?.compose !== "function") return void 0;
|
|
236
|
+
return d.compose(
|
|
237
|
+
(dispatch) => (opts, handler) => dispatch({ ...opts, headersTimeout: 0, bodyTimeout: 0 }, handler)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
var RouterTransport = class {
|
|
241
|
+
baseUrl;
|
|
242
|
+
access;
|
|
243
|
+
#token;
|
|
244
|
+
#send;
|
|
245
|
+
#principalId;
|
|
246
|
+
#tokenPattern;
|
|
247
|
+
#unauthorizedError;
|
|
248
|
+
#deniedToken;
|
|
249
|
+
#deniedError;
|
|
250
|
+
constructor(options) {
|
|
251
|
+
this.baseUrl = validateRouterUrl(options.routerUrl);
|
|
252
|
+
const url = new URL(this.baseUrl);
|
|
253
|
+
if (url.search || url.hash) throw new AccessDeniedError();
|
|
254
|
+
this.access = Object.freeze({
|
|
255
|
+
writeBank: options.access.writeBank,
|
|
256
|
+
additionalReadBanks: Object.freeze([...options.access.additionalReadBanks])
|
|
257
|
+
});
|
|
258
|
+
visibleBanks(this.access);
|
|
259
|
+
this.#principalId = options.principalId;
|
|
260
|
+
this.#tokenPattern = options.tokenPattern ?? TOKEN_FORMAT_PATTERN;
|
|
261
|
+
this.#unauthorizedError = options.unauthorizedError ?? (() => new AccessDeniedError());
|
|
262
|
+
this.#token = options.token;
|
|
263
|
+
this.#send = options.fetch ?? ((input, init) => fetch(input, init));
|
|
264
|
+
this.credential();
|
|
265
|
+
}
|
|
266
|
+
hasCredentials() {
|
|
267
|
+
try {
|
|
268
|
+
this.credential();
|
|
269
|
+
return true;
|
|
270
|
+
} catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
assertAuthorized() {
|
|
275
|
+
this.credential();
|
|
276
|
+
}
|
|
277
|
+
authorizationIdentity() {
|
|
278
|
+
this.credential();
|
|
279
|
+
const principal = typeof this.#principalId === "function" ? this.#principalId() : this.#principalId;
|
|
280
|
+
if (!principal) throw new AccessDeniedError();
|
|
281
|
+
return createHash("sha256").update(principal).digest("hex");
|
|
282
|
+
}
|
|
283
|
+
credential() {
|
|
284
|
+
const token = this.#token();
|
|
285
|
+
if (!token || !this.#tokenPattern.test(token)) throw new AccessDeniedError();
|
|
286
|
+
if (this.#deniedToken === token) throw this.#deniedError ?? new AccessDeniedError();
|
|
287
|
+
if (this.#deniedToken !== void 0) {
|
|
288
|
+
this.#deniedToken = void 0;
|
|
289
|
+
this.#deniedError = void 0;
|
|
290
|
+
}
|
|
291
|
+
return token;
|
|
292
|
+
}
|
|
293
|
+
bankUrl(bank, suffix = "") {
|
|
294
|
+
requireBank(this.access, bank, "read");
|
|
295
|
+
return `${this.baseUrl}/v1/default/banks/${encodeURIComponent(bank)}${suffix}`;
|
|
296
|
+
}
|
|
297
|
+
async request(url, init = {}) {
|
|
298
|
+
const token = this.credential();
|
|
299
|
+
const method = init.method ?? "GET";
|
|
300
|
+
assertSafeRequest(url, init.body);
|
|
301
|
+
const listing = assertAuthorizedUrl(url, method, this.baseUrl, this.access);
|
|
302
|
+
const response = await this.send(url, init, token);
|
|
303
|
+
if (response.status === 401 || response.status === 403) {
|
|
304
|
+
const error = response.status === 401 ? this.#unauthorizedError() : new AccessDeniedError();
|
|
305
|
+
this.#deniedToken = token;
|
|
306
|
+
this.#deniedError = error;
|
|
307
|
+
response.body?.cancel().catch(() => void 0);
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
if (!response.ok && (response.status !== 404 || method !== "GET")) {
|
|
311
|
+
response.body?.cancel().catch(() => void 0);
|
|
312
|
+
throw new RouterRequestError(
|
|
313
|
+
response.status,
|
|
314
|
+
response.status === 429 ? boundedRetryAfterMs(response.headers.get("retry-after")) : 0
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
if (listing && response.ok) {
|
|
318
|
+
return filterVisibleBanks(response, this.access);
|
|
319
|
+
}
|
|
320
|
+
if (response.status === 404) {
|
|
321
|
+
response.body?.cancel().catch(() => void 0);
|
|
322
|
+
return new Response(null, { status: 404 });
|
|
323
|
+
}
|
|
324
|
+
return response;
|
|
325
|
+
}
|
|
326
|
+
async send(url, init, token) {
|
|
327
|
+
try {
|
|
328
|
+
init.signal?.throwIfAborted();
|
|
329
|
+
const signal = init.signal ?? AbortSignal.timeout(15e3);
|
|
330
|
+
const dispatcher = signalOnlyDispatcher();
|
|
331
|
+
return await this.#send(url, {
|
|
332
|
+
...init,
|
|
333
|
+
...dispatcher ? { dispatcher } : {},
|
|
334
|
+
redirect: "error",
|
|
335
|
+
headers: {
|
|
336
|
+
"Content-Type": "application/json",
|
|
337
|
+
"Accept-Encoding": "identity",
|
|
338
|
+
Authorization: `Bearer ${token}`,
|
|
339
|
+
...this.#principalId ? {
|
|
340
|
+
"x-memory-router-agent": typeof this.#principalId === "string" ? this.#principalId : this.#principalId()
|
|
341
|
+
} : {}
|
|
342
|
+
},
|
|
343
|
+
signal
|
|
344
|
+
});
|
|
345
|
+
} catch (error) {
|
|
346
|
+
init.signal?.throwIfAborted();
|
|
347
|
+
process.stderr.write(`memory request failed: ${error instanceof Error ? error.name : typeof error}
|
|
348
|
+
`);
|
|
349
|
+
throw new RouterRequestError(503);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// ../../coding-agents/human-session.ts
|
|
355
|
+
import { createHash as createHash2 } from "crypto";
|
|
356
|
+
import { createRequire } from "module";
|
|
357
|
+
var HUMAN_TOKEN_PATTERN = /^mrh_[0-9a-f]{32}_[0-9a-f]{64}$/;
|
|
358
|
+
var HumanLoginRequiredError = class extends AccessDeniedError {
|
|
359
|
+
constructor() {
|
|
360
|
+
super();
|
|
361
|
+
this.message = "human session unavailable; unlock OS secure storage and run hindsight-coding-agents login; if native support is missing, reinstall from a verified package";
|
|
362
|
+
this.name = "HumanLoginRequiredError";
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
function humanRouterUrl(value) {
|
|
366
|
+
const input = value.trim();
|
|
367
|
+
if (Array.from(input).some((character) => {
|
|
368
|
+
const code = character.codePointAt(0);
|
|
369
|
+
return code !== void 0 && (code <= 31 || code === 127);
|
|
370
|
+
})) {
|
|
371
|
+
throw new HumanLoginRequiredError();
|
|
372
|
+
}
|
|
373
|
+
const parts = /^https:\/\/([^/?#\\@]+)(\/[^?#]*)?$/i.exec(input);
|
|
374
|
+
if (!parts) throw new HumanLoginRequiredError();
|
|
375
|
+
const rawPath = parts[2] ?? "";
|
|
376
|
+
if (input.includes("\\") || /%|\/\.\.?(?:\/|$)|(?:\/{2}$)/.test(rawPath)) throw new HumanLoginRequiredError();
|
|
377
|
+
const canonical2 = validateRouterUrl(input);
|
|
378
|
+
const url = new URL(canonical2);
|
|
379
|
+
if (url.search || url.hash) throw new HumanLoginRequiredError();
|
|
380
|
+
return canonical2;
|
|
381
|
+
}
|
|
382
|
+
function secureEntry(routerUrl, credentialRef) {
|
|
383
|
+
try {
|
|
384
|
+
if (!["darwin", "win32", "linux"].includes(process.platform) || !/^[A-Za-z0-9._-]{1,64}$/.test(credentialRef)) {
|
|
385
|
+
throw new HumanLoginRequiredError();
|
|
386
|
+
}
|
|
387
|
+
const { Entry: NativeEntry } = createRequire(import.meta.url)("@napi-rs/keyring");
|
|
388
|
+
const service = `hindsight-memory-router:${createHash2("sha256").update(humanRouterUrl(routerUrl)).digest("hex")}`;
|
|
389
|
+
return new NativeEntry(service, credentialRef, { linux: { store: "secret-service" } });
|
|
390
|
+
} catch {
|
|
391
|
+
throw new HumanLoginRequiredError();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function validateHumanSession(value, routerUrl, allowExpired = false) {
|
|
395
|
+
if (typeof value !== "object" || value === null) throw new HumanLoginRequiredError();
|
|
396
|
+
const session = value;
|
|
397
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
398
|
+
if (typeof session.token !== "string" || !HUMAN_TOKEN_PATTERN.test(session.token) || typeof session.principal_id !== "string" || !PRINCIPAL_ID_PATTERN.test(session.principal_id) || typeof session.expires_at !== "number" || !Number.isSafeInteger(session.expires_at) || !allowExpired && session.expires_at <= now || session.expires_at > now + 3660 || session.routerUrl !== humanRouterUrl(routerUrl))
|
|
399
|
+
throw new HumanLoginRequiredError();
|
|
400
|
+
return {
|
|
401
|
+
token: session.token,
|
|
402
|
+
principal_id: session.principal_id,
|
|
403
|
+
expires_at: session.expires_at,
|
|
404
|
+
routerUrl: session.routerUrl
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function readHumanSession(routerUrl, credentialRef) {
|
|
408
|
+
try {
|
|
409
|
+
const password = secureEntry(routerUrl, credentialRef).getPassword();
|
|
410
|
+
if (!password || password.length > 4096) throw new HumanLoginRequiredError();
|
|
411
|
+
return validateHumanSession(JSON.parse(password), routerUrl);
|
|
412
|
+
} catch {
|
|
413
|
+
throw new HumanLoginRequiredError();
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ../../coding-agents/runtime.ts
|
|
418
|
+
function transportForConfig(harness, { config, principal }) {
|
|
419
|
+
if (principal.human) {
|
|
420
|
+
const routerUrl = humanRouterUrl(config.routerUrl);
|
|
421
|
+
const reference = principal.human.credentialRef;
|
|
422
|
+
let session = readHumanSession(routerUrl, reference);
|
|
423
|
+
return new RouterTransport({
|
|
424
|
+
routerUrl,
|
|
425
|
+
access: principal,
|
|
426
|
+
principalId: () => session.principal_id,
|
|
427
|
+
tokenPattern: HUMAN_TOKEN_PATTERN,
|
|
428
|
+
unauthorizedError: () => new HumanLoginRequiredError(),
|
|
429
|
+
token: () => {
|
|
430
|
+
session = readHumanSession(routerUrl, reference);
|
|
431
|
+
return session.token;
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
const tokenEnv = principal.tokenEnv;
|
|
436
|
+
if (!tokenEnv) throw new AccessDeniedError();
|
|
437
|
+
return new RouterTransport({
|
|
438
|
+
routerUrl: config.routerUrl,
|
|
439
|
+
access: principal,
|
|
440
|
+
principalId: harness,
|
|
441
|
+
token: () => process.env[tokenEnv]
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function managedSettings(harness) {
|
|
445
|
+
if (!harness) throw new AccessDeniedError();
|
|
446
|
+
const snapshot = loadManagedConfig(process.env, harness);
|
|
447
|
+
const transport = transportForConfig(harness, snapshot);
|
|
448
|
+
const { config } = snapshot;
|
|
449
|
+
const readOnlySettings = transport.access.writeBank ? {} : {
|
|
450
|
+
retainSessions: false,
|
|
451
|
+
autoSeed: false,
|
|
452
|
+
codebaseSurvey: false,
|
|
453
|
+
gitIngest: "none"
|
|
454
|
+
};
|
|
455
|
+
return {
|
|
456
|
+
harness,
|
|
457
|
+
routerHarness: harness,
|
|
458
|
+
toolPrefix: config.toolPrefix,
|
|
459
|
+
apiUrl: transport.baseUrl,
|
|
460
|
+
apiToken: void 0,
|
|
461
|
+
serverMode: "self-hosted",
|
|
462
|
+
dynamicBankId: false,
|
|
463
|
+
autoUpdate: false,
|
|
464
|
+
optInOnly: true,
|
|
465
|
+
manageBankConfig: false,
|
|
466
|
+
...readOnlySettings
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function managedBank(harness, directory) {
|
|
470
|
+
return bankForPrincipal(loadManagedConfig(process.env, harness).principal, directory);
|
|
471
|
+
}
|
|
472
|
+
function bankForPrincipal(principal, directory) {
|
|
473
|
+
if (!directory) throw new AccessDeniedError();
|
|
474
|
+
let location;
|
|
475
|
+
try {
|
|
476
|
+
location = realpathSync(directory);
|
|
477
|
+
} catch {
|
|
478
|
+
throw new AccessDeniedError();
|
|
479
|
+
}
|
|
480
|
+
const match = Object.entries(principal.mapPathToBank ?? {}).flatMap(([path, bank]) => {
|
|
481
|
+
if (!isAbsolute2(path)) return [];
|
|
482
|
+
try {
|
|
483
|
+
return [[realpathSync(resolve(path)), bank]];
|
|
484
|
+
} catch {
|
|
485
|
+
return [];
|
|
486
|
+
}
|
|
487
|
+
}).filter(([path]) => isWithinDirectory(location, path)).sort(([a], [b]) => b.length - a.length)[0];
|
|
488
|
+
if (!match) throw new AccessDeniedError();
|
|
489
|
+
requireBank(principal, match[1], "read");
|
|
490
|
+
return match[1];
|
|
491
|
+
}
|
|
492
|
+
function isWithinDirectory(location, directory) {
|
|
493
|
+
return location === directory || location.startsWith(directory.endsWith(sep) ? directory : directory + sep);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/core/config.ts
|
|
497
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
498
|
+
import { homedir as homedir3 } from "os";
|
|
499
|
+
import { join as join7 } from "path";
|
|
500
|
+
|
|
501
|
+
// src/core/seed.ts
|
|
502
|
+
import { spawn as realSpawn } from "child_process";
|
|
503
|
+
import { dirname, join } from "path";
|
|
504
|
+
import { fileURLToPath } from "url";
|
|
505
|
+
var DEFAULT_SEED_LIMIT = 300;
|
|
506
|
+
|
|
507
|
+
// src/core/bank.ts
|
|
508
|
+
import { existsSync } from "fs";
|
|
509
|
+
import { homedir as homedir2 } from "os";
|
|
510
|
+
import { basename, dirname as dirname4, join as join5, normalize, sep as sep2 } from "path";
|
|
511
|
+
|
|
512
|
+
// src/core/diag.ts
|
|
513
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
514
|
+
import { join as join3 } from "path";
|
|
515
|
+
|
|
516
|
+
// src/core/log.ts
|
|
517
|
+
import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
|
|
518
|
+
import { homedir } from "os";
|
|
519
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
520
|
+
function logsDir() {
|
|
521
|
+
return join2(homedir(), ".hindsight", "coding-agents-logs");
|
|
522
|
+
}
|
|
523
|
+
var LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
524
|
+
function appendLogLine(file, text) {
|
|
525
|
+
mkdirSync(dirname2(file), { recursive: true, mode: 448 });
|
|
526
|
+
try {
|
|
527
|
+
if (statSync(file).size >= LOG_MAX_BYTES) renameSync(file, `${file}.1`);
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
530
|
+
appendFileSync(file, text, { mode: 384 });
|
|
531
|
+
}
|
|
532
|
+
var WEIGHT = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
533
|
+
var current = ["debug", "info", "warn", "error"].find(
|
|
534
|
+
(l) => l === process.env.HINDSIGHT_LOG_LEVEL
|
|
535
|
+
) ?? "info";
|
|
536
|
+
function logFilePath() {
|
|
537
|
+
return process.env.HINDSIGHT_LOG_FILE || join2(logsDir(), "plugin.log");
|
|
538
|
+
}
|
|
539
|
+
var NUMERIC_FIELDS = /* @__PURE__ */ new Set([
|
|
540
|
+
"ms",
|
|
541
|
+
"chars",
|
|
542
|
+
"count",
|
|
543
|
+
"turns",
|
|
544
|
+
"timeoutMs",
|
|
545
|
+
"pages",
|
|
546
|
+
"newChats",
|
|
547
|
+
"failures",
|
|
548
|
+
"hostCatalog",
|
|
549
|
+
"hindsight",
|
|
550
|
+
"commits",
|
|
551
|
+
"limit"
|
|
552
|
+
]);
|
|
553
|
+
var BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["cold", "wired", "hasSession", "appendSupported", "retry", "settled"]);
|
|
554
|
+
var ERROR_CLASSES = ["timeout", "aborted", "connection_refused", "dns_failure", "operation_failed"];
|
|
555
|
+
function errorClass(value) {
|
|
556
|
+
const seen = /* @__PURE__ */ new Set();
|
|
557
|
+
let error = value;
|
|
558
|
+
while (error && typeof error === "object" && !seen.has(error) && seen.size < 8) {
|
|
559
|
+
seen.add(error);
|
|
560
|
+
const detail = error;
|
|
561
|
+
if (detail.timedOut === true || detail.name === "TimeoutError" || detail.code === "ETIMEDOUT") return "timeout";
|
|
562
|
+
if (detail.name === "AbortError") return "aborted";
|
|
563
|
+
if (detail.code === "ECONNREFUSED") return "connection_refused";
|
|
564
|
+
if (detail.code === "ENOTFOUND" || detail.code === "EAI_AGAIN") return "dns_failure";
|
|
565
|
+
error = detail.cause;
|
|
566
|
+
}
|
|
567
|
+
return "operation_failed";
|
|
568
|
+
}
|
|
569
|
+
function logMetadata(extra = {}) {
|
|
570
|
+
const safe = {};
|
|
571
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
572
|
+
if (NUMERIC_FIELDS.has(key) && typeof value === "number" && Number.isFinite(value)) safe[key] = value;
|
|
573
|
+
else if (key === "status" && typeof value === "number" && Number.isInteger(value) && value >= 100 && value <= 599) safe[key] = value;
|
|
574
|
+
else if (BOOLEAN_FIELDS.has(key) && typeof value === "boolean") safe[key] = value;
|
|
575
|
+
else if (key === "mode" && (value === "append" || value === "replace" || value === "skip")) safe[key] = value;
|
|
576
|
+
else if (key === "error") safe.errorClass = errorClass(value);
|
|
577
|
+
else if (key === "errorClass" && ERROR_CLASSES.some((known) => known === value)) safe.errorClass = String(value);
|
|
578
|
+
}
|
|
579
|
+
return safe;
|
|
580
|
+
}
|
|
581
|
+
function write(level, scope, msg, extra) {
|
|
582
|
+
if (WEIGHT[level] < WEIGHT[current]) return;
|
|
583
|
+
try {
|
|
584
|
+
const metadata = logMetadata(extra);
|
|
585
|
+
appendLogLine(
|
|
586
|
+
logFilePath(),
|
|
587
|
+
`${(/* @__PURE__ */ new Date()).toISOString()} ${level.toUpperCase().padEnd(5)} [${scope}] ${msg}` + (Object.keys(metadata).length ? ` ${JSON.stringify(metadata)}` : "") + "\n"
|
|
588
|
+
);
|
|
589
|
+
} catch {
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
var log = {
|
|
593
|
+
debug: (scope, msg, extra) => write("debug", scope, msg, extra),
|
|
594
|
+
info: (scope, msg, extra) => write("info", scope, msg, extra),
|
|
595
|
+
warn: (scope, msg, extra) => write("warn", scope, msg, extra),
|
|
596
|
+
error: (scope, msg, extra) => write("error", scope, msg, extra)
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// src/core/diag.ts
|
|
600
|
+
function diagFilePath() {
|
|
601
|
+
return process.env.HINDSIGHT_DIAG_FILE || join3(logsDir(), "diag.jsonl");
|
|
602
|
+
}
|
|
603
|
+
function diag(harness, event, extra = {}) {
|
|
604
|
+
try {
|
|
605
|
+
const metadata = logMetadata(extra);
|
|
606
|
+
log.debug(harness, `diag:${event}`, metadata);
|
|
607
|
+
appendLogLine(
|
|
608
|
+
diagFilePath(),
|
|
609
|
+
JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), harness, event, ...metadata }) + "\n"
|
|
610
|
+
);
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/core/git-layout.ts
|
|
616
|
+
import { lstatSync, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "fs";
|
|
617
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join4, resolve as resolve2 } from "path";
|
|
618
|
+
var TRANSIENT = /* @__PURE__ */ new Set(["EAGAIN", "EMFILE", "ENFILE", "EBUSY", "EINTR", "EIO", "ETIMEDOUT"]);
|
|
619
|
+
var ATTEMPTS = 3;
|
|
620
|
+
var BACKOFF_MS = 20;
|
|
621
|
+
function errorCode(error) {
|
|
622
|
+
return error?.code ?? "";
|
|
623
|
+
}
|
|
624
|
+
function sleepSync(ms) {
|
|
625
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
626
|
+
}
|
|
627
|
+
var TransientProbeError = class extends Error {
|
|
628
|
+
};
|
|
629
|
+
function entryKind(path) {
|
|
630
|
+
try {
|
|
631
|
+
const st = lstatSync(path);
|
|
632
|
+
return st.isDirectory() ? "dir" : "file";
|
|
633
|
+
} catch (error) {
|
|
634
|
+
if (TRANSIENT.has(errorCode(error))) throw new TransientProbeError(errorCode(error));
|
|
635
|
+
return "none";
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function readTextOrNull(path) {
|
|
639
|
+
try {
|
|
640
|
+
return readFileSync3(path, "utf8");
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (TRANSIENT.has(errorCode(error))) throw new TransientProbeError(errorCode(error));
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function gitDirFromPointer(text, holder) {
|
|
647
|
+
const match = /^\s*gitdir:\s*(.+?)\s*$/m.exec(text);
|
|
648
|
+
if (!match) return null;
|
|
649
|
+
const target = match[1];
|
|
650
|
+
return isAbsolute3(target) ? target : resolve2(holder, target);
|
|
651
|
+
}
|
|
652
|
+
function commonDirOf(gitDir) {
|
|
653
|
+
const pointer = entryKind(join4(gitDir, "commondir")) === "file" ? readTextOrNull(join4(gitDir, "commondir"))?.trim() : null;
|
|
654
|
+
if (!pointer) return gitDir;
|
|
655
|
+
return isAbsolute3(pointer) ? pointer : resolve2(gitDir, pointer);
|
|
656
|
+
}
|
|
657
|
+
function isBare(commonDir) {
|
|
658
|
+
const config = readTextOrNull(join4(commonDir, "config"));
|
|
659
|
+
return config !== null && /^\s*bare\s*=\s*true\s*$/im.test(config);
|
|
660
|
+
}
|
|
661
|
+
function looksLikeBareRepository(directory) {
|
|
662
|
+
return entryKind(join4(directory, "HEAD")) === "file" && entryKind(join4(directory, "objects")) === "dir" && entryKind(join4(directory, "refs")) === "dir";
|
|
663
|
+
}
|
|
664
|
+
function canonical(path) {
|
|
665
|
+
try {
|
|
666
|
+
return realpathSync2(path);
|
|
667
|
+
} catch {
|
|
668
|
+
return path;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function resolved(commonDir) {
|
|
672
|
+
if (entryKind(commonDir) !== "dir") return null;
|
|
673
|
+
const canonicalDir = canonical(commonDir);
|
|
674
|
+
return { status: "resolved", commonDir: canonicalDir, bare: isBare(canonicalDir) };
|
|
675
|
+
}
|
|
676
|
+
function probeOnce(directory) {
|
|
677
|
+
let current2 = resolve2(directory);
|
|
678
|
+
for (; ; ) {
|
|
679
|
+
const dotGit = join4(current2, ".git");
|
|
680
|
+
const kind = entryKind(dotGit);
|
|
681
|
+
if (kind === "dir") {
|
|
682
|
+
const layout = resolved(commonDirOf(dotGit));
|
|
683
|
+
if (layout) return layout;
|
|
684
|
+
} else if (kind === "file") {
|
|
685
|
+
const text = readTextOrNull(dotGit);
|
|
686
|
+
const gitDir = text ? gitDirFromPointer(text, current2) : null;
|
|
687
|
+
const layout = gitDir ? resolved(commonDirOf(gitDir)) : null;
|
|
688
|
+
if (layout) return layout;
|
|
689
|
+
}
|
|
690
|
+
if (looksLikeBareRepository(current2)) {
|
|
691
|
+
const layout = resolved(current2);
|
|
692
|
+
if (layout) return layout;
|
|
693
|
+
}
|
|
694
|
+
const parent = dirname3(current2);
|
|
695
|
+
if (parent === current2) return { status: "absent" };
|
|
696
|
+
current2 = parent;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function probeGitLayout(directory) {
|
|
700
|
+
if (!directory) return { status: "absent" };
|
|
701
|
+
let last = "";
|
|
702
|
+
for (let attempt = 0; attempt < ATTEMPTS; attempt++) {
|
|
703
|
+
if (attempt) sleepSync(BACKOFF_MS * attempt);
|
|
704
|
+
try {
|
|
705
|
+
return probeOnce(directory);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
last = error instanceof TransientProbeError ? error.message : errorCode(error) || "unknown";
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return { status: "failed", reason: last };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/core/bank.ts
|
|
714
|
+
var BankResolutionError = class extends Error {
|
|
715
|
+
constructor(message) {
|
|
716
|
+
super(message);
|
|
717
|
+
this.name = "BankResolutionError";
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
function resolveProjectRoot(directory) {
|
|
721
|
+
if (!directory) return { status: "absent" };
|
|
722
|
+
const layout = probeGitLayout(directory);
|
|
723
|
+
if (layout.status !== "resolved") return layout;
|
|
724
|
+
const commonDir = layout.commonDir;
|
|
725
|
+
if (basename(commonDir) === ".git") return { status: "resolved", root: dirname4(commonDir) };
|
|
726
|
+
if (basename(commonDir).startsWith(".") && layout.bare) {
|
|
727
|
+
return { status: "resolved", root: dirname4(commonDir) };
|
|
728
|
+
}
|
|
729
|
+
return { status: "resolved", root: commonDir };
|
|
730
|
+
}
|
|
731
|
+
var PROJECT_ROOT_ENV = ["CLAUDE_PROJECT_DIR"];
|
|
732
|
+
function nearestExistingDir(directory) {
|
|
733
|
+
let current2 = directory;
|
|
734
|
+
while (current2) {
|
|
735
|
+
if (existsSync(current2)) return current2;
|
|
736
|
+
const parent = dirname4(current2);
|
|
737
|
+
if (parent === current2) return "";
|
|
738
|
+
current2 = parent;
|
|
739
|
+
}
|
|
740
|
+
return "";
|
|
741
|
+
}
|
|
742
|
+
function mainWorktreeRoot(directory, sessionRoot = "") {
|
|
743
|
+
const candidates = [
|
|
744
|
+
nearestExistingDir(directory),
|
|
745
|
+
sessionRoot,
|
|
746
|
+
...PROJECT_ROOT_ENV.map((v) => process.env[v] || "")
|
|
747
|
+
];
|
|
748
|
+
let failure = null;
|
|
749
|
+
for (const candidate of candidates) {
|
|
750
|
+
if (!candidate) continue;
|
|
751
|
+
const projectRoot = resolveProjectRoot(candidate);
|
|
752
|
+
if (projectRoot.status === "resolved") return projectRoot;
|
|
753
|
+
if (projectRoot.status === "failed") failure = projectRoot;
|
|
754
|
+
}
|
|
755
|
+
return failure ?? { status: "absent" };
|
|
756
|
+
}
|
|
757
|
+
function configuredDir(dir) {
|
|
758
|
+
const expanded = dir === "~" || dir.startsWith("~/") ? join5(homedir2(), dir.slice(1)) : dir;
|
|
759
|
+
return normalize(expanded).replace(new RegExp(`\\${sep2}+$`), "");
|
|
760
|
+
}
|
|
761
|
+
function isWithin(directory, configured) {
|
|
762
|
+
return directory === configured || directory.startsWith(configured + sep2);
|
|
763
|
+
}
|
|
764
|
+
function isOptedIn(config, directory) {
|
|
765
|
+
try {
|
|
766
|
+
managedBank(config.routerHarness, directory);
|
|
767
|
+
return true;
|
|
768
|
+
} catch {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
function mapLookup(map, directory) {
|
|
773
|
+
const cwd = normalize(directory);
|
|
774
|
+
let best;
|
|
775
|
+
for (const [dir, value] of Object.entries(map)) {
|
|
776
|
+
const p = configuredDir(dir);
|
|
777
|
+
if (isWithin(cwd, p)) {
|
|
778
|
+
if (!best || p.length > best.len) best = { len: p.length, value };
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return best?.value;
|
|
782
|
+
}
|
|
783
|
+
function lookupDirectories(config, directory, sessionRoot = "") {
|
|
784
|
+
const directories = [normalize(directory)];
|
|
785
|
+
if (config.resolveWorktrees ?? true) {
|
|
786
|
+
const projectRoot = mainWorktreeRoot(directory, sessionRoot);
|
|
787
|
+
const normalizedRoot = projectRoot.status === "resolved" ? normalize(projectRoot.root) : "";
|
|
788
|
+
if (normalizedRoot && normalizedRoot !== directories[0]) directories.push(normalizedRoot);
|
|
789
|
+
}
|
|
790
|
+
return directories;
|
|
791
|
+
}
|
|
792
|
+
function pathSection(config, paths, directory) {
|
|
793
|
+
if (!directory || !paths) return void 0;
|
|
794
|
+
return lookupDirectories(config, directory).map((candidate) => mapLookup(paths, candidate)).find((section) => section !== void 0);
|
|
795
|
+
}
|
|
796
|
+
function deriveBankId(config, directory, harness = "coding", sessionRoot) {
|
|
797
|
+
return managedBank(harness, directory);
|
|
798
|
+
}
|
|
799
|
+
function deriveBankIdOrSkip(config, directory, harness = "coding", sessionRoot) {
|
|
800
|
+
try {
|
|
801
|
+
return deriveBankId(config, directory, harness, sessionRoot);
|
|
802
|
+
} catch (error) {
|
|
803
|
+
if (!(error instanceof BankResolutionError)) throw error;
|
|
804
|
+
log.warn(harness, "bank unresolved: skipping (repository could not be identified)", {
|
|
805
|
+
directory,
|
|
806
|
+
error
|
|
807
|
+
});
|
|
808
|
+
diag(harness, "bank_unresolved", { directory, error });
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/core/missions.ts
|
|
814
|
+
import { createHash as createHash3 } from "crypto";
|
|
815
|
+
var RETAIN_EXTRACTION_MODES = ["concise", "verbose", "verbatim", "chunks"];
|
|
816
|
+
var DEFAULT_RETAIN_EXTRACTION_MODE = "concise";
|
|
817
|
+
var PAGE_TAXONOMY = [
|
|
818
|
+
{
|
|
819
|
+
name: "Component map",
|
|
820
|
+
source_query: "From this project's commit history and past discussions, what are the main components/modules/subsystems, what is each responsible for, and how do they relate to or depend on one another? Describe the structure and responsibilities.",
|
|
821
|
+
tags: ["knowledge:component"]
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
name: "Core concepts",
|
|
825
|
+
source_query: "What are the core concepts, domain abstractions, and key entities in this project \u2014 the vocabulary a developer must understand? For each, explain what it represents and its role, drawn from how they are introduced and discussed across the history and conversations.",
|
|
826
|
+
tags: ["knowledge:concept"]
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
name: "Conventions and patterns",
|
|
830
|
+
source_query: "What conventions, idioms, and recurring patterns does this project follow \u2014 its approach to testing, error handling, naming, structure, and how changes are typically made? Describe how THIS project does things, as evidenced across its history and discussions.",
|
|
831
|
+
tags: ["knowledge:convention"]
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
name: "Key decisions and rationale",
|
|
835
|
+
source_query: "What are the significant technical decisions made in this project and the rationale behind them \u2014 the durable 'why we do it this way' a developer should know? Summarize the decisions and their reasoning from the commit rationales and past conversations.",
|
|
836
|
+
tags: ["knowledge:decision"]
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
name: "Initiatives and enhancements",
|
|
840
|
+
source_query: "Based on this repository's commit history, what are the major initiatives, features, and enhancements the project has worked on? Summarize the themes and notable changes over time. When a source memory's context carries a `[[page:<id>]]` link, repeat that link in the summary of what it describes, so each initiative links to its detailed page.",
|
|
841
|
+
tags: ["knowledge:feature-work"]
|
|
842
|
+
}
|
|
843
|
+
];
|
|
844
|
+
var PAGE_NAMES = PAGE_TAXONOMY.map((page) => page.name);
|
|
845
|
+
var DEFAULT_PAGE_TRIGGER_CRON = "H * * * *";
|
|
846
|
+
var CRON_FIELD_RANGES = [
|
|
847
|
+
[0, 59],
|
|
848
|
+
// minute
|
|
849
|
+
[0, 23],
|
|
850
|
+
// hour
|
|
851
|
+
[1, 31],
|
|
852
|
+
// day of month
|
|
853
|
+
[1, 12],
|
|
854
|
+
// month
|
|
855
|
+
[0, 6]
|
|
856
|
+
// day of week
|
|
857
|
+
];
|
|
858
|
+
var HASHED_FIELD = /^H(?:\((\d+)-(\d+)\))?$/;
|
|
859
|
+
function isHashedCron(cron) {
|
|
860
|
+
return /(^|\s)H/.test(cron);
|
|
861
|
+
}
|
|
862
|
+
function parseHashedCron(cron) {
|
|
863
|
+
const fields = cron.trim().split(/\s+/);
|
|
864
|
+
if (fields.length !== CRON_FIELD_RANGES.length) return void 0;
|
|
865
|
+
for (const [i, field] of fields.entries()) {
|
|
866
|
+
if (!field.startsWith("H")) continue;
|
|
867
|
+
const m = HASHED_FIELD.exec(field);
|
|
868
|
+
if (!m) return void 0;
|
|
869
|
+
if (m[1] === void 0) continue;
|
|
870
|
+
const [lo, hi] = [Number(m[1]), Number(m[2])];
|
|
871
|
+
const [min, max] = CRON_FIELD_RANGES[i];
|
|
872
|
+
if (lo > hi || lo < min || hi > max) return void 0;
|
|
873
|
+
}
|
|
874
|
+
return fields;
|
|
875
|
+
}
|
|
876
|
+
var PLUGIN_GOVERNED_BANK_FIELDS = [
|
|
877
|
+
"retain_strategies",
|
|
878
|
+
"entity_labels",
|
|
879
|
+
"retain_extraction_mode"
|
|
880
|
+
];
|
|
881
|
+
|
|
882
|
+
// package.json
|
|
883
|
+
var version = "0.8.7";
|
|
884
|
+
|
|
885
|
+
// src/core/util.ts
|
|
886
|
+
import { accessSync, constants } from "fs";
|
|
887
|
+
import { delimiter, join as join6 } from "path";
|
|
888
|
+
|
|
889
|
+
// src/core/hindsight.ts
|
|
890
|
+
var USER_AGENT = `hindsight-coding-agents/${version}`;
|
|
891
|
+
var DEFAULT_OBSERVATION_SCOPES = "shared";
|
|
892
|
+
var DEFAULT_PAGE_SEARCH_LIMIT = 10;
|
|
893
|
+
var RETRY_AFTER_FLOOR_MS = 10 * 1e3;
|
|
894
|
+
var RETRY_AFTER_CEILING_MS = 60 * 1e3;
|
|
895
|
+
|
|
896
|
+
// src/core/config.ts
|
|
897
|
+
var CONFIG_PATH = process.env.HINDSIGHT_CONFIG || join7(homedir3(), ".hindsight", "coding-agent.json");
|
|
898
|
+
var DEFAULT_DAEMON_PORT = 9077;
|
|
899
|
+
var DEFAULT_DAEMON_PROFILE = "coding-agent";
|
|
900
|
+
function resolvePageTrigger(raw) {
|
|
901
|
+
if (raw.pageTriggerType === "manual") return { type: "manual" };
|
|
902
|
+
if (raw.pageTriggerType === "auto-refresh") return { type: "auto-refresh" };
|
|
903
|
+
if (raw.pageTriggerType !== void 0 && raw.pageTriggerType !== "cron")
|
|
904
|
+
log.warn(
|
|
905
|
+
"config",
|
|
906
|
+
"ignoring invalid pageTriggerType \u2014 expected cron|auto-refresh|manual"
|
|
907
|
+
);
|
|
908
|
+
const cron = raw.pageTriggerCron?.trim();
|
|
909
|
+
if (!cron) return { type: "cron", cron: DEFAULT_PAGE_TRIGGER_CRON };
|
|
910
|
+
if (!isHashedCron(cron) || parseHashedCron(cron)) return { type: "cron", cron };
|
|
911
|
+
log.warn(
|
|
912
|
+
"config",
|
|
913
|
+
`pageTriggerCron has a malformed hashed field \u2014 write \`H\` or \`H(<lo>-<hi>)\` within the field's own range, e.g. "H H(0-5) * * *" \u2014 falling back to ${JSON.stringify(DEFAULT_PAGE_TRIGGER_CRON)}`
|
|
914
|
+
);
|
|
915
|
+
return { type: "cron", cron: DEFAULT_PAGE_TRIGGER_CRON };
|
|
916
|
+
}
|
|
917
|
+
function resolvePages(raw) {
|
|
918
|
+
const value = raw;
|
|
919
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
920
|
+
const out = {};
|
|
921
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
922
|
+
if (!PAGE_NAMES.some((known) => known.toLowerCase() === name.trim().toLowerCase())) {
|
|
923
|
+
log.warn(
|
|
924
|
+
"config",
|
|
925
|
+
`ignoring unknown pages entry \u2014 no seeded page has that name; expected one of: ${PAGE_NAMES.join(", ")}`
|
|
926
|
+
);
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (entry === false) {
|
|
930
|
+
out[name] = false;
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
const query = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.source_query : void 0;
|
|
934
|
+
if (typeof query === "string" && query.trim()) {
|
|
935
|
+
out[name] = { source_query: query };
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
log.warn(
|
|
939
|
+
"config",
|
|
940
|
+
'ignoring invalid pages entry \u2014 expected false, or {"source_query": "<your question>"}'
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
function resolveCustomPages(raw) {
|
|
946
|
+
const value = raw;
|
|
947
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
948
|
+
const out = {};
|
|
949
|
+
for (const [rawName, entry] of Object.entries(value)) {
|
|
950
|
+
const name = rawName.trim();
|
|
951
|
+
if (!name) continue;
|
|
952
|
+
if (PAGE_NAMES.some((known) => known.toLowerCase() === name.toLowerCase())) {
|
|
953
|
+
log.warn(
|
|
954
|
+
"config",
|
|
955
|
+
"ignoring customPages entry with a seeded page name; reword it under `pages` instead"
|
|
956
|
+
);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const query = entry && typeof entry === "object" && !Array.isArray(entry) ? entry.source_query : void 0;
|
|
960
|
+
if (typeof query !== "string" || !query.trim()) {
|
|
961
|
+
log.warn(
|
|
962
|
+
"config",
|
|
963
|
+
'ignoring invalid customPages entry \u2014 expected {"source_query": "<your question>"}'
|
|
964
|
+
);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const rawTags = entry.tags;
|
|
968
|
+
const tags = Array.isArray(rawTags) ? rawTags.filter((t) => typeof t === "string" && t.trim() !== "") : [];
|
|
969
|
+
out[name] = tags.length ? { source_query: query, tags } : { source_query: query };
|
|
970
|
+
}
|
|
971
|
+
return out;
|
|
972
|
+
}
|
|
973
|
+
var DEFAULT_REFLECT_TIMEOUT_MS = 2e4;
|
|
974
|
+
var DEFAULT_REFLECT_TOOL_TIMEOUT_MS = 33e4;
|
|
975
|
+
var DEFAULT_INJECT_TIMEOUT_MS = 7e3;
|
|
976
|
+
var REFLECT_BUDGETS = ["low", "mid", "high"];
|
|
977
|
+
function resolveReflectBudget(raw) {
|
|
978
|
+
const value = raw.reflectBudget;
|
|
979
|
+
if (value === void 0) return "high";
|
|
980
|
+
if (typeof value === "string" && REFLECT_BUDGETS.includes(value))
|
|
981
|
+
return value;
|
|
982
|
+
log.warn("config", "ignoring invalid reflectBudget \u2014 expected low|mid|high");
|
|
983
|
+
return "high";
|
|
984
|
+
}
|
|
985
|
+
var OBSERVATION_SCOPE_MODES = [
|
|
986
|
+
"shared",
|
|
987
|
+
"combined",
|
|
988
|
+
"per_tag",
|
|
989
|
+
"all_combinations",
|
|
990
|
+
"per_source"
|
|
991
|
+
];
|
|
992
|
+
function resolveObservationScopes(raw) {
|
|
993
|
+
const value = raw;
|
|
994
|
+
if (typeof value === "string")
|
|
995
|
+
return OBSERVATION_SCOPE_MODES.includes(value) ? value : DEFAULT_OBSERVATION_SCOPES;
|
|
996
|
+
if (Array.isArray(value)) {
|
|
997
|
+
const scopes = value.filter((scope) => Array.isArray(scope)).map((scope) => scope.filter((t) => typeof t === "string" && t.trim() !== ""));
|
|
998
|
+
if (scopes.length) return scopes;
|
|
999
|
+
}
|
|
1000
|
+
return DEFAULT_OBSERVATION_SCOPES;
|
|
1001
|
+
}
|
|
1002
|
+
function resolveDefaultBankConfig(raw) {
|
|
1003
|
+
const value = raw;
|
|
1004
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
1005
|
+
const out = {};
|
|
1006
|
+
for (const [key, v] of Object.entries(value)) {
|
|
1007
|
+
if (PLUGIN_GOVERNED_BANK_FIELDS.includes(key)) {
|
|
1008
|
+
log.warn(
|
|
1009
|
+
"config",
|
|
1010
|
+
`ignoring defaultBankConfig.${key} \u2014 the plugin manages this field itself` + (key === "retain_extraction_mode" ? " (set retainExtractionMode instead)" : "")
|
|
1011
|
+
);
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
out[key] = v;
|
|
1015
|
+
}
|
|
1016
|
+
return out;
|
|
1017
|
+
}
|
|
1018
|
+
var AUTO_INJECT_MODES = ["reflect", "pages", "recall", "none"];
|
|
1019
|
+
function resolveAutoInject(raw) {
|
|
1020
|
+
if (raw.autoReflect !== void 0) {
|
|
1021
|
+
const replacement = raw.autoReflect === false ? "none" : "reflect";
|
|
1022
|
+
log.warn("config", `autoReflect is deprecated \u2014 use autoInject: "${replacement}" instead`);
|
|
1023
|
+
}
|
|
1024
|
+
if (AUTO_INJECT_MODES.includes(raw.autoInject)) return raw.autoInject;
|
|
1025
|
+
return raw.autoReflect === false ? "none" : "reflect";
|
|
1026
|
+
}
|
|
1027
|
+
function resolveConfig(raw = {}) {
|
|
1028
|
+
const serverMode = ["cloud", "self-hosted", "daemon"].includes(raw.serverMode) ? raw.serverMode : "self-hosted";
|
|
1029
|
+
const apiPort = raw.apiPort || DEFAULT_DAEMON_PORT;
|
|
1030
|
+
const pageTrigger = resolvePageTrigger(raw);
|
|
1031
|
+
return {
|
|
1032
|
+
serverMode,
|
|
1033
|
+
// Only loadConfig can supply the operator-managed router endpoint.
|
|
1034
|
+
apiUrl: raw.apiUrl ?? "",
|
|
1035
|
+
apiToken: raw.apiToken || void 0,
|
|
1036
|
+
apiPort,
|
|
1037
|
+
daemonIdleTimeout: raw.daemonIdleTimeout,
|
|
1038
|
+
daemonProfile: raw.daemonProfile || DEFAULT_DAEMON_PROFILE,
|
|
1039
|
+
embedVersion: raw.embedVersion || void 0,
|
|
1040
|
+
embedPackagePath: raw.embedPackagePath || void 0,
|
|
1041
|
+
bankId: raw.bankId,
|
|
1042
|
+
dynamicBankId: raw.dynamicBankId,
|
|
1043
|
+
bankIdTemplate: raw.bankIdTemplate,
|
|
1044
|
+
mapPathToBank: raw.mapPathToBank,
|
|
1045
|
+
resolveWorktrees: raw.resolveWorktrees,
|
|
1046
|
+
optInOnly: raw.optInOnly ?? false,
|
|
1047
|
+
// Same shape as retainTags: a config typo must not become a path that silently approves nothing.
|
|
1048
|
+
optInPaths: Array.isArray(raw.optInPaths) ? raw.optInPaths.filter((p) => typeof p === "string" && p.trim() !== "") : [],
|
|
1049
|
+
harness: raw.harness ?? "opencode",
|
|
1050
|
+
disabled: raw.disabled ?? false,
|
|
1051
|
+
retainSessions: raw.retainSessions ?? true,
|
|
1052
|
+
// write sessions back by default, every harness
|
|
1053
|
+
manageBankConfig: raw.manageBankConfig ?? true,
|
|
1054
|
+
retainExtractionMode: RETAIN_EXTRACTION_MODES.includes(raw.retainExtractionMode) ? raw.retainExtractionMode : DEFAULT_RETAIN_EXTRACTION_MODE,
|
|
1055
|
+
defaultBankConfig: resolveDefaultBankConfig(raw.defaultBankConfig),
|
|
1056
|
+
maxParallelRetains: raw.maxParallelRetains || 10,
|
|
1057
|
+
reflectTimeoutMs: raw.reflectTimeoutMs || DEFAULT_REFLECT_TIMEOUT_MS,
|
|
1058
|
+
// Inherit an explicitly-raised reflectTimeoutMs (that is what users reaching for a longer
|
|
1059
|
+
// reflect already set), but never let it LOWER the tool below the default — a short window is
|
|
1060
|
+
// set to bound the automatic hook, not to cut off a call the agent is waiting on.
|
|
1061
|
+
reflectToolTimeoutMs: raw.reflectToolTimeoutMs || Math.max(raw.reflectTimeoutMs || 0, DEFAULT_REFLECT_TOOL_TIMEOUT_MS),
|
|
1062
|
+
injectTimeoutMs: raw.injectTimeoutMs || DEFAULT_INJECT_TIMEOUT_MS,
|
|
1063
|
+
reflectBudget: resolveReflectBudget(raw),
|
|
1064
|
+
autoInject: resolveAutoInject(raw),
|
|
1065
|
+
pageSearchLimit: raw.pageSearchLimit || DEFAULT_PAGE_SEARCH_LIMIT,
|
|
1066
|
+
// Same shape as retainMetadata: an object, or nothing. An array would spread into numeric
|
|
1067
|
+
// keys and reach the API as garbage, so it is rejected like any other non-object.
|
|
1068
|
+
recallOptions: raw.recallOptions && typeof raw.recallOptions === "object" && !Array.isArray(raw.recallOptions) ? { ...raw.recallOptions } : {},
|
|
1069
|
+
toolGuideExtra: typeof raw.toolGuideExtra === "string" && raw.toolGuideExtra.trim() ? raw.toolGuideExtra : void 0,
|
|
1070
|
+
// Every turn, not every tenth. The guide is what tells the agent WHEN to reach for memory, and
|
|
1071
|
+
// at a cadence of 10 a normal session is told once, on turn 1, and never again. Measured over
|
|
1072
|
+
// 40 real Claude Code turns, moving this from 10 to 1 took searches from 15% of turns to 32.5%
|
|
1073
|
+
// with no wording change at all. The cost is the guide's ~2KB re-sent per turn, which caches.
|
|
1074
|
+
pageRefreshEveryTurns: raw.pageRefreshEveryTurns || 1,
|
|
1075
|
+
pageTriggerType: pageTrigger.type,
|
|
1076
|
+
pageTriggerCron: pageTrigger.cron,
|
|
1077
|
+
pages: resolvePages(raw.pages),
|
|
1078
|
+
customPages: resolveCustomPages(raw.customPages),
|
|
1079
|
+
autoSeed: raw.autoSeed ?? true,
|
|
1080
|
+
seedLimit: raw.seedLimit || DEFAULT_SEED_LIMIT,
|
|
1081
|
+
codebaseSurvey: raw.codebaseSurvey ?? true,
|
|
1082
|
+
surveyModel: raw.surveyModel || "haiku",
|
|
1083
|
+
surveyBudgetUsd: raw.surveyBudgetUsd || 2,
|
|
1084
|
+
surveyRefreshCommits: raw.surveyRefreshCommits ?? 20,
|
|
1085
|
+
gitIngest: ["message", "full", "none"].includes(raw.gitIngest) ? raw.gitIngest : "message",
|
|
1086
|
+
// Hostile input is a config typo, not an attack: keep only string entries so a stray number or
|
|
1087
|
+
// nested object cannot reach the API as a tag and fail the whole retain.
|
|
1088
|
+
retainTags: Array.isArray(raw.retainTags) ? raw.retainTags.filter((t) => typeof t === "string" && t.trim() !== "") : [],
|
|
1089
|
+
// Undefined, not "", when unset or blank: the retain call distinguishes "not configured" (use
|
|
1090
|
+
// the built-in default) from a configured value, and an empty context would strip the default.
|
|
1091
|
+
retainContext: typeof raw.retainContext === "string" && raw.retainContext.trim() !== "" ? raw.retainContext : void 0,
|
|
1092
|
+
retainMetadata: raw.retainMetadata && typeof raw.retainMetadata === "object" ? Object.fromEntries(
|
|
1093
|
+
Object.entries(raw.retainMetadata).filter(([, v]) => typeof v === "string")
|
|
1094
|
+
) : {},
|
|
1095
|
+
observationScopes: resolveObservationScopes(raw.observationScopes),
|
|
1096
|
+
banks: raw.banks && typeof raw.banks === "object" ? raw.banks : {},
|
|
1097
|
+
paths: raw.paths && typeof raw.paths === "object" ? raw.paths : {},
|
|
1098
|
+
logLevel: ["debug", "info", "warn", "error"].includes(raw.logLevel) ? raw.logLevel : "info",
|
|
1099
|
+
autoUpdate: raw.autoUpdate ?? true
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function readRaw(path) {
|
|
1103
|
+
try {
|
|
1104
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1105
|
+
} catch (e) {
|
|
1106
|
+
if (e?.code !== "ENOENT") {
|
|
1107
|
+
console.error(`hindsight: ignoring invalid config at ${path}: ${e?.message || e}`);
|
|
1108
|
+
}
|
|
1109
|
+
return {};
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
function mergeRaw(a, b) {
|
|
1113
|
+
const { harnesses: _drop, ...flat } = b;
|
|
1114
|
+
return {
|
|
1115
|
+
...a,
|
|
1116
|
+
...flat,
|
|
1117
|
+
banks: { ...a.banks ?? {}, ...b.banks ?? {} },
|
|
1118
|
+
paths: { ...a.paths ?? {}, ...b.paths ?? {} }
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function applyLayer(raw, layer, harness) {
|
|
1122
|
+
let out = mergeRaw(raw, layer);
|
|
1123
|
+
const perHarness = harness ? layer.harnesses?.[harness] : void 0;
|
|
1124
|
+
if (perHarness) out = mergeRaw(out, perHarness);
|
|
1125
|
+
return out;
|
|
1126
|
+
}
|
|
1127
|
+
var ENV_KEYS = {
|
|
1128
|
+
serverMode: "HINDSIGHT_SERVER_MODE",
|
|
1129
|
+
apiUrl: "HINDSIGHT_API_URL",
|
|
1130
|
+
apiToken: "HINDSIGHT_API_TOKEN",
|
|
1131
|
+
// These four keep the names the old per-agent Claude Code plugin used, so a user migrating from
|
|
1132
|
+
// it can carry their existing environment over unchanged.
|
|
1133
|
+
apiPort: "HINDSIGHT_API_PORT",
|
|
1134
|
+
daemonIdleTimeout: "HINDSIGHT_DAEMON_IDLE_TIMEOUT",
|
|
1135
|
+
embedVersion: "HINDSIGHT_EMBED_VERSION",
|
|
1136
|
+
embedPackagePath: "HINDSIGHT_EMBED_PACKAGE_PATH",
|
|
1137
|
+
daemonProfile: "HINDSIGHT_DAEMON_PROFILE",
|
|
1138
|
+
bankId: "HINDSIGHT_BANK_ID",
|
|
1139
|
+
dynamicBankId: "HINDSIGHT_DYNAMIC_BANK_ID",
|
|
1140
|
+
bankIdTemplate: "HINDSIGHT_BANK_ID_TEMPLATE",
|
|
1141
|
+
resolveWorktrees: "HINDSIGHT_RESOLVE_WORKTREES",
|
|
1142
|
+
optInOnly: "HINDSIGHT_OPT_IN_ONLY",
|
|
1143
|
+
optInPaths: "HINDSIGHT_OPT_IN_PATHS",
|
|
1144
|
+
harness: "HINDSIGHT_HARNESS",
|
|
1145
|
+
disabled: "HINDSIGHT_DISABLED",
|
|
1146
|
+
retainSessions: "HINDSIGHT_RETAIN_SESSIONS",
|
|
1147
|
+
maxParallelRetains: "HINDSIGHT_MAX_PARALLEL_RETAINS",
|
|
1148
|
+
reflectTimeoutMs: "HINDSIGHT_REFLECT_TIMEOUT_MS",
|
|
1149
|
+
reflectToolTimeoutMs: "HINDSIGHT_REFLECT_TOOL_TIMEOUT_MS",
|
|
1150
|
+
injectTimeoutMs: "HINDSIGHT_INJECT_TIMEOUT_MS",
|
|
1151
|
+
reflectBudget: "HINDSIGHT_REFLECT_BUDGET",
|
|
1152
|
+
autoInject: "HINDSIGHT_AUTO_INJECT",
|
|
1153
|
+
pageSearchLimit: "HINDSIGHT_PAGE_SEARCH_LIMIT",
|
|
1154
|
+
toolGuideExtra: "HINDSIGHT_TOOL_GUIDE_EXTRA",
|
|
1155
|
+
autoReflect: "HINDSIGHT_AUTO_REFLECT",
|
|
1156
|
+
pageRefreshEveryTurns: "HINDSIGHT_PAGE_REFRESH_EVERY_TURNS",
|
|
1157
|
+
pageTriggerType: "HINDSIGHT_PAGE_TRIGGER_TYPE",
|
|
1158
|
+
pageTriggerCron: "HINDSIGHT_PAGE_TRIGGER_CRON",
|
|
1159
|
+
autoSeed: "HINDSIGHT_AUTO_SEED",
|
|
1160
|
+
seedLimit: "HINDSIGHT_SEED_LIMIT",
|
|
1161
|
+
codebaseSurvey: "HINDSIGHT_CODEBASE_SURVEY",
|
|
1162
|
+
surveyModel: "HINDSIGHT_SURVEY_MODEL",
|
|
1163
|
+
surveyBudgetUsd: "HINDSIGHT_SURVEY_BUDGET_USD",
|
|
1164
|
+
surveyRefreshCommits: "HINDSIGHT_SURVEY_REFRESH_COMMITS",
|
|
1165
|
+
logLevel: "HINDSIGHT_LOG_LEVEL",
|
|
1166
|
+
autoUpdate: "HINDSIGHT_AUTO_UPDATE",
|
|
1167
|
+
gitIngest: "HINDSIGHT_GIT_INGEST",
|
|
1168
|
+
// Scalar modes only ("shared", "combined", "per_tag", "all_combinations"). An explicit scope
|
|
1169
|
+
// list is a list OF lists, which does not survive flattening into one variable — file-only.
|
|
1170
|
+
observationScopes: "HINDSIGHT_OBSERVATION_SCOPES",
|
|
1171
|
+
// Comma-separated, e.g. HINDSIGHT_RETAIN_TAGS="project:{gitProject},env:work". A LIST rather than
|
|
1172
|
+
// a map, so it flattens cleanly; its sibling retainMetadata stays file-only for the reason above.
|
|
1173
|
+
retainTags: "HINDSIGHT_RETAIN_TAGS",
|
|
1174
|
+
manageBankConfig: "HINDSIGHT_MANAGE_BANK_CONFIG",
|
|
1175
|
+
retainExtractionMode: "HINDSIGHT_RETAIN_EXTRACTION_MODE"
|
|
1176
|
+
};
|
|
1177
|
+
var ENV_BOOLEANS = /* @__PURE__ */ new Set([
|
|
1178
|
+
"dynamicBankId",
|
|
1179
|
+
"resolveWorktrees",
|
|
1180
|
+
"optInOnly",
|
|
1181
|
+
"disabled",
|
|
1182
|
+
"retainSessions",
|
|
1183
|
+
"autoReflect",
|
|
1184
|
+
"autoSeed",
|
|
1185
|
+
"codebaseSurvey",
|
|
1186
|
+
"autoUpdate",
|
|
1187
|
+
"manageBankConfig"
|
|
1188
|
+
]);
|
|
1189
|
+
var ENV_LISTS = /* @__PURE__ */ new Set(["retainTags", "optInPaths"]);
|
|
1190
|
+
var ENV_NUMBERS = /* @__PURE__ */ new Set([
|
|
1191
|
+
"apiPort",
|
|
1192
|
+
"daemonIdleTimeout",
|
|
1193
|
+
"maxParallelRetains",
|
|
1194
|
+
"reflectTimeoutMs",
|
|
1195
|
+
"reflectToolTimeoutMs",
|
|
1196
|
+
"injectTimeoutMs",
|
|
1197
|
+
"pageSearchLimit",
|
|
1198
|
+
"pageRefreshEveryTurns",
|
|
1199
|
+
"seedLimit",
|
|
1200
|
+
"surveyBudgetUsd",
|
|
1201
|
+
"surveyRefreshCommits"
|
|
1202
|
+
]);
|
|
1203
|
+
function readEnvConfig(env = process.env) {
|
|
1204
|
+
const out = {};
|
|
1205
|
+
for (const [key, name] of Object.entries(ENV_KEYS)) {
|
|
1206
|
+
const value = env[name];
|
|
1207
|
+
if (value === void 0 || value.trim() === "") continue;
|
|
1208
|
+
if (ENV_BOOLEANS.has(key)) {
|
|
1209
|
+
out[key] = ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
1210
|
+
} else if (ENV_LISTS.has(key)) {
|
|
1211
|
+
const items = value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
1212
|
+
if (items.length) out[key] = items;
|
|
1213
|
+
} else if (ENV_NUMBERS.has(key)) {
|
|
1214
|
+
const n = Number(value);
|
|
1215
|
+
if (Number.isFinite(n)) out[key] = n;
|
|
1216
|
+
else console.error(`hindsight: ignoring ${name}=${value} \u2014 not a number`);
|
|
1217
|
+
} else {
|
|
1218
|
+
out[key] = value;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return out;
|
|
1222
|
+
}
|
|
1223
|
+
function loadConfig(opts = {}) {
|
|
1224
|
+
const o = typeof opts === "string" ? { path: opts } : opts;
|
|
1225
|
+
const withEnv = applyLayer({}, readEnvConfig(), o.harness);
|
|
1226
|
+
const raw = applyLayer(withEnv, readRaw(o.path ?? CONFIG_PATH), o.harness);
|
|
1227
|
+
if (!raw.harness && o.harness) raw.harness = o.harness;
|
|
1228
|
+
const managed = managedSettings(o.harness);
|
|
1229
|
+
if (raw.apiUrl !== void 0 && raw.apiUrl.replace(/\/+$/, "") !== managed.apiUrl || raw.serverMode !== void 0 && raw.serverMode !== "self-hosted") throw new AccessDeniedError();
|
|
1230
|
+
return { ...resolveConfig(raw), ...managed };
|
|
1231
|
+
}
|
|
1232
|
+
var BANK_OVERRIDE_EXCLUDED = [
|
|
1233
|
+
"bankId",
|
|
1234
|
+
"bankIdTemplate",
|
|
1235
|
+
"mapPathToBank",
|
|
1236
|
+
"dynamicBankId",
|
|
1237
|
+
"resolveWorktrees",
|
|
1238
|
+
// Approval is decided BEFORE the bank is resolved, so a `banks.<id>` section naming these could
|
|
1239
|
+
// only ever arrive too late to matter — strip them rather than let them read as effective.
|
|
1240
|
+
"optInOnly",
|
|
1241
|
+
"optInPaths",
|
|
1242
|
+
"harness",
|
|
1243
|
+
"routerHarness",
|
|
1244
|
+
"toolPrefix",
|
|
1245
|
+
"apiUrl",
|
|
1246
|
+
"apiToken",
|
|
1247
|
+
"serverMode",
|
|
1248
|
+
"autoUpdate"
|
|
1249
|
+
];
|
|
1250
|
+
function applyBankConfig(cfg, resolvedId, directory) {
|
|
1251
|
+
if (directory !== void 0 && !isOptedIn(cfg, directory))
|
|
1252
|
+
return { cfg: { ...cfg, ...managedSettings(cfg.routerHarness), disabled: true }, bankId: resolvedId };
|
|
1253
|
+
const byPath = directory === void 0 ? void 0 : pathSection(cfg, cfg.paths, directory);
|
|
1254
|
+
if (byPath) {
|
|
1255
|
+
const { bank: _rename, ...rest } = overrideFields(byPath);
|
|
1256
|
+
cfg = { ...cfg, ...resolvePartial(cfg, rest) };
|
|
1257
|
+
}
|
|
1258
|
+
const section = cfg.banks[resolvedId];
|
|
1259
|
+
if (!section) return { cfg: { ...cfg, ...managedSettings(cfg.routerHarness) }, bankId: resolvedId };
|
|
1260
|
+
const safe = overrideFields(section);
|
|
1261
|
+
const bankId = resolvedId;
|
|
1262
|
+
delete safe.bank;
|
|
1263
|
+
return { cfg: { ...cfg, ...resolvePartial(cfg, safe), ...managedSettings(cfg.routerHarness) }, bankId };
|
|
1264
|
+
}
|
|
1265
|
+
function overrideFields(section) {
|
|
1266
|
+
const safe = { ...section };
|
|
1267
|
+
for (const k of BANK_OVERRIDE_EXCLUDED) delete safe[k];
|
|
1268
|
+
delete safe.banks;
|
|
1269
|
+
delete safe.paths;
|
|
1270
|
+
return safe;
|
|
1271
|
+
}
|
|
1272
|
+
function resolvePartial(cfg, patch) {
|
|
1273
|
+
const full = resolveConfig(patch);
|
|
1274
|
+
const out = {};
|
|
1275
|
+
for (const key of Object.keys(patch)) {
|
|
1276
|
+
if (key in full)
|
|
1277
|
+
out[key] = full[key];
|
|
1278
|
+
}
|
|
1279
|
+
if ("autoReflect" in patch && !("autoInject" in patch)) out.autoInject = full.autoInject;
|
|
1280
|
+
if ("serverMode" in patch || "apiUrl" in patch || "apiPort" in patch) {
|
|
1281
|
+
const conn = resolveConfig({
|
|
1282
|
+
serverMode: patch.serverMode ?? cfg.serverMode,
|
|
1283
|
+
apiPort: patch.apiPort ?? cfg.apiPort,
|
|
1284
|
+
apiUrl: patch.apiUrl ?? (cfg.serverMode === "daemon" ? void 0 : cfg.apiUrl)
|
|
1285
|
+
});
|
|
1286
|
+
out.serverMode = conn.serverMode;
|
|
1287
|
+
out.apiUrl = conn.apiUrl;
|
|
1288
|
+
out.apiPort = conn.apiPort;
|
|
1289
|
+
}
|
|
1290
|
+
return out;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// src/core/brand.ts
|
|
1294
|
+
function brandWord(prefix = DEFAULT_TOOL_PREFIX) {
|
|
1295
|
+
const start = [0, 116, 217];
|
|
1296
|
+
const end = [0, 146, 150];
|
|
1297
|
+
const word = toolBrand(prefix).displayName;
|
|
1298
|
+
let out = "";
|
|
1299
|
+
for (let i = 0; i < word.length; i++) {
|
|
1300
|
+
const t = i / Math.max(1, word.length - 1);
|
|
1301
|
+
const [r, g, b] = start.map((s, k) => Math.round(s + (end[k] - s) * t));
|
|
1302
|
+
out += `\x1B[38;2;${r};${g};${b}m${word[i]}`;
|
|
1303
|
+
}
|
|
1304
|
+
return `${out}\x1B[0m`;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/antigravity-statusline.ts
|
|
1308
|
+
function buildAntigravityStatusLine(state, cfg) {
|
|
1309
|
+
const cwd = state.cwd || state.workspace?.current_dir;
|
|
1310
|
+
if (!cwd) return brandWord(cfg.toolPrefix);
|
|
1311
|
+
const bankId = deriveBankIdOrSkip(cfg, cwd, "antigravity-cli");
|
|
1312
|
+
if (bankId === null) return brandWord(cfg.toolPrefix);
|
|
1313
|
+
const resolved2 = applyBankConfig(cfg, bankId, cwd);
|
|
1314
|
+
return resolved2.cfg.disabled ? "" : `${brandWord(cfg.toolPrefix)} \xB7 ${resolved2.bankId}`;
|
|
1315
|
+
}
|
|
1316
|
+
async function main() {
|
|
1317
|
+
const chunks = [];
|
|
1318
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
1319
|
+
try {
|
|
1320
|
+
const state = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1321
|
+
const cfg = loadConfig({ harness: "antigravity-cli" });
|
|
1322
|
+
process.stdout.write(buildAntigravityStatusLine(state, cfg));
|
|
1323
|
+
} catch {
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (process.argv[1]?.endsWith("antigravity-statusline.js")) void main();
|
|
1327
|
+
export {
|
|
1328
|
+
buildAntigravityStatusLine
|
|
1329
|
+
};
|