@yanlinglabs/winter-agent-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/brand.d.ts +113 -0
- package/dist/errors.d.ts +42 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +2191 -0
- package/dist/options.d.ts +252 -0
- package/dist/paths/home.d.ts +48 -0
- package/dist/paths/keys.d.ts +6 -0
- package/dist/paths/project-key.d.ts +1 -0
- package/dist/permissions/types.d.ts +272 -0
- package/dist/protocol/codec.d.ts +10 -0
- package/dist/protocol/config.d.ts +535 -0
- package/dist/protocol/frames.d.ts +598 -0
- package/dist/query.d.ts +81 -0
- package/dist/sessions.d.ts +50 -0
- package/dist/settings/model-slots.d.ts +33 -0
- package/dist/settings/resolve.d.ts +79 -0
- package/dist/settings/sources.d.ts +29 -0
- package/dist/settings/types.d.ts +291 -0
- package/dist/store/fork-session.d.ts +19 -0
- package/dist/store/leases.d.ts +15 -0
- package/dist/store/session-store.d.ts +111 -0
- package/dist/transport.d.ts +26 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2191 @@
|
|
|
1
|
+
// src/query.ts
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
// src/protocol/frames.ts
|
|
5
|
+
var PROTOCOL_VERSION = "1.0";
|
|
6
|
+
|
|
7
|
+
// src/protocol/codec.ts
|
|
8
|
+
class ProtocolError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "ProtocolError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function encodeFrame(frame) {
|
|
15
|
+
return JSON.stringify(frame) + `
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
function decodeFrame(line) {
|
|
19
|
+
let v;
|
|
20
|
+
try {
|
|
21
|
+
v = JSON.parse(line);
|
|
22
|
+
} catch {
|
|
23
|
+
throw new ProtocolError(`malformed JSON frame: ${line.slice(0, 80)}`);
|
|
24
|
+
}
|
|
25
|
+
if (typeof v !== "object" || v === null || Array.isArray(v) || typeof v.type !== "string")
|
|
26
|
+
throw new ProtocolError("frame missing string 'type'");
|
|
27
|
+
return v;
|
|
28
|
+
}
|
|
29
|
+
function splitFrames(chunk, carry) {
|
|
30
|
+
const text = carry + chunk;
|
|
31
|
+
const parts = text.split(`
|
|
32
|
+
`);
|
|
33
|
+
const nextCarry = parts.pop() ?? "";
|
|
34
|
+
const frames = parts.filter((l) => l.length > 0).map(decodeFrame);
|
|
35
|
+
return { frames, carry: nextCarry };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/brand.ts
|
|
39
|
+
var BRAND_TOKEN_RE = /^[a-z][a-z0-9-]{0,31}$/;
|
|
40
|
+
var DOT_DIR_RE = /^\.[a-z][a-z0-9-]{0,31}$/;
|
|
41
|
+
var INSTRUCTIONS_FILE_RE = /^[A-Z][A-Z0-9_]{0,31}\.md$/;
|
|
42
|
+
var ENV_PREFIX_RE = /^[A-Z][A-Z0-9]{0,15}_$/;
|
|
43
|
+
var KEYCHAIN_SERVICE_RE = /^[a-z][a-z0-9.-]{0,63}$/;
|
|
44
|
+
var MAX_PRODUCT_NAME = 64;
|
|
45
|
+
var CONTROL_BYTE_RE = /[\u0000-\u001f\u007f]/;
|
|
46
|
+
function isHttpsUrl(value) {
|
|
47
|
+
let url;
|
|
48
|
+
try {
|
|
49
|
+
url = new URL(value);
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (CONTROL_BYTE_RE.test(value))
|
|
54
|
+
return false;
|
|
55
|
+
return url.protocol === "https:";
|
|
56
|
+
}
|
|
57
|
+
var FIRST_PARTY_ORIGINATORS = ["codex", "codex_cli_rs", "openai", "anthropic", "claude", "claude-code"];
|
|
58
|
+
var WINTER_BRAND = Object.freeze({
|
|
59
|
+
productName: "Winter",
|
|
60
|
+
packageName: "winter-agent-sdk",
|
|
61
|
+
homeDirName: ".winter",
|
|
62
|
+
projectDirName: ".winter",
|
|
63
|
+
instructionsFile: "WINTER.md",
|
|
64
|
+
envPrefix: "WINTER_",
|
|
65
|
+
keychainService: "com.winter.core",
|
|
66
|
+
mcpServerName: "winter",
|
|
67
|
+
presetName: "winter_code",
|
|
68
|
+
processLabel: "winter",
|
|
69
|
+
codexOriginator: "winter",
|
|
70
|
+
tempRootName: "winter",
|
|
71
|
+
pluginManifestDir: ".winter-plugin",
|
|
72
|
+
contactUrl: "https://github.com/yanlingLabs/winter-agent-sdk"
|
|
73
|
+
});
|
|
74
|
+
var FIELD_RULES = [
|
|
75
|
+
{ field: "packageName", re: BRAND_TOKEN_RE, shape: "a lowercase token of 1-32 chars: a letter, then letters/digits/hyphens" },
|
|
76
|
+
{ field: "mcpServerName", re: BRAND_TOKEN_RE, shape: "a lowercase token of 1-32 chars: a letter, then letters/digits/hyphens" },
|
|
77
|
+
{ field: "processLabel", re: BRAND_TOKEN_RE, shape: "a lowercase token of 1-32 chars: a letter, then letters/digits/hyphens" },
|
|
78
|
+
{ field: "tempRootName", re: BRAND_TOKEN_RE, shape: "a lowercase token of 1-32 chars: a letter, then letters/digits/hyphens" },
|
|
79
|
+
{ field: "codexOriginator", re: BRAND_TOKEN_RE, shape: "a lowercase token of 1-32 chars: a letter, then letters/digits/hyphens" },
|
|
80
|
+
{ field: "homeDirName", re: DOT_DIR_RE, shape: "a LEADING DOT then a lowercase token (it names a hidden directory)" },
|
|
81
|
+
{ field: "projectDirName", re: DOT_DIR_RE, shape: "a LEADING DOT then a lowercase token (it names a hidden directory)" },
|
|
82
|
+
{ field: "pluginManifestDir", re: DOT_DIR_RE, shape: "a LEADING DOT then a lowercase token (it names a hidden directory)" },
|
|
83
|
+
{ field: "instructionsFile", re: INSTRUCTIONS_FILE_RE, shape: 'an UPPERCASE name with a `.md` extension, e.g. "ACME.md"' },
|
|
84
|
+
{ field: "envPrefix", re: ENV_PREFIX_RE, shape: 'an UPPERCASE prefix ENDING IN AN UNDERSCORE, e.g. "ACME_"' },
|
|
85
|
+
{ field: "keychainService", re: KEYCHAIN_SERVICE_RE, shape: 'a lowercase reverse-DNS-style service name, e.g. "com.acme.core"' }
|
|
86
|
+
];
|
|
87
|
+
function resolveBrand(partial) {
|
|
88
|
+
const p = partial ?? {};
|
|
89
|
+
const pick = (key) => p[key] === undefined ? WINTER_BRAND[key] : p[key];
|
|
90
|
+
const brand = {
|
|
91
|
+
productName: pick("productName"),
|
|
92
|
+
packageName: pick("packageName"),
|
|
93
|
+
homeDirName: pick("homeDirName"),
|
|
94
|
+
projectDirName: pick("projectDirName"),
|
|
95
|
+
instructionsFile: pick("instructionsFile"),
|
|
96
|
+
envPrefix: pick("envPrefix"),
|
|
97
|
+
keychainService: pick("keychainService"),
|
|
98
|
+
mcpServerName: pick("mcpServerName"),
|
|
99
|
+
presetName: pick("presetName"),
|
|
100
|
+
processLabel: pick("processLabel"),
|
|
101
|
+
codexOriginator: pick("codexOriginator"),
|
|
102
|
+
tempRootName: pick("tempRootName"),
|
|
103
|
+
pluginManifestDir: pick("pluginManifestDir"),
|
|
104
|
+
contactUrl: pick("contactUrl")
|
|
105
|
+
};
|
|
106
|
+
for (const key of Object.keys(brand)) {
|
|
107
|
+
if (typeof brand[key] !== "string")
|
|
108
|
+
return { ok: false, reason: `brand.${key}: expected a string, got ${brand[key] === null ? "null" : typeof brand[key]}` };
|
|
109
|
+
}
|
|
110
|
+
if (brand.productName.length === 0 || brand.productName.length > MAX_PRODUCT_NAME) {
|
|
111
|
+
return { ok: false, reason: `brand.productName: expected 1-${MAX_PRODUCT_NAME} characters, got ${brand.productName.length}` };
|
|
112
|
+
}
|
|
113
|
+
if (brand.presetName.length === 0)
|
|
114
|
+
return { ok: false, reason: "brand.presetName: expected a non-empty preset name" };
|
|
115
|
+
for (const rule of FIELD_RULES) {
|
|
116
|
+
const value = brand[rule.field];
|
|
117
|
+
if (!rule.re.test(value))
|
|
118
|
+
return { ok: false, reason: `brand.${rule.field}: ${JSON.stringify(value)} is not ${rule.shape}` };
|
|
119
|
+
}
|
|
120
|
+
if (!isHttpsUrl(brand.contactUrl)) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
reason: `brand.contactUrl: ${JSON.stringify(brand.contactUrl)} is not an https:// URL — it is published to vendors in identity headers as the way to reach whoever runs this client`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (FIRST_PARTY_ORIGINATORS.includes(brand.codexOriginator)) {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
reason: `brand.codexOriginator: ${JSON.stringify(brand.codexOriginator)} is a first-party value. ` + `The originator field names the CLIENT, and sending a vendor's own name presents this software as that vendor's tool — ` + `supply your own token instead (one of: ${FIRST_PARTY_ORIGINATORS.join(", ")} is never it).`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return { ok: true, brand };
|
|
133
|
+
}
|
|
134
|
+
function envName(brand, suffix) {
|
|
135
|
+
return `${brand.envPrefix}${suffix}`;
|
|
136
|
+
}
|
|
137
|
+
function mcpToolName(brand, tool) {
|
|
138
|
+
return `mcp__${brand.mcpServerName}__${tool}`;
|
|
139
|
+
}
|
|
140
|
+
function userAgent(brand, version) {
|
|
141
|
+
return `${brand.packageName}/${version}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/options.ts
|
|
145
|
+
var SYSTEM_PROMPT_DYNAMIC_BOUNDARY = "__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__";
|
|
146
|
+
var DEFAULT_CONTEXT_WINDOW_TOKENS = 200000;
|
|
147
|
+
var DEFAULT_COMPACTION_THRESHOLD = 0.92;
|
|
148
|
+
var DEFAULT_PLANS_DIRECTORY = `${WINTER_BRAND.projectDirName}/plans`;
|
|
149
|
+
var DEFAULT_OUTPUT_STYLE = "default";
|
|
150
|
+
var DEFAULT_PROVIDER_STALL_TIMEOUT_MS = 120000;
|
|
151
|
+
var DEFAULT_KEYCHAIN_SERVICE = WINTER_BRAND.keychainService;
|
|
152
|
+
function isWinterMcpServerInstance(value) {
|
|
153
|
+
if (typeof value !== "object" || value === null)
|
|
154
|
+
return false;
|
|
155
|
+
const candidate = value;
|
|
156
|
+
return typeof candidate.listTools === "function" && typeof candidate.callTool === "function";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/transport.ts
|
|
160
|
+
import { createRequire } from "node:module";
|
|
161
|
+
import { dirname, join } from "node:path";
|
|
162
|
+
import { spawn } from "node:child_process";
|
|
163
|
+
|
|
164
|
+
// src/errors.ts
|
|
165
|
+
class WinterSDKError extends Error {
|
|
166
|
+
constructor(message) {
|
|
167
|
+
super(message);
|
|
168
|
+
this.name = "WinterSDKError";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
class CLIConnectionError extends WinterSDKError {
|
|
173
|
+
constructor(message) {
|
|
174
|
+
super(message);
|
|
175
|
+
this.name = "CLIConnectionError";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class ProcessError extends WinterSDKError {
|
|
180
|
+
code;
|
|
181
|
+
signal;
|
|
182
|
+
constructor(message, code, signal) {
|
|
183
|
+
super(message);
|
|
184
|
+
this.code = code;
|
|
185
|
+
this.signal = signal;
|
|
186
|
+
this.name = "ProcessError";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
class ResultError extends ProcessError {
|
|
191
|
+
result;
|
|
192
|
+
constructor(result) {
|
|
193
|
+
super(result.terminal_reason === "api_error" ? `provider request failed: ${result.result ?? "unknown provider error"}` : `result error: ${result.subtype}`);
|
|
194
|
+
this.result = result;
|
|
195
|
+
this.name = "ResultError";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
class ProtocolDecodeError extends WinterSDKError {
|
|
200
|
+
constructor(message) {
|
|
201
|
+
super(message);
|
|
202
|
+
this.name = "ProtocolDecodeError";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
class AbortError extends WinterSDKError {
|
|
207
|
+
constructor(message) {
|
|
208
|
+
super(message);
|
|
209
|
+
this.name = "AbortError";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
class SessionNotFoundError extends WinterSDKError {
|
|
214
|
+
reason;
|
|
215
|
+
constructor(reason, message) {
|
|
216
|
+
super(message);
|
|
217
|
+
this.name = "SessionNotFoundError";
|
|
218
|
+
this.reason = reason;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
class InvalidBrandError extends WinterSDKError {
|
|
223
|
+
reason;
|
|
224
|
+
code = "invalid_brand";
|
|
225
|
+
constructor(reason) {
|
|
226
|
+
super(`invalid_brand: ${reason}`);
|
|
227
|
+
this.reason = reason;
|
|
228
|
+
this.name = "InvalidBrandError";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
class WinterRpcError extends WinterSDKError {
|
|
233
|
+
code;
|
|
234
|
+
constructor(code, message) {
|
|
235
|
+
super(message);
|
|
236
|
+
this.code = code;
|
|
237
|
+
this.name = "WinterRpcError";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
class WinterRpcTimeoutError extends WinterRpcError {
|
|
242
|
+
constructor(subtype, timeoutMs) {
|
|
243
|
+
super("timeout", `control request '${subtype}' timed out after ${timeoutMs}ms`);
|
|
244
|
+
this.name = "WinterRpcTimeoutError";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/transport.ts
|
|
249
|
+
var PLATFORM_PACKAGE = "@yanlinglabs/winter-agent-sdk-darwin-arm64";
|
|
250
|
+
function resolveRuntimeExecutable(opts) {
|
|
251
|
+
if (opts.pathToClaudeCodeExecutable)
|
|
252
|
+
return opts.pathToClaudeCodeExecutable;
|
|
253
|
+
const require2 = createRequire(import.meta.url);
|
|
254
|
+
let pkgJsonPath;
|
|
255
|
+
try {
|
|
256
|
+
pkgJsonPath = require2.resolve(`${PLATFORM_PACKAGE}/package.json`);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
throw new WinterSDKError(`runtime executable not found — no pathToClaudeCodeExecutable configured and the platform package ${PLATFORM_PACKAGE} is not installed for this platform (${err.message})`);
|
|
259
|
+
}
|
|
260
|
+
const pkg = require2(pkgJsonPath);
|
|
261
|
+
const binField = pkg.bin;
|
|
262
|
+
const relativeBin = typeof binField === "string" ? binField : binField?.winter;
|
|
263
|
+
if (!relativeBin) {
|
|
264
|
+
throw new WinterSDKError(`runtime executable not found — platform package ${PLATFORM_PACKAGE} has no "bin" entry configured yet`);
|
|
265
|
+
}
|
|
266
|
+
return join(dirname(pkgJsonPath), relativeBin);
|
|
267
|
+
}
|
|
268
|
+
function textChunks(stream) {
|
|
269
|
+
stream.setEncoding("utf8");
|
|
270
|
+
return async function* () {
|
|
271
|
+
try {
|
|
272
|
+
for await (const chunk of stream)
|
|
273
|
+
yield chunk;
|
|
274
|
+
} catch {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
}();
|
|
278
|
+
}
|
|
279
|
+
function defaultSpawn(opts) {
|
|
280
|
+
const child = spawn(opts.command, opts.args, {
|
|
281
|
+
cwd: opts.cwd,
|
|
282
|
+
env: opts.env,
|
|
283
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
284
|
+
});
|
|
285
|
+
let exitedSettled = false;
|
|
286
|
+
let resolveExited;
|
|
287
|
+
const exited = new Promise((resolve) => {
|
|
288
|
+
resolveExited = resolve;
|
|
289
|
+
});
|
|
290
|
+
child.on("close", (code, signal) => {
|
|
291
|
+
if (exitedSettled)
|
|
292
|
+
return;
|
|
293
|
+
exitedSettled = true;
|
|
294
|
+
resolveExited({ code, signal });
|
|
295
|
+
});
|
|
296
|
+
child.on("error", () => {
|
|
297
|
+
if (!exitedSettled) {
|
|
298
|
+
exitedSettled = true;
|
|
299
|
+
resolveExited({ code: null, signal: null });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
child.stdin.on("error", () => {});
|
|
303
|
+
child.stdout.on("error", () => {});
|
|
304
|
+
child.stderr.on("error", () => {});
|
|
305
|
+
return {
|
|
306
|
+
stdin: {
|
|
307
|
+
write: (chunk) => {
|
|
308
|
+
child.stdin.write(chunk, "utf8");
|
|
309
|
+
},
|
|
310
|
+
end: () => {
|
|
311
|
+
child.stdin.end();
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
stdout: textChunks(child.stdout),
|
|
315
|
+
stderr: textChunks(child.stderr),
|
|
316
|
+
kill: (signal) => {
|
|
317
|
+
child.kill(signal);
|
|
318
|
+
},
|
|
319
|
+
exited,
|
|
320
|
+
get pid() {
|
|
321
|
+
return child.pid ?? null;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/paths/home.ts
|
|
327
|
+
import { homedir } from "node:os";
|
|
328
|
+
import { join as join2 } from "node:path";
|
|
329
|
+
function isUnset(value) {
|
|
330
|
+
return value === undefined || value.trim() === "";
|
|
331
|
+
}
|
|
332
|
+
function resolveWinterHome(env, brand) {
|
|
333
|
+
const b = brand ?? WINTER_BRAND;
|
|
334
|
+
const e = env ?? process.env;
|
|
335
|
+
const override = e[envName(b, "HOME")];
|
|
336
|
+
if (!isUnset(override))
|
|
337
|
+
return override;
|
|
338
|
+
const profile = e[envName(b, "PROFILE")];
|
|
339
|
+
const dirName = profile !== undefined && profile.trim() === "dev" ? `${b.homeDirName}-dev` : b.homeDirName;
|
|
340
|
+
return join2(homedir(), dirName);
|
|
341
|
+
}
|
|
342
|
+
function resolveKeychainServiceForProfile(brand, env, hostSetKeychainService) {
|
|
343
|
+
if (hostSetKeychainService)
|
|
344
|
+
return brand.keychainService;
|
|
345
|
+
const profile = (env ?? process.env)[envName(brand, "PROFILE")];
|
|
346
|
+
if (profile === undefined || profile.trim() !== "dev")
|
|
347
|
+
return brand.keychainService;
|
|
348
|
+
const suffixed = `${brand.keychainService}.dev`;
|
|
349
|
+
return suffixed.length <= 64 ? suffixed : brand.keychainService;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// src/query.ts
|
|
353
|
+
var DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024;
|
|
354
|
+
var KILL_GRACE_MS = 50;
|
|
355
|
+
function hookIdFor(event, source, groupIndex, hookIndex) {
|
|
356
|
+
return `${event}:${source}:${groupIndex}:${hookIndex}`;
|
|
357
|
+
}
|
|
358
|
+
function toWireMcpServers(servers) {
|
|
359
|
+
if (!servers)
|
|
360
|
+
return;
|
|
361
|
+
const out = {};
|
|
362
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
363
|
+
out[name] = cfg.type === "sdk" ? {
|
|
364
|
+
type: "sdk",
|
|
365
|
+
name: cfg.name,
|
|
366
|
+
...cfg.timeout !== undefined ? { timeout: cfg.timeout } : {},
|
|
367
|
+
...isWinterMcpServerInstance(cfg.instance) ? { tools: cfg.instance.listTools() } : {}
|
|
368
|
+
} : cfg;
|
|
369
|
+
}
|
|
370
|
+
return out;
|
|
371
|
+
}
|
|
372
|
+
function makeSdkMcpCallHandler(mcpServers) {
|
|
373
|
+
return async (payload) => {
|
|
374
|
+
const req = payload;
|
|
375
|
+
const cfg = req.server !== undefined ? mcpServers[req.server] : undefined;
|
|
376
|
+
if (!cfg || cfg.type !== "sdk") {
|
|
377
|
+
return { ok: false, error: { code: "unknown_sdk_server", message: `no in-process SDK server named '${String(req.server)}' is configured` } };
|
|
378
|
+
}
|
|
379
|
+
if (!isWinterMcpServerInstance(cfg.instance)) {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
error: {
|
|
383
|
+
code: "instance_not_callable",
|
|
384
|
+
message: `server '${req.server}'s instance does not implement listTools()/callTool() (WinterMcpServerInstance) -- it is wire-safe but not end-to-end callable`
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
const result = await cfg.instance.callTool(req.tool ?? "", req.arguments ?? {});
|
|
390
|
+
return { ok: true, payload: result };
|
|
391
|
+
} catch (err) {
|
|
392
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
393
|
+
return { ok: false, error: { code: "sdk_tool_threw", message } };
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function makeElicitationHandler(onElicitation, abortController) {
|
|
398
|
+
return async (payload) => {
|
|
399
|
+
const req = payload;
|
|
400
|
+
const controller = new AbortController;
|
|
401
|
+
if (abortController?.signal.aborted)
|
|
402
|
+
controller.abort();
|
|
403
|
+
else
|
|
404
|
+
abortController?.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
405
|
+
let result;
|
|
406
|
+
try {
|
|
407
|
+
result = await onElicitation({
|
|
408
|
+
serverName: req.serverName ?? "",
|
|
409
|
+
message: req.message ?? "",
|
|
410
|
+
...req.mode !== undefined ? { mode: req.mode } : {},
|
|
411
|
+
...req.url !== undefined ? { url: req.url } : {},
|
|
412
|
+
...req.elicitationId !== undefined ? { elicitationId: req.elicitationId } : {},
|
|
413
|
+
...req.requestedSchema !== undefined ? { requestedSchema: req.requestedSchema } : {},
|
|
414
|
+
...req.title !== undefined ? { title: req.title } : {},
|
|
415
|
+
...req.displayName !== undefined ? { displayName: req.displayName } : {},
|
|
416
|
+
...req.description !== undefined ? { description: req.description } : {}
|
|
417
|
+
}, { signal: controller.signal, requestId: randomUUID() });
|
|
418
|
+
} catch (err) {
|
|
419
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
420
|
+
console.error(`winter: onElicitation callback threw for server '${req.serverName}': ${message} -- declining deterministically`);
|
|
421
|
+
return { ok: true, payload: { action: "decline" } };
|
|
422
|
+
}
|
|
423
|
+
return { ok: true, payload: result ?? { action: "decline" } };
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function buildRuntimeHooksConfig(hooks) {
|
|
427
|
+
if (!hooks)
|
|
428
|
+
return;
|
|
429
|
+
const out = {};
|
|
430
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
431
|
+
if (!groups || groups.length === 0)
|
|
432
|
+
continue;
|
|
433
|
+
out[event] = groups.map((group) => {
|
|
434
|
+
const hookNames = group.hooks.map((hook) => hook.name || null);
|
|
435
|
+
return {
|
|
436
|
+
...group.matcher !== undefined ? { matcher: group.matcher } : {},
|
|
437
|
+
hookCount: group.hooks.length,
|
|
438
|
+
...group.timeout !== undefined ? { timeoutSec: group.timeout } : {},
|
|
439
|
+
source: "sdk",
|
|
440
|
+
...hookNames.some((n) => n !== null) ? { hookNames } : {}
|
|
441
|
+
};
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
445
|
+
}
|
|
446
|
+
function buildHookInput(req, cwd) {
|
|
447
|
+
const payload = req.payload ?? {};
|
|
448
|
+
return {
|
|
449
|
+
session_id: req.sessionId,
|
|
450
|
+
transcript_path: "",
|
|
451
|
+
cwd,
|
|
452
|
+
...req.agentID !== undefined ? { agent_id: req.agentID } : {},
|
|
453
|
+
hook_event_name: req.event,
|
|
454
|
+
...req.toolName !== undefined ? { tool_name: req.toolName } : {},
|
|
455
|
+
...req.input !== undefined ? { tool_input: req.input } : {},
|
|
456
|
+
...req.toolUseID !== undefined ? { tool_use_id: req.toolUseID } : {},
|
|
457
|
+
...payload
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function makeHookHandler(hooks, cwd, abortController) {
|
|
461
|
+
const byId = new Map;
|
|
462
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
463
|
+
(groups ?? []).forEach((group, groupIndex) => {
|
|
464
|
+
group.hooks.forEach((hook, hookIndex) => {
|
|
465
|
+
byId.set(hookIdFor(event, "sdk", groupIndex, hookIndex), hook);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
return async (payload) => {
|
|
470
|
+
const req = payload;
|
|
471
|
+
const callback = byId.get(req.hookId);
|
|
472
|
+
if (!callback) {
|
|
473
|
+
return { ok: false, error: { code: "unknown_hook_id", message: `no SDK-callback hook registered for hookId '${req.hookId}'` } };
|
|
474
|
+
}
|
|
475
|
+
const controller = new AbortController;
|
|
476
|
+
if (abortController?.signal.aborted)
|
|
477
|
+
controller.abort();
|
|
478
|
+
else
|
|
479
|
+
abortController?.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
480
|
+
const input = buildHookInput(req, cwd);
|
|
481
|
+
let output;
|
|
482
|
+
try {
|
|
483
|
+
output = await callback(input, req.toolUseID, { signal: controller.signal });
|
|
484
|
+
} catch (err) {
|
|
485
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
486
|
+
console.error(`winter: hook callback threw for '${req.event}' (hookId '${req.hookId}'): ${message}`);
|
|
487
|
+
return { ok: false, error: { code: "hook_threw", message } };
|
|
488
|
+
}
|
|
489
|
+
return { ok: true, payload: output };
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
function isModelFamilyListing(payload) {
|
|
493
|
+
return typeof payload === "object" && payload !== null && Array.isArray(payload.families);
|
|
494
|
+
}
|
|
495
|
+
function query(args) {
|
|
496
|
+
const { prompt, options } = args;
|
|
497
|
+
if (options.canUseTool) {
|
|
498
|
+
const isBareOrBareEquivalent = (rule) => !rule.includes("(") || /\(\*\)$/.test(rule);
|
|
499
|
+
const hasBareAllowedTool = options.allowedTools?.some(isBareOrBareEquivalent) ?? false;
|
|
500
|
+
if (options.permissionMode === "bypassPermissions" || hasBareAllowedTool) {
|
|
501
|
+
const cause = options.permissionMode === "bypassPermissions" ? "permissionMode is 'bypassPermissions'" : "an allowedTools entry is bare (unscoped)";
|
|
502
|
+
console.error(`winter: WINTER_SDK_CAN_USE_TOOL_SHADOWED: canUseTool is configured but ${cause} — some or all tool calls will never reach it`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (options.sessionStore !== undefined && options.persistSession === false) {
|
|
506
|
+
const homeVar = envName({ envPrefix: options.brand?.envPrefix ?? WINTER_BRAND.envPrefix }, "HOME");
|
|
507
|
+
throw new Error(`sessionStore cannot be used with persistSession: false -- the storage adapter requires local writes to mirror from. Use ${homeVar}=/tmp for ephemeral local writes with external mirroring.`);
|
|
508
|
+
}
|
|
509
|
+
if (options.sessionStore !== undefined && options.enableFileCheckpointing === true) {
|
|
510
|
+
throw new Error("enableFileCheckpointing is not yet supported with sessionStore (backup blobs are not mirrored, so rewindFiles() fails after a store-backed resume).");
|
|
511
|
+
}
|
|
512
|
+
if (options.keychainService !== undefined && options.brand?.keychainService !== undefined && options.brand.keychainService !== options.keychainService) {
|
|
513
|
+
console.error(`winter: both 'keychainService' (${options.keychainService}) and 'brand.keychainService' (${options.brand.keychainService}) are set and differ — ` + `the deprecated 'keychainService' option wins. Set only 'brand.keychainService'.`);
|
|
514
|
+
}
|
|
515
|
+
const brandResolution = resolveBrand({
|
|
516
|
+
...options.brand,
|
|
517
|
+
...options.keychainService !== undefined ? { keychainService: options.keychainService } : {}
|
|
518
|
+
});
|
|
519
|
+
if (!brandResolution.ok)
|
|
520
|
+
throw new InvalidBrandError(brandResolution.reason);
|
|
521
|
+
const hostSetKeychainService = options.brand?.keychainService !== undefined || options.keychainService !== undefined;
|
|
522
|
+
const brand = {
|
|
523
|
+
...brandResolution.brand,
|
|
524
|
+
keychainService: resolveKeychainServiceForProfile(brandResolution.brand, options.env, hostSetKeychainService)
|
|
525
|
+
};
|
|
526
|
+
const runtimeHooksConfig = buildRuntimeHooksConfig(options.hooks);
|
|
527
|
+
const wireMcpServers = toWireMcpServers(options.mcpServers);
|
|
528
|
+
const config = {
|
|
529
|
+
sessionId: options.sessionId ?? randomUUID(),
|
|
530
|
+
cwd: options.cwd ?? process.cwd(),
|
|
531
|
+
model: options.model ?? "sonnet",
|
|
532
|
+
permissionMode: options.permissionMode ?? "default",
|
|
533
|
+
...options.maxTurns !== undefined ? { maxTurns: options.maxTurns } : {},
|
|
534
|
+
...options.resume !== undefined ? { resume: options.resume } : {},
|
|
535
|
+
...options.continue !== undefined ? { continue: options.continue } : {},
|
|
536
|
+
...options.forkSession !== undefined ? { forkSession: options.forkSession } : {},
|
|
537
|
+
...options.resumeSessionAt !== undefined ? { resumeSessionAt: options.resumeSessionAt } : {},
|
|
538
|
+
...options.resumeDropsTurn !== undefined ? { resumeDropsTurn: options.resumeDropsTurn } : {},
|
|
539
|
+
...options.persistSession !== undefined ? { persistSession: options.persistSession } : {},
|
|
540
|
+
...options.allowedTools !== undefined ? { allowedTools: options.allowedTools } : {},
|
|
541
|
+
...options.disallowedTools !== undefined ? { disallowedTools: options.disallowedTools } : {},
|
|
542
|
+
...options.permissions !== undefined ? { permissions: options.permissions } : {},
|
|
543
|
+
...options.settingSources !== undefined ? { settingSources: options.settingSources } : {},
|
|
544
|
+
...options.managedSettings !== undefined ? { managedSettings: options.managedSettings } : {},
|
|
545
|
+
...options.serverManagedSettings !== undefined ? { serverManagedSettings: options.serverManagedSettings } : {},
|
|
546
|
+
...options.permissionPromptToolName !== undefined ? { permissionPromptToolName: options.permissionPromptToolName } : {},
|
|
547
|
+
...options.additionalDirectories !== undefined ? { additionalDirectories: options.additionalDirectories } : {},
|
|
548
|
+
...options.sandbox !== undefined ? { sandbox: options.sandbox } : {},
|
|
549
|
+
...options.outputsDir !== undefined ? { outputsDir: options.outputsDir } : {},
|
|
550
|
+
...options.capabilities !== undefined ? { capabilities: options.capabilities } : {},
|
|
551
|
+
...options.toolSearchEnabled !== undefined ? { toolSearchEnabled: options.toolSearchEnabled } : {},
|
|
552
|
+
...options.insideSubagent !== undefined ? { insideSubagent: options.insideSubagent } : {},
|
|
553
|
+
...options.familyMetadata !== undefined ? { familyMetadata: options.familyMetadata } : {},
|
|
554
|
+
...options.allowDangerouslySkipPermissions !== undefined ? { allowDangerouslySkipPermissions: options.allowDangerouslySkipPermissions } : {},
|
|
555
|
+
...runtimeHooksConfig !== undefined ? { hooks: runtimeHooksConfig } : {},
|
|
556
|
+
...options.includeHookEvents !== undefined ? { includeHookEvents: options.includeHookEvents } : {},
|
|
557
|
+
...wireMcpServers !== undefined ? { mcpServers: wireMcpServers } : {},
|
|
558
|
+
...options.strictMcpConfig !== undefined ? { strictMcpConfig: options.strictMcpConfig } : {},
|
|
559
|
+
...options.toolAliases !== undefined ? { toolAliases: options.toolAliases } : {},
|
|
560
|
+
...options.agents !== undefined ? { agents: options.agents } : {},
|
|
561
|
+
...options.forwardSubagentText !== undefined ? { forwardSubagentText: options.forwardSubagentText } : {},
|
|
562
|
+
...options.systemPrompt !== undefined ? { systemPrompt: options.systemPrompt } : {},
|
|
563
|
+
...options.plugins !== undefined ? { plugins: options.plugins } : {},
|
|
564
|
+
...options.skills !== undefined ? { skills: options.skills } : {},
|
|
565
|
+
...options.outputFormat !== undefined ? { outputFormat: options.outputFormat } : {},
|
|
566
|
+
...options.enableFileCheckpointing !== undefined ? { enableFileCheckpointing: options.enableFileCheckpointing } : {},
|
|
567
|
+
...options.contextWindowTokens !== undefined ? { contextWindowTokens: options.contextWindowTokens } : {},
|
|
568
|
+
...options.compactionThreshold !== undefined ? { compactionThreshold: options.compactionThreshold } : {},
|
|
569
|
+
...options.trustedWorkspace !== undefined ? { trustedWorkspace: options.trustedWorkspace } : {},
|
|
570
|
+
...options.plansDirectory !== undefined ? { plansDirectory: options.plansDirectory } : {},
|
|
571
|
+
...options.outputStyle !== undefined ? { outputStyle: options.outputStyle } : {},
|
|
572
|
+
...options.provider !== undefined ? { provider: options.provider } : {},
|
|
573
|
+
...options.fallbackModel !== undefined ? { fallbackModel: options.fallbackModel } : {},
|
|
574
|
+
...options.thinking !== undefined ? { thinking: options.thinking } : {},
|
|
575
|
+
...options.effort !== undefined ? { effort: options.effort } : {},
|
|
576
|
+
...options.maxThinkingTokens !== undefined ? { maxThinkingTokens: options.maxThinkingTokens } : {},
|
|
577
|
+
...options.includePartialMessages !== undefined ? { includePartialMessages: options.includePartialMessages } : {},
|
|
578
|
+
...options.maxBudgetUsd !== undefined ? { maxBudgetUsd: options.maxBudgetUsd } : {},
|
|
579
|
+
...options.providerStallTimeoutMs !== undefined ? { providerStallTimeoutMs: options.providerStallTimeoutMs } : {},
|
|
580
|
+
...brand.keychainService !== WINTER_BRAND.keychainService || options.keychainService !== undefined ? { keychainService: brand.keychainService } : {},
|
|
581
|
+
...options.autoClassifier !== undefined ? { autoClassifier: options.autoClassifier } : {},
|
|
582
|
+
...options.advisor !== undefined ? { advisor: options.advisor } : {},
|
|
583
|
+
brand
|
|
584
|
+
};
|
|
585
|
+
const command = options.spawnClaudeCodeProcess ? options.pathToClaudeCodeExecutable ?? "winter" : resolveRuntimeExecutable(options);
|
|
586
|
+
const spawnOptions = {
|
|
587
|
+
command,
|
|
588
|
+
args: ["--run", "--config-json", JSON.stringify(config)],
|
|
589
|
+
cwd: config.cwd,
|
|
590
|
+
env: options.env ?? process.env,
|
|
591
|
+
...options.abortController ? { signal: options.abortController.signal } : {}
|
|
592
|
+
};
|
|
593
|
+
const proc = (options.spawnClaudeCodeProcess ?? defaultSpawn)(spawnOptions);
|
|
594
|
+
const maxBufferSize = options.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
|
|
595
|
+
const pendingHostRequests = new Map;
|
|
596
|
+
let generatorTerminated = false;
|
|
597
|
+
function sendControlRequest(subtype, payload) {
|
|
598
|
+
if (generatorTerminated) {
|
|
599
|
+
return Promise.reject(new WinterRpcError("connection_closed", `query() has already completed: cannot issue a '${subtype}' control request`));
|
|
600
|
+
}
|
|
601
|
+
const requestId = randomUUID();
|
|
602
|
+
return new Promise((resolve, reject) => {
|
|
603
|
+
pendingHostRequests.set(requestId, { resolve, reject });
|
|
604
|
+
proc.stdin.write(encodeFrame({ type: "control_request", requestId, subtype, payload }));
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
const respondedOutOfBand = new Set;
|
|
608
|
+
function writeControlResponse(frame) {
|
|
609
|
+
if (respondedOutOfBand.delete(frame.requestId))
|
|
610
|
+
return;
|
|
611
|
+
proc.stdin.write(encodeFrame(frame));
|
|
612
|
+
}
|
|
613
|
+
const controlRequestHandlers = new Map;
|
|
614
|
+
async function handleIncomingControlRequest(cf) {
|
|
615
|
+
try {
|
|
616
|
+
const handler = controlRequestHandlers.get(cf.subtype);
|
|
617
|
+
if (!handler) {
|
|
618
|
+
writeControlResponse({
|
|
619
|
+
type: "control_response",
|
|
620
|
+
requestId: cf.requestId,
|
|
621
|
+
ok: false,
|
|
622
|
+
error: { code: "unhandled_subtype", message: `no handler registered for control subtype '${cf.subtype}'` }
|
|
623
|
+
});
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
const result = await handler(cf.payload);
|
|
628
|
+
if (result.ok) {
|
|
629
|
+
writeControlResponse({
|
|
630
|
+
type: "control_response",
|
|
631
|
+
requestId: cf.requestId,
|
|
632
|
+
ok: true,
|
|
633
|
+
...result.payload !== undefined ? { payload: result.payload } : {}
|
|
634
|
+
});
|
|
635
|
+
} else {
|
|
636
|
+
writeControlResponse({ type: "control_response", requestId: cf.requestId, ok: false, error: result.error });
|
|
637
|
+
}
|
|
638
|
+
} catch (err) {
|
|
639
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
640
|
+
writeControlResponse({ type: "control_response", requestId: cf.requestId, ok: false, error: { code: "handler_threw", message } });
|
|
641
|
+
}
|
|
642
|
+
} catch {}
|
|
643
|
+
}
|
|
644
|
+
function makePermissionHandler(canUseTool) {
|
|
645
|
+
return async (payload) => {
|
|
646
|
+
const req = payload;
|
|
647
|
+
const controller = new AbortController;
|
|
648
|
+
if (options.abortController?.signal.aborted)
|
|
649
|
+
controller.abort();
|
|
650
|
+
else
|
|
651
|
+
options.abortController?.signal.addEventListener("abort", () => controller.abort(), { once: true });
|
|
652
|
+
let result;
|
|
653
|
+
try {
|
|
654
|
+
result = await canUseTool(req.toolName, req.input, {
|
|
655
|
+
signal: controller.signal,
|
|
656
|
+
...req.suggestions !== undefined ? { suggestions: req.suggestions } : {},
|
|
657
|
+
...req.blockedPath !== undefined ? { blockedPath: req.blockedPath } : {},
|
|
658
|
+
...req.decisionReason !== undefined ? { decisionReason: req.decisionReason } : {},
|
|
659
|
+
...req.title !== undefined ? { title: req.title } : {},
|
|
660
|
+
...req.displayName !== undefined ? { displayName: req.displayName } : {},
|
|
661
|
+
...req.description !== undefined ? { description: req.description } : {},
|
|
662
|
+
toolUseID: req.toolUseID,
|
|
663
|
+
...req.agentID !== undefined ? { agentID: req.agentID } : {},
|
|
664
|
+
requestId: req.requestId,
|
|
665
|
+
...req.matchedAskRule !== undefined ? { matchedAskRule: req.matchedAskRule } : {}
|
|
666
|
+
});
|
|
667
|
+
} catch (err) {
|
|
668
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
669
|
+
console.error(`winter: canUseTool callback threw for '${req.toolName}': ${message} — failing closed (deny)`);
|
|
670
|
+
const deny = { behavior: "deny", message: `canUseTool callback threw: ${message}` };
|
|
671
|
+
return { ok: true, payload: deny };
|
|
672
|
+
}
|
|
673
|
+
if (result === null) {
|
|
674
|
+
if (respondedOutOfBand.has(req.requestId)) {
|
|
675
|
+
return { ok: true };
|
|
676
|
+
}
|
|
677
|
+
console.error(`winter: canUseTool callback for '${req.toolName}' returned null with no prior out-of-band response — failing closed (deny)`);
|
|
678
|
+
const deny = {
|
|
679
|
+
behavior: "deny",
|
|
680
|
+
message: "canUseTool callback returned null with no prior out-of-band response (accidental null fails closed, WS-07 §7.2)"
|
|
681
|
+
};
|
|
682
|
+
return { ok: true, payload: deny };
|
|
683
|
+
}
|
|
684
|
+
return { ok: true, payload: result };
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
if (options.canUseTool) {
|
|
688
|
+
controlRequestHandlers.set("permission", makePermissionHandler(options.canUseTool));
|
|
689
|
+
}
|
|
690
|
+
if (options.hooks && Object.keys(options.hooks).length > 0) {
|
|
691
|
+
controlRequestHandlers.set("hook", makeHookHandler(options.hooks, config.cwd, options.abortController));
|
|
692
|
+
}
|
|
693
|
+
if (options.mcpServers && Object.values(options.mcpServers).some((cfg) => cfg.type === "sdk")) {
|
|
694
|
+
controlRequestHandlers.set("sdk_mcp_call", makeSdkMcpCallHandler(options.mcpServers));
|
|
695
|
+
}
|
|
696
|
+
if (options.onElicitation) {
|
|
697
|
+
controlRequestHandlers.set("mcp_elicitation", makeElicitationHandler(options.onElicitation, options.abortController));
|
|
698
|
+
}
|
|
699
|
+
if (proc.stderr) {
|
|
700
|
+
const stderrIterable = proc.stderr;
|
|
701
|
+
(async () => {
|
|
702
|
+
try {
|
|
703
|
+
for await (const chunk of stderrIterable)
|
|
704
|
+
options.stderr?.(chunk);
|
|
705
|
+
} catch {}
|
|
706
|
+
})();
|
|
707
|
+
}
|
|
708
|
+
async function* iterate() {
|
|
709
|
+
let aborted = false;
|
|
710
|
+
let killTimer;
|
|
711
|
+
const onAbort = () => {
|
|
712
|
+
if (aborted)
|
|
713
|
+
return;
|
|
714
|
+
aborted = true;
|
|
715
|
+
proc.kill();
|
|
716
|
+
killTimer = setTimeout(() => proc.kill("SIGKILL"), KILL_GRACE_MS);
|
|
717
|
+
killTimer.unref?.();
|
|
718
|
+
};
|
|
719
|
+
options.abortController?.signal.addEventListener("abort", onAbort);
|
|
720
|
+
try {
|
|
721
|
+
if (options.abortController?.signal.aborted)
|
|
722
|
+
onAbort();
|
|
723
|
+
(async () => {
|
|
724
|
+
try {
|
|
725
|
+
if (typeof prompt === "string") {
|
|
726
|
+
proc.stdin.write(encodeFrame({ type: "user", text: prompt }));
|
|
727
|
+
} else {
|
|
728
|
+
for await (const text of prompt) {
|
|
729
|
+
proc.stdin.write(encodeFrame({ type: "user", text }));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
sendControlRequest("end_input", undefined).catch(() => {});
|
|
733
|
+
} catch {}
|
|
734
|
+
})();
|
|
735
|
+
let sawInit = false;
|
|
736
|
+
let sawTerminal = false;
|
|
737
|
+
let terminalError = null;
|
|
738
|
+
let carry = "";
|
|
739
|
+
const isStreamingInput = typeof prompt !== "string";
|
|
740
|
+
readLoop:
|
|
741
|
+
for await (const chunk of proc.stdout) {
|
|
742
|
+
let frames;
|
|
743
|
+
try {
|
|
744
|
+
const split = splitFrames(chunk, carry);
|
|
745
|
+
frames = split.frames;
|
|
746
|
+
carry = split.carry;
|
|
747
|
+
} catch (e) {
|
|
748
|
+
throw new ProtocolDecodeError(e instanceof ProtocolError ? e.message : String(e));
|
|
749
|
+
}
|
|
750
|
+
for (const frame of frames) {
|
|
751
|
+
if (!sawInit) {
|
|
752
|
+
if (frame.type !== "init") {
|
|
753
|
+
throw new ProtocolDecodeError(`protocol violation: expected 'init' as the first frame, got '${frame.type}'`);
|
|
754
|
+
}
|
|
755
|
+
const init = frame;
|
|
756
|
+
const runtimeMajor = init.protocolVersion.split(".")[0];
|
|
757
|
+
const sdkMajor = PROTOCOL_VERSION.split(".")[0];
|
|
758
|
+
if (runtimeMajor !== sdkMajor) {
|
|
759
|
+
throw new CLIConnectionError(`protocol version mismatch: runtime speaks ${init.protocolVersion}, sdk expects ${PROTOCOL_VERSION}`);
|
|
760
|
+
}
|
|
761
|
+
sawInit = true;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (frame.type === "control_response") {
|
|
765
|
+
const cf = frame;
|
|
766
|
+
const pendingReq = pendingHostRequests.get(cf.requestId);
|
|
767
|
+
if (pendingReq) {
|
|
768
|
+
pendingHostRequests.delete(cf.requestId);
|
|
769
|
+
if (cf.ok)
|
|
770
|
+
pendingReq.resolve(cf.payload);
|
|
771
|
+
else
|
|
772
|
+
pendingReq.reject(new WinterRpcError(cf.error?.code ?? "unknown_error", cf.error?.message ?? "control request failed"));
|
|
773
|
+
}
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (frame.type === "control_request") {
|
|
777
|
+
handleIncomingControlRequest(frame);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
if (frame.type !== "data")
|
|
781
|
+
continue;
|
|
782
|
+
const message = frame.message;
|
|
783
|
+
yield message;
|
|
784
|
+
if (message.type === "result") {
|
|
785
|
+
sawTerminal = true;
|
|
786
|
+
if (message.is_error)
|
|
787
|
+
terminalError = message;
|
|
788
|
+
if (!isStreamingInput)
|
|
789
|
+
break readLoop;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (carry.length > maxBufferSize) {
|
|
793
|
+
throw new ProtocolDecodeError(`protocol line exceeds maxBufferSize (${maxBufferSize} bytes)`);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (terminalError)
|
|
797
|
+
throw new ResultError(terminalError);
|
|
798
|
+
if (sawTerminal)
|
|
799
|
+
return;
|
|
800
|
+
if (aborted)
|
|
801
|
+
throw new AbortError("query aborted: runtime process killed");
|
|
802
|
+
if (!sawInit)
|
|
803
|
+
throw new CLIConnectionError("runtime exited before init");
|
|
804
|
+
const exitInfo = await proc.exited;
|
|
805
|
+
throw new ProcessError("unexpected process death: runtime exited without a terminal result", exitInfo.code, exitInfo.signal);
|
|
806
|
+
} finally {
|
|
807
|
+
generatorTerminated = true;
|
|
808
|
+
options.abortController?.signal.removeEventListener("abort", onAbort);
|
|
809
|
+
if (killTimer)
|
|
810
|
+
clearTimeout(killTimer);
|
|
811
|
+
try {
|
|
812
|
+
proc.stdin.end();
|
|
813
|
+
} catch {}
|
|
814
|
+
if (pendingHostRequests.size > 0) {
|
|
815
|
+
const err = new WinterRpcError("connection_closed", "runtime connection closed before this control request was acknowledged");
|
|
816
|
+
for (const pendingReq of pendingHostRequests.values())
|
|
817
|
+
pendingReq.reject(err);
|
|
818
|
+
pendingHostRequests.clear();
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
const gen = iterate();
|
|
823
|
+
gen.interrupt = async () => {
|
|
824
|
+
await sendControlRequest("interrupt", { scope: "turn" });
|
|
825
|
+
};
|
|
826
|
+
gen.setModel = async (model) => {
|
|
827
|
+
await sendControlRequest("set_model", model !== undefined ? { model } : {});
|
|
828
|
+
};
|
|
829
|
+
gen.supportedModels = async () => {
|
|
830
|
+
const payload = await sendControlRequest("list_models", undefined);
|
|
831
|
+
return Array.isArray(payload) ? payload : [];
|
|
832
|
+
};
|
|
833
|
+
gen.listModelFamilies = async () => {
|
|
834
|
+
const payload = await sendControlRequest("list_model_families", undefined);
|
|
835
|
+
return isModelFamilyListing(payload) ? payload : { active: undefined, families: [] };
|
|
836
|
+
};
|
|
837
|
+
gen.accountInfo = async () => {
|
|
838
|
+
const payload = await sendControlRequest("account_info", undefined);
|
|
839
|
+
return typeof payload === "object" && payload !== null && !Array.isArray(payload) ? payload : {};
|
|
840
|
+
};
|
|
841
|
+
gen.setPermissionMode = async (mode) => {
|
|
842
|
+
await sendControlRequest("set_permission_mode", mode);
|
|
843
|
+
};
|
|
844
|
+
gen.rewindFiles = async (userMessageId, options) => {
|
|
845
|
+
const payload = await sendControlRequest("rewind_files", { user_message_id: userMessageId, ...options?.dryRun !== undefined ? { dry_run: options.dryRun } : {} });
|
|
846
|
+
if (typeof payload !== "object" || payload === null || typeof payload.canRewind !== "boolean") {
|
|
847
|
+
return { canRewind: false, error: "the runtime returned no rewind result" };
|
|
848
|
+
}
|
|
849
|
+
return payload;
|
|
850
|
+
};
|
|
851
|
+
gen.__internal = {
|
|
852
|
+
registerControlRequestHandler(subtype, handler) {
|
|
853
|
+
controlRequestHandlers.set(subtype, handler);
|
|
854
|
+
},
|
|
855
|
+
respondPermission(requestId, result) {
|
|
856
|
+
respondedOutOfBand.add(requestId);
|
|
857
|
+
try {
|
|
858
|
+
proc.stdin.write(encodeFrame({ type: "control_response", requestId, ok: true, payload: result }));
|
|
859
|
+
} catch {}
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
return gen;
|
|
863
|
+
}
|
|
864
|
+
// src/paths/project-key.ts
|
|
865
|
+
var MAX_UNSUFFIXED_LENGTH = 200;
|
|
866
|
+
function sanitize(absPath) {
|
|
867
|
+
return absPath.replace(/[^a-zA-Z0-9]/g, "-");
|
|
868
|
+
}
|
|
869
|
+
function rollingHash32(raw) {
|
|
870
|
+
let hash = 0;
|
|
871
|
+
for (let i = 0;i < raw.length; i++) {
|
|
872
|
+
hash = (hash << 5) - hash + raw.charCodeAt(i) | 0;
|
|
873
|
+
}
|
|
874
|
+
return hash;
|
|
875
|
+
}
|
|
876
|
+
function overflowSuffix(rawAbsPath) {
|
|
877
|
+
return Math.abs(rollingHash32(rawAbsPath)).toString(36);
|
|
878
|
+
}
|
|
879
|
+
function transcriptProjectKey(absPath) {
|
|
880
|
+
const sanitized = sanitize(absPath);
|
|
881
|
+
if (sanitized.length <= MAX_UNSUFFIXED_LENGTH)
|
|
882
|
+
return sanitized;
|
|
883
|
+
return `${sanitized.slice(0, MAX_UNSUFFIXED_LENGTH)}-${overflowSuffix(absPath)}`;
|
|
884
|
+
}
|
|
885
|
+
// src/paths/keys.ts
|
|
886
|
+
import { realpathSync } from "node:fs";
|
|
887
|
+
import { resolve as resolvePath, dirname as dirname2, isAbsolute } from "node:path";
|
|
888
|
+
import { execFileSync } from "node:child_process";
|
|
889
|
+
function platformNormalize(p) {
|
|
890
|
+
return process.platform === "darwin" ? p.normalize("NFC") : p;
|
|
891
|
+
}
|
|
892
|
+
function resolveCanonical(raw) {
|
|
893
|
+
const resolved = resolvePath(raw);
|
|
894
|
+
try {
|
|
895
|
+
return platformNormalize(realpathSync(resolved));
|
|
896
|
+
} catch {
|
|
897
|
+
return platformNormalize(resolved);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
function gitCommonRoot(resolvedCwd) {
|
|
901
|
+
try {
|
|
902
|
+
const raw = execFileSync("git", ["-C", resolvedCwd, "rev-parse", "--git-common-dir"], {
|
|
903
|
+
encoding: "utf8",
|
|
904
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
905
|
+
}).trim();
|
|
906
|
+
const commonDir = isAbsolute(raw) ? raw : resolvePath(resolvedCwd, raw);
|
|
907
|
+
return platformNormalize(dirname2(realpathSync(commonDir)));
|
|
908
|
+
} catch {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function compatibilityKeys(cwd) {
|
|
913
|
+
const resolvedCwd = resolveCanonical(cwd);
|
|
914
|
+
const cwdKey = transcriptProjectKey(resolvedCwd);
|
|
915
|
+
const commonRoot = gitCommonRoot(resolvedCwd);
|
|
916
|
+
const memoryKey = commonRoot === null ? cwdKey : transcriptProjectKey(commonRoot);
|
|
917
|
+
return { transcriptProjectKey: cwdKey, memoryProjectKey: memoryKey, tempProjectKey: cwdKey };
|
|
918
|
+
}
|
|
919
|
+
// src/store/session-store.ts
|
|
920
|
+
import {
|
|
921
|
+
mkdirSync,
|
|
922
|
+
lstatSync,
|
|
923
|
+
chmodSync,
|
|
924
|
+
readdirSync,
|
|
925
|
+
readFileSync as readFileSync2,
|
|
926
|
+
statSync,
|
|
927
|
+
openSync as openSync2,
|
|
928
|
+
fsyncSync as fsyncSync2,
|
|
929
|
+
closeSync as closeSync2,
|
|
930
|
+
renameSync as renameSync2,
|
|
931
|
+
rmSync,
|
|
932
|
+
ftruncateSync,
|
|
933
|
+
constants as fsConstants
|
|
934
|
+
} from "node:fs";
|
|
935
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
936
|
+
import { join as join3 } from "node:path";
|
|
937
|
+
|
|
938
|
+
// src/store/leases.ts
|
|
939
|
+
import { openSync, readFileSync, writeSync, fsyncSync, closeSync, renameSync, linkSync, unlinkSync } from "node:fs";
|
|
940
|
+
|
|
941
|
+
class WinterStoreError extends Error {
|
|
942
|
+
constructor(message) {
|
|
943
|
+
super(message);
|
|
944
|
+
this.name = "WinterStoreError";
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
class WinterStoreLeaseError extends WinterStoreError {
|
|
949
|
+
heldByPid;
|
|
950
|
+
constructor(message, heldByPid) {
|
|
951
|
+
super(message);
|
|
952
|
+
this.heldByPid = heldByPid;
|
|
953
|
+
this.name = "WinterStoreLeaseError";
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function isPidAlive(pid) {
|
|
957
|
+
try {
|
|
958
|
+
process.kill(pid, 0);
|
|
959
|
+
return true;
|
|
960
|
+
} catch (err) {
|
|
961
|
+
const code = err.code;
|
|
962
|
+
if (code === "ESRCH")
|
|
963
|
+
return false;
|
|
964
|
+
if (code === "EPERM")
|
|
965
|
+
return true;
|
|
966
|
+
throw err;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function writeAllSync(fd, buf) {
|
|
970
|
+
let written = 0;
|
|
971
|
+
while (written < buf.length) {
|
|
972
|
+
written += writeSync(fd, buf, written, buf.length - written);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
function readLeaseInfo(lockPath) {
|
|
976
|
+
let raw;
|
|
977
|
+
try {
|
|
978
|
+
raw = readFileSync(lockPath, "utf8");
|
|
979
|
+
} catch (err) {
|
|
980
|
+
if (err.code === "ENOENT")
|
|
981
|
+
return null;
|
|
982
|
+
throw err;
|
|
983
|
+
}
|
|
984
|
+
try {
|
|
985
|
+
const parsed = JSON.parse(raw);
|
|
986
|
+
if (typeof parsed.pid === "number" && typeof parsed.startTimeMs === "number") {
|
|
987
|
+
return { pid: parsed.pid, startTimeMs: parsed.startTimeMs };
|
|
988
|
+
}
|
|
989
|
+
} catch {}
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
function createExclusive(lockPath, info) {
|
|
993
|
+
const data = Buffer.from(JSON.stringify(info), "utf8");
|
|
994
|
+
const tmpPath = `${lockPath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
995
|
+
const fd = openSync(tmpPath, "wx", 384);
|
|
996
|
+
try {
|
|
997
|
+
writeAllSync(fd, data);
|
|
998
|
+
fsyncSync(fd);
|
|
999
|
+
} finally {
|
|
1000
|
+
closeSync(fd);
|
|
1001
|
+
}
|
|
1002
|
+
try {
|
|
1003
|
+
linkSync(tmpPath, lockPath);
|
|
1004
|
+
} finally {
|
|
1005
|
+
try {
|
|
1006
|
+
unlinkSync(tmpPath);
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
if (err.code !== "ENOENT")
|
|
1009
|
+
throw err;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function writeLeaseInfoReplacing(lockPath, info) {
|
|
1014
|
+
const data = Buffer.from(JSON.stringify(info), "utf8");
|
|
1015
|
+
const tmpPath = `${lockPath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1016
|
+
const fd = openSync(tmpPath, "wx", 384);
|
|
1017
|
+
try {
|
|
1018
|
+
writeAllSync(fd, data);
|
|
1019
|
+
fsyncSync(fd);
|
|
1020
|
+
} finally {
|
|
1021
|
+
closeSync(fd);
|
|
1022
|
+
}
|
|
1023
|
+
renameSync(tmpPath, lockPath);
|
|
1024
|
+
}
|
|
1025
|
+
function acquireLease(lockPath) {
|
|
1026
|
+
const fresh = { pid: process.pid, startTimeMs: Date.now() };
|
|
1027
|
+
try {
|
|
1028
|
+
createExclusive(lockPath, fresh);
|
|
1029
|
+
return fresh;
|
|
1030
|
+
} catch (err) {
|
|
1031
|
+
if (err.code !== "EEXIST")
|
|
1032
|
+
throw err;
|
|
1033
|
+
}
|
|
1034
|
+
const existing = readLeaseInfo(lockPath);
|
|
1035
|
+
if (existing !== null && existing.pid === process.pid) {
|
|
1036
|
+
return existing;
|
|
1037
|
+
}
|
|
1038
|
+
if (existing !== null && isPidAlive(existing.pid)) {
|
|
1039
|
+
throw new WinterStoreLeaseError(`session lease is held by another live process (pid ${existing.pid}); refusing a concurrent writer`, existing.pid);
|
|
1040
|
+
}
|
|
1041
|
+
writeLeaseInfoReplacing(lockPath, fresh);
|
|
1042
|
+
return fresh;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// src/store/session-store.ts
|
|
1046
|
+
var DIALECT_RECORD_ENTRY_TYPE = "winter_dialect_record";
|
|
1047
|
+
var PROVIDER_STATE_FILE_SUFFIX = ".provider-state.jsonl";
|
|
1048
|
+
function assertSafeSingleSegment(value, label) {
|
|
1049
|
+
if (value === "")
|
|
1050
|
+
throw new WinterStoreError(`${label} must not be empty`);
|
|
1051
|
+
if (value.includes("/"))
|
|
1052
|
+
throw new WinterStoreError(`${label} must not contain a path separator: ${JSON.stringify(value)}`);
|
|
1053
|
+
if (value === "." || value === "..")
|
|
1054
|
+
throw new WinterStoreError(`${label} must not be a traversal segment: ${JSON.stringify(value)}`);
|
|
1055
|
+
}
|
|
1056
|
+
function assertSafeSubpath(value) {
|
|
1057
|
+
if (value === "")
|
|
1058
|
+
throw new WinterStoreError("subpath must not be empty");
|
|
1059
|
+
if (value.startsWith("/"))
|
|
1060
|
+
throw new WinterStoreError(`subpath must not be absolute: ${JSON.stringify(value)}`);
|
|
1061
|
+
const segments = value.split("/");
|
|
1062
|
+
for (const segment of segments) {
|
|
1063
|
+
if (segment === "") {
|
|
1064
|
+
throw new WinterStoreError(`subpath must not contain empty segments (double/trailing separators): ${JSON.stringify(value)}`);
|
|
1065
|
+
}
|
|
1066
|
+
if (segment === "." || segment === "..") {
|
|
1067
|
+
throw new WinterStoreError(`subpath must not contain a traversal segment: ${JSON.stringify(value)}`);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return segments;
|
|
1071
|
+
}
|
|
1072
|
+
function projectDir(winterHome, projectKey) {
|
|
1073
|
+
return join3(winterHome, "projects", projectKey);
|
|
1074
|
+
}
|
|
1075
|
+
function sessionStem(winterHome, projectKey, sessionId) {
|
|
1076
|
+
assertSafeSingleSegment(projectKey, "projectKey");
|
|
1077
|
+
assertSafeSingleSegment(sessionId, "sessionId");
|
|
1078
|
+
return join3(projectDir(winterHome, projectKey), sessionId);
|
|
1079
|
+
}
|
|
1080
|
+
function locateResource(winterHome, key) {
|
|
1081
|
+
assertSafeSingleSegment(key.projectKey, "projectKey");
|
|
1082
|
+
assertSafeSingleSegment(key.sessionId, "sessionId");
|
|
1083
|
+
const projDir = projectDir(winterHome, key.projectKey);
|
|
1084
|
+
const dirLevels = [winterHome, join3(winterHome, "projects"), projDir];
|
|
1085
|
+
if (key.subpath === undefined) {
|
|
1086
|
+
return { dirLevels, stem: join3(projDir, key.sessionId) };
|
|
1087
|
+
}
|
|
1088
|
+
const segments = assertSafeSubpath(key.subpath);
|
|
1089
|
+
let current = join3(projDir, key.sessionId);
|
|
1090
|
+
dirLevels.push(current);
|
|
1091
|
+
for (let i = 0;i < segments.length - 1; i++) {
|
|
1092
|
+
current = join3(current, segments[i]);
|
|
1093
|
+
dirLevels.push(current);
|
|
1094
|
+
}
|
|
1095
|
+
const stem = join3(current, segments[segments.length - 1]);
|
|
1096
|
+
return { dirLevels, stem };
|
|
1097
|
+
}
|
|
1098
|
+
function realUid() {
|
|
1099
|
+
return process.getuid();
|
|
1100
|
+
}
|
|
1101
|
+
function ensureSecureDir(path) {
|
|
1102
|
+
try {
|
|
1103
|
+
mkdirSync(path, { mode: 448 });
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
if (err.code !== "EEXIST")
|
|
1106
|
+
throw err;
|
|
1107
|
+
}
|
|
1108
|
+
const stat = lstatSync(path);
|
|
1109
|
+
if (stat.isSymbolicLink())
|
|
1110
|
+
throw new WinterStoreError(`refusing a symlink at a level the store must own: ${path}`);
|
|
1111
|
+
if (!stat.isDirectory())
|
|
1112
|
+
throw new WinterStoreError(`expected a directory, found something else at: ${path}`);
|
|
1113
|
+
if (stat.uid !== realUid())
|
|
1114
|
+
throw new WinterStoreError(`refusing a directory owned by a different uid: ${path}`);
|
|
1115
|
+
chmodSync(path, 448);
|
|
1116
|
+
}
|
|
1117
|
+
var APPEND_FLAGS = fsConstants.O_WRONLY | fsConstants.O_APPEND | fsConstants.O_CREAT | fsConstants.O_NOFOLLOW;
|
|
1118
|
+
var RW_EXISTING_FLAGS = fsConstants.O_RDWR | fsConstants.O_NOFOLLOW;
|
|
1119
|
+
function appendLinesAtomically(path, lines) {
|
|
1120
|
+
const data = Buffer.from(lines.map((l) => l + `
|
|
1121
|
+
`).join(""), "utf8");
|
|
1122
|
+
const fd = openSync2(path, APPEND_FLAGS, 384);
|
|
1123
|
+
try {
|
|
1124
|
+
writeAllSync(fd, data);
|
|
1125
|
+
fsyncSync2(fd);
|
|
1126
|
+
} finally {
|
|
1127
|
+
closeSync2(fd);
|
|
1128
|
+
}
|
|
1129
|
+
chmodSync(path, 384);
|
|
1130
|
+
}
|
|
1131
|
+
function quarantineTornTail(jsonlPath, tornRaw) {
|
|
1132
|
+
const quarantinePath = `${jsonlPath}.tail-quarantine`;
|
|
1133
|
+
const fd = openSync2(quarantinePath, APPEND_FLAGS, 384);
|
|
1134
|
+
try {
|
|
1135
|
+
writeAllSync(fd, tornRaw);
|
|
1136
|
+
fsyncSync2(fd);
|
|
1137
|
+
} finally {
|
|
1138
|
+
closeSync2(fd);
|
|
1139
|
+
}
|
|
1140
|
+
chmodSync(quarantinePath, 384);
|
|
1141
|
+
}
|
|
1142
|
+
function repairTruncate(jsonlPath, keepBytes) {
|
|
1143
|
+
const fd = openSync2(jsonlPath, RW_EXISTING_FLAGS);
|
|
1144
|
+
try {
|
|
1145
|
+
ftruncateSync(fd, keepBytes);
|
|
1146
|
+
fsyncSync2(fd);
|
|
1147
|
+
} finally {
|
|
1148
|
+
closeSync2(fd);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function writeJsonAtomically(path, value) {
|
|
1152
|
+
const data = Buffer.from(JSON.stringify(value), "utf8");
|
|
1153
|
+
const tmpPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1154
|
+
const fd = openSync2(tmpPath, "wx", 384);
|
|
1155
|
+
try {
|
|
1156
|
+
writeAllSync(fd, data);
|
|
1157
|
+
fsyncSync2(fd);
|
|
1158
|
+
} finally {
|
|
1159
|
+
closeSync2(fd);
|
|
1160
|
+
}
|
|
1161
|
+
renameSync2(tmpPath, path);
|
|
1162
|
+
chmodSync(path, 384);
|
|
1163
|
+
}
|
|
1164
|
+
function readJsonIfExists(path) {
|
|
1165
|
+
let raw;
|
|
1166
|
+
try {
|
|
1167
|
+
raw = readFileSync2(path, "utf8");
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
if (err.code === "ENOENT")
|
|
1170
|
+
return null;
|
|
1171
|
+
throw err;
|
|
1172
|
+
}
|
|
1173
|
+
try {
|
|
1174
|
+
return JSON.parse(raw);
|
|
1175
|
+
} catch {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function rmIfExists(path) {
|
|
1180
|
+
try {
|
|
1181
|
+
rmSync(path);
|
|
1182
|
+
} catch (err) {
|
|
1183
|
+
if (err.code !== "ENOENT")
|
|
1184
|
+
throw err;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
var NEWLINE = 10;
|
|
1188
|
+
function isParseableJson(text) {
|
|
1189
|
+
try {
|
|
1190
|
+
JSON.parse(text);
|
|
1191
|
+
return true;
|
|
1192
|
+
} catch {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
function decodeCompleteLines(buf) {
|
|
1197
|
+
if (buf.length === 0)
|
|
1198
|
+
return [];
|
|
1199
|
+
const lines = buf.toString("utf8").split(`
|
|
1200
|
+
`);
|
|
1201
|
+
lines.pop();
|
|
1202
|
+
return lines.map((line) => JSON.parse(line));
|
|
1203
|
+
}
|
|
1204
|
+
function parseWithTailRepair(buf) {
|
|
1205
|
+
if (buf.length === 0)
|
|
1206
|
+
return { entries: [], torn: null };
|
|
1207
|
+
const endsWithNewline = buf[buf.length - 1] === NEWLINE;
|
|
1208
|
+
if (endsWithNewline) {
|
|
1209
|
+
const searchEnd = buf.length - 2;
|
|
1210
|
+
const prevNL = searchEnd < 0 ? -1 : buf.lastIndexOf(NEWLINE, searchEnd);
|
|
1211
|
+
const lastLineStart = prevNL + 1;
|
|
1212
|
+
const lastLine = buf.subarray(lastLineStart, buf.length - 1).toString("utf8");
|
|
1213
|
+
if (isParseableJson(lastLine)) {
|
|
1214
|
+
return { entries: decodeCompleteLines(buf), torn: null };
|
|
1215
|
+
}
|
|
1216
|
+
const keepBytes = lastLineStart;
|
|
1217
|
+
const tornRaw = Buffer.from(buf.subarray(lastLineStart));
|
|
1218
|
+
return { entries: decodeCompleteLines(buf.subarray(0, keepBytes)), torn: { raw: tornRaw, keepBytes } };
|
|
1219
|
+
}
|
|
1220
|
+
const lastNL = buf.lastIndexOf(NEWLINE);
|
|
1221
|
+
const keepBytes = lastNL + 1;
|
|
1222
|
+
const tornRaw = Buffer.from(buf.subarray(keepBytes));
|
|
1223
|
+
return { entries: decodeCompleteLines(buf.subarray(0, keepBytes)), torn: { raw: tornRaw, keepBytes } };
|
|
1224
|
+
}
|
|
1225
|
+
function hasLiveForeignLeaseHolder(lockPath) {
|
|
1226
|
+
const lease = readLeaseInfo(lockPath);
|
|
1227
|
+
return lease !== null && lease.pid !== process.pid && isPidAlive(lease.pid);
|
|
1228
|
+
}
|
|
1229
|
+
function walkResourceStems(dir, prefix, out) {
|
|
1230
|
+
let entries;
|
|
1231
|
+
try {
|
|
1232
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1233
|
+
} catch (err) {
|
|
1234
|
+
if (err.code === "ENOENT")
|
|
1235
|
+
return;
|
|
1236
|
+
throw err;
|
|
1237
|
+
}
|
|
1238
|
+
for (const dirent of entries) {
|
|
1239
|
+
const relPath = prefix === "" ? dirent.name : `${prefix}/${dirent.name}`;
|
|
1240
|
+
if (dirent.isDirectory()) {
|
|
1241
|
+
walkResourceStems(join3(dir, dirent.name), relPath, out);
|
|
1242
|
+
} else if (dirent.isFile() && dirent.name.endsWith(".jsonl")) {
|
|
1243
|
+
out.add(relPath.slice(0, -".jsonl".length));
|
|
1244
|
+
} else if (dirent.isFile() && dirent.name.endsWith(".meta.json")) {
|
|
1245
|
+
out.add(relPath.slice(0, -".meta.json".length));
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
function foldSummary(summaryPath, sessionId, newEntries, dialectExtra) {
|
|
1250
|
+
const previous = readJsonIfExists(summaryPath) ?? {};
|
|
1251
|
+
const mechanical = {};
|
|
1252
|
+
if (newEntries.length > 0) {
|
|
1253
|
+
const lastEntry = newEntries[newEntries.length - 1];
|
|
1254
|
+
mechanical.entryCount = (previous.entryCount ?? 0) + newEntries.length;
|
|
1255
|
+
mechanical.lastEntryType = lastEntry.type;
|
|
1256
|
+
if (lastEntry.timestamp !== undefined)
|
|
1257
|
+
mechanical.lastTimestamp = lastEntry.timestamp;
|
|
1258
|
+
}
|
|
1259
|
+
const updated = {
|
|
1260
|
+
...previous,
|
|
1261
|
+
...dialectExtra,
|
|
1262
|
+
sessionId,
|
|
1263
|
+
...mechanical,
|
|
1264
|
+
mtime: Date.now()
|
|
1265
|
+
};
|
|
1266
|
+
writeJsonAtomically(summaryPath, updated);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
class WinterCompatibilitySessionStore {
|
|
1270
|
+
winterHome;
|
|
1271
|
+
constructor(opts) {
|
|
1272
|
+
this.winterHome = opts.winterHome;
|
|
1273
|
+
}
|
|
1274
|
+
async append(key, entries) {
|
|
1275
|
+
if (entries.length === 0)
|
|
1276
|
+
return;
|
|
1277
|
+
const { dirLevels, stem } = locateResource(this.winterHome, key);
|
|
1278
|
+
for (const level of dirLevels)
|
|
1279
|
+
ensureSecureDir(level);
|
|
1280
|
+
const lockPath = `${sessionStem(this.winterHome, key.projectKey, key.sessionId)}.lock`;
|
|
1281
|
+
acquireLease(lockPath);
|
|
1282
|
+
chmodSync(lockPath, 384);
|
|
1283
|
+
const jsonlPath = `${stem}.jsonl`;
|
|
1284
|
+
const metaPath = `${stem}.meta.json`;
|
|
1285
|
+
const nativeEntries = [];
|
|
1286
|
+
let latestMetadata;
|
|
1287
|
+
let dialectExtra;
|
|
1288
|
+
for (const e of entries) {
|
|
1289
|
+
if (e.type === "agent_metadata") {
|
|
1290
|
+
latestMetadata = e;
|
|
1291
|
+
} else if (e.type === DIALECT_RECORD_ENTRY_TYPE) {
|
|
1292
|
+
const { type: _type, ...fields } = e;
|
|
1293
|
+
dialectExtra = fields;
|
|
1294
|
+
} else {
|
|
1295
|
+
nativeEntries.push(e);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (nativeEntries.length > 0) {
|
|
1299
|
+
appendLinesAtomically(jsonlPath, nativeEntries.map((e) => JSON.stringify(e)));
|
|
1300
|
+
}
|
|
1301
|
+
if (latestMetadata !== undefined) {
|
|
1302
|
+
writeJsonAtomically(metaPath, latestMetadata);
|
|
1303
|
+
}
|
|
1304
|
+
if (key.subpath === undefined && (nativeEntries.length > 0 || dialectExtra !== undefined)) {
|
|
1305
|
+
foldSummary(`${sessionStem(this.winterHome, key.projectKey, key.sessionId)}.summary.json`, key.sessionId, nativeEntries, dialectExtra);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
async load(key) {
|
|
1309
|
+
const { stem } = locateResource(this.winterHome, key);
|
|
1310
|
+
const jsonlPath = `${stem}.jsonl`;
|
|
1311
|
+
const metaPath = `${stem}.meta.json`;
|
|
1312
|
+
let raw;
|
|
1313
|
+
try {
|
|
1314
|
+
raw = readFileSync2(jsonlPath);
|
|
1315
|
+
} catch (err) {
|
|
1316
|
+
if (err.code !== "ENOENT")
|
|
1317
|
+
throw err;
|
|
1318
|
+
raw = null;
|
|
1319
|
+
}
|
|
1320
|
+
if (raw === null) {
|
|
1321
|
+
const meta = readJsonIfExists(metaPath);
|
|
1322
|
+
return meta === null ? null : [meta];
|
|
1323
|
+
}
|
|
1324
|
+
const { entries, torn } = parseWithTailRepair(raw);
|
|
1325
|
+
if (torn !== null) {
|
|
1326
|
+
const lockPath = `${sessionStem(this.winterHome, key.projectKey, key.sessionId)}.lock`;
|
|
1327
|
+
if (!hasLiveForeignLeaseHolder(lockPath)) {
|
|
1328
|
+
quarantineTornTail(jsonlPath, torn.raw);
|
|
1329
|
+
repairTruncate(jsonlPath, torn.keepBytes);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
const meta = readJsonIfExists(metaPath);
|
|
1333
|
+
if (meta !== null)
|
|
1334
|
+
entries.push(meta);
|
|
1335
|
+
return entries;
|
|
1336
|
+
}
|
|
1337
|
+
async listSessions(projectKey) {
|
|
1338
|
+
assertSafeSingleSegment(projectKey, "projectKey");
|
|
1339
|
+
const dir = projectDir(this.winterHome, projectKey);
|
|
1340
|
+
let names;
|
|
1341
|
+
try {
|
|
1342
|
+
names = readdirSync(dir);
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
if (err.code === "ENOENT")
|
|
1345
|
+
return [];
|
|
1346
|
+
throw err;
|
|
1347
|
+
}
|
|
1348
|
+
const result = [];
|
|
1349
|
+
const seenSessionIds = new Set;
|
|
1350
|
+
for (const name of names) {
|
|
1351
|
+
if (!name.endsWith(".jsonl"))
|
|
1352
|
+
continue;
|
|
1353
|
+
const full = join3(dir, name);
|
|
1354
|
+
const stat = statSync(full);
|
|
1355
|
+
if (!stat.isFile())
|
|
1356
|
+
continue;
|
|
1357
|
+
const sessionId = name.slice(0, -".jsonl".length);
|
|
1358
|
+
seenSessionIds.add(sessionId);
|
|
1359
|
+
result.push({ sessionId, mtime: stat.mtimeMs });
|
|
1360
|
+
}
|
|
1361
|
+
for (const name of names) {
|
|
1362
|
+
if (!name.endsWith(".meta.json"))
|
|
1363
|
+
continue;
|
|
1364
|
+
const sessionId = name.slice(0, -".meta.json".length);
|
|
1365
|
+
if (seenSessionIds.has(sessionId))
|
|
1366
|
+
continue;
|
|
1367
|
+
const full = join3(dir, name);
|
|
1368
|
+
const stat = statSync(full);
|
|
1369
|
+
if (!stat.isFile())
|
|
1370
|
+
continue;
|
|
1371
|
+
seenSessionIds.add(sessionId);
|
|
1372
|
+
result.push({ sessionId, mtime: stat.mtimeMs });
|
|
1373
|
+
}
|
|
1374
|
+
return result;
|
|
1375
|
+
}
|
|
1376
|
+
async listSessionSummaries(projectKey) {
|
|
1377
|
+
assertSafeSingleSegment(projectKey, "projectKey");
|
|
1378
|
+
const dir = projectDir(this.winterHome, projectKey);
|
|
1379
|
+
let names;
|
|
1380
|
+
try {
|
|
1381
|
+
names = readdirSync(dir);
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
if (err.code === "ENOENT")
|
|
1384
|
+
return [];
|
|
1385
|
+
throw err;
|
|
1386
|
+
}
|
|
1387
|
+
const result = [];
|
|
1388
|
+
for (const name of names) {
|
|
1389
|
+
if (!name.endsWith(".summary.json"))
|
|
1390
|
+
continue;
|
|
1391
|
+
const parsed = readJsonIfExists(join3(dir, name));
|
|
1392
|
+
if (parsed !== null)
|
|
1393
|
+
result.push(parsed);
|
|
1394
|
+
}
|
|
1395
|
+
return result;
|
|
1396
|
+
}
|
|
1397
|
+
async delete(key) {
|
|
1398
|
+
const { stem } = locateResource(this.winterHome, key);
|
|
1399
|
+
if (key.subpath === undefined) {
|
|
1400
|
+
rmIfExists(`${stem}.jsonl`);
|
|
1401
|
+
rmIfExists(`${stem}.jsonl.tail-quarantine`);
|
|
1402
|
+
rmIfExists(`${stem}.lock`);
|
|
1403
|
+
rmIfExists(`${stem}.summary.json`);
|
|
1404
|
+
rmIfExists(`${stem}.meta.json`);
|
|
1405
|
+
rmIfExists(`${stem}${PROVIDER_STATE_FILE_SUFFIX}`);
|
|
1406
|
+
rmSync(stem, { recursive: true, force: true });
|
|
1407
|
+
} else {
|
|
1408
|
+
rmIfExists(`${stem}.jsonl`);
|
|
1409
|
+
rmIfExists(`${stem}.jsonl.tail-quarantine`);
|
|
1410
|
+
rmIfExists(`${stem}.meta.json`);
|
|
1411
|
+
rmIfExists(`${stem}${PROVIDER_STATE_FILE_SUFFIX}`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
async listSubkeys(key) {
|
|
1415
|
+
const sessionDir = sessionStem(this.winterHome, key.projectKey, key.sessionId);
|
|
1416
|
+
const results = new Set;
|
|
1417
|
+
walkResourceStems(sessionDir, "", results);
|
|
1418
|
+
return [...results];
|
|
1419
|
+
}
|
|
1420
|
+
async listProjectKeys() {
|
|
1421
|
+
const dir = join3(this.winterHome, "projects");
|
|
1422
|
+
let entries;
|
|
1423
|
+
try {
|
|
1424
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1425
|
+
} catch (err) {
|
|
1426
|
+
if (err.code === "ENOENT")
|
|
1427
|
+
return [];
|
|
1428
|
+
throw err;
|
|
1429
|
+
}
|
|
1430
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
1431
|
+
}
|
|
1432
|
+
async copyProviderStateForFork(src, dest) {
|
|
1433
|
+
const { stem: srcStem } = locateResource(this.winterHome, src);
|
|
1434
|
+
const { stem: destStem, dirLevels } = locateResource(this.winterHome, dest);
|
|
1435
|
+
let raw;
|
|
1436
|
+
try {
|
|
1437
|
+
raw = readFileSync2(`${srcStem}${PROVIDER_STATE_FILE_SUFFIX}`, "utf8");
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
if (err.code === "ENOENT")
|
|
1440
|
+
return 0;
|
|
1441
|
+
throw err;
|
|
1442
|
+
}
|
|
1443
|
+
const lines = [];
|
|
1444
|
+
for (const line of raw.split(`
|
|
1445
|
+
`)) {
|
|
1446
|
+
if (line.length === 0)
|
|
1447
|
+
continue;
|
|
1448
|
+
let record;
|
|
1449
|
+
try {
|
|
1450
|
+
record = JSON.parse(line);
|
|
1451
|
+
} catch {
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
if (typeof record !== "object" || record === null || Array.isArray(record))
|
|
1455
|
+
continue;
|
|
1456
|
+
lines.push(JSON.stringify({ ...record, uuid: randomUUID2(), sessionId: dest.sessionId }));
|
|
1457
|
+
}
|
|
1458
|
+
if (lines.length === 0)
|
|
1459
|
+
return 0;
|
|
1460
|
+
for (const level of dirLevels)
|
|
1461
|
+
ensureSecureDir(level);
|
|
1462
|
+
appendLinesAtomically(`${destStem}${PROVIDER_STATE_FILE_SUFFIX}`, lines);
|
|
1463
|
+
return lines.length;
|
|
1464
|
+
}
|
|
1465
|
+
async readSessionSummary(key) {
|
|
1466
|
+
assertSafeSingleSegment(key.projectKey, "projectKey");
|
|
1467
|
+
assertSafeSingleSegment(key.sessionId, "sessionId");
|
|
1468
|
+
return readJsonIfExists(`${sessionStem(this.winterHome, key.projectKey, key.sessionId)}.summary.json`);
|
|
1469
|
+
}
|
|
1470
|
+
async mergeSessionMetadata(key, patch) {
|
|
1471
|
+
const stem = sessionStem(this.winterHome, key.projectKey, key.sessionId);
|
|
1472
|
+
const lockPath = `${stem}.lock`;
|
|
1473
|
+
acquireLease(lockPath);
|
|
1474
|
+
chmodSync(lockPath, 384);
|
|
1475
|
+
const summaryPath = `${stem}.summary.json`;
|
|
1476
|
+
const previous = readJsonIfExists(summaryPath) ?? {};
|
|
1477
|
+
const updated = {
|
|
1478
|
+
...previous,
|
|
1479
|
+
...patch,
|
|
1480
|
+
sessionId: key.sessionId,
|
|
1481
|
+
mtime: Date.now()
|
|
1482
|
+
};
|
|
1483
|
+
writeJsonAtomically(summaryPath, updated);
|
|
1484
|
+
}
|
|
1485
|
+
async acquireSessionLease(key) {
|
|
1486
|
+
const { dirLevels } = locateResource(this.winterHome, key);
|
|
1487
|
+
for (const level of dirLevels)
|
|
1488
|
+
ensureSecureDir(level);
|
|
1489
|
+
const lockPath = `${sessionStem(this.winterHome, key.projectKey, key.sessionId)}.lock`;
|
|
1490
|
+
acquireLease(lockPath);
|
|
1491
|
+
chmodSync(lockPath, 384);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
// src/store/fork-session.ts
|
|
1495
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
1496
|
+
var IDENTITY_FIELDS = ["providerId", "modelKey", "adapterId", "adapterVersion", "catalogVersion", "authRef", "classifierPin"];
|
|
1497
|
+
async function forkSessionByKey(store, src) {
|
|
1498
|
+
const entries = await store.load(src);
|
|
1499
|
+
if (entries === null) {
|
|
1500
|
+
throw new SessionNotFoundError("not_found", `forkSession: source session not found: ${JSON.stringify(src)}`);
|
|
1501
|
+
}
|
|
1502
|
+
const newSessionId = randomUUID3();
|
|
1503
|
+
const rewritten = entries.map((e) => typeof e.sessionId === "string" ? { ...e, sessionId: newSessionId } : e);
|
|
1504
|
+
const destKey = { projectKey: src.projectKey, sessionId: newSessionId, ...src.subpath !== undefined ? { subpath: src.subpath } : {} };
|
|
1505
|
+
await store.append(destKey, rewritten);
|
|
1506
|
+
const carry = store;
|
|
1507
|
+
try {
|
|
1508
|
+
await carry.copyProviderStateForFork?.(src, destKey);
|
|
1509
|
+
const summary = destKey.subpath === undefined && carry.readSessionSummary !== undefined ? await carry.readSessionSummary({ projectKey: src.projectKey, sessionId: src.sessionId }) : null;
|
|
1510
|
+
if (summary !== null && typeof summary.providerId === "string" && typeof summary.modelKey === "string") {
|
|
1511
|
+
const identity = { type: DIALECT_RECORD_ENTRY_TYPE };
|
|
1512
|
+
for (const field of IDENTITY_FIELDS) {
|
|
1513
|
+
const value = summary[field];
|
|
1514
|
+
if (typeof value === "string")
|
|
1515
|
+
identity[field] = value;
|
|
1516
|
+
}
|
|
1517
|
+
await store.append(destKey, [identity]);
|
|
1518
|
+
}
|
|
1519
|
+
} catch {}
|
|
1520
|
+
return { sessionId: newSessionId };
|
|
1521
|
+
}
|
|
1522
|
+
// src/sessions.ts
|
|
1523
|
+
function resolveHome(winterHome, brand) {
|
|
1524
|
+
if (winterHome !== undefined)
|
|
1525
|
+
return winterHome;
|
|
1526
|
+
if (brand === undefined)
|
|
1527
|
+
return resolveWinterHome();
|
|
1528
|
+
const resolved = resolveBrand(brand);
|
|
1529
|
+
if (!resolved.ok)
|
|
1530
|
+
throw new TypeError(`sessions: invalid brand profile -- ${resolved.reason}`);
|
|
1531
|
+
return resolveWinterHome(undefined, resolved.brand);
|
|
1532
|
+
}
|
|
1533
|
+
function openStore(opts) {
|
|
1534
|
+
return new WinterCompatibilitySessionStore({ winterHome: resolveHome(opts?.winterHome, opts?.brand) });
|
|
1535
|
+
}
|
|
1536
|
+
async function findInProject(store, projectKey, sessionId) {
|
|
1537
|
+
const sessions = await store.listSessions(projectKey);
|
|
1538
|
+
return sessions.find((s) => s.sessionId === sessionId);
|
|
1539
|
+
}
|
|
1540
|
+
async function resolveSession(store, sessionId, directory) {
|
|
1541
|
+
if (directory !== undefined) {
|
|
1542
|
+
const projectKey = compatibilityKeys(directory).transcriptProjectKey;
|
|
1543
|
+
const found = await findInProject(store, projectKey, sessionId);
|
|
1544
|
+
if (found === undefined) {
|
|
1545
|
+
throw new SessionNotFoundError("not_found", `session not found: ${sessionId} (in directory-scoped project ${projectKey})`);
|
|
1546
|
+
}
|
|
1547
|
+
return { projectKey, mtime: found.mtime };
|
|
1548
|
+
}
|
|
1549
|
+
const allProjectKeys = await store.listProjectKeys();
|
|
1550
|
+
const matches = [];
|
|
1551
|
+
for (const projectKey of allProjectKeys) {
|
|
1552
|
+
const found = await findInProject(store, projectKey, sessionId);
|
|
1553
|
+
if (found !== undefined)
|
|
1554
|
+
matches.push({ projectKey, mtime: found.mtime });
|
|
1555
|
+
}
|
|
1556
|
+
if (matches.length === 0) {
|
|
1557
|
+
throw new SessionNotFoundError("not_found", `session not found in any project: ${sessionId}`);
|
|
1558
|
+
}
|
|
1559
|
+
if (matches.length > 1) {
|
|
1560
|
+
throw new SessionNotFoundError("ambiguous", `session ${sessionId} found in ${matches.length} projects (${matches.map((m) => m.projectKey).join(", ")}); refusing to pick arbitrarily`);
|
|
1561
|
+
}
|
|
1562
|
+
return matches[0];
|
|
1563
|
+
}
|
|
1564
|
+
async function listSessions(opts) {
|
|
1565
|
+
const store = openStore(opts);
|
|
1566
|
+
const projectKeys = opts?.directory !== undefined ? [compatibilityKeys(opts.directory).transcriptProjectKey] : await store.listProjectKeys();
|
|
1567
|
+
const result = [];
|
|
1568
|
+
for (const projectKey of projectKeys) {
|
|
1569
|
+
const [sessions, summaries] = await Promise.all([store.listSessions(projectKey), store.listSessionSummaries(projectKey)]);
|
|
1570
|
+
const summaryById = new Map(summaries.map((s) => [s.sessionId, s]));
|
|
1571
|
+
for (const s of sessions) {
|
|
1572
|
+
const summary = summaryById.get(s.sessionId);
|
|
1573
|
+
result.push({
|
|
1574
|
+
sessionId: s.sessionId,
|
|
1575
|
+
projectKey,
|
|
1576
|
+
mtime: s.mtime,
|
|
1577
|
+
...summary?.name !== undefined ? { name: summary.name } : {},
|
|
1578
|
+
...summary?.tags !== undefined ? { tags: summary.tags } : {}
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return result;
|
|
1583
|
+
}
|
|
1584
|
+
async function getSessionInfo(sessionId, opts) {
|
|
1585
|
+
const store = openStore(opts);
|
|
1586
|
+
const { projectKey, mtime } = await resolveSession(store, sessionId, opts?.directory);
|
|
1587
|
+
const summaries = await store.listSessionSummaries(projectKey);
|
|
1588
|
+
const summary = summaries.find((s) => s.sessionId === sessionId);
|
|
1589
|
+
const entryCount = summary?.entryCount ?? (await store.load({ projectKey, sessionId }))?.length ?? 0;
|
|
1590
|
+
return {
|
|
1591
|
+
sessionId,
|
|
1592
|
+
projectKey,
|
|
1593
|
+
mtime,
|
|
1594
|
+
entryCount,
|
|
1595
|
+
...summary?.name !== undefined ? { name: summary.name } : {},
|
|
1596
|
+
...summary?.tags !== undefined ? { tags: summary.tags } : {}
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
async function getSessionMessages(sessionId, opts) {
|
|
1600
|
+
const store = openStore(opts);
|
|
1601
|
+
const { projectKey } = await resolveSession(store, sessionId, opts?.directory);
|
|
1602
|
+
const entries = await store.load({ projectKey, sessionId });
|
|
1603
|
+
return entries ?? [];
|
|
1604
|
+
}
|
|
1605
|
+
async function renameSession(sessionId, name, opts) {
|
|
1606
|
+
const store = openStore(opts);
|
|
1607
|
+
const { projectKey } = await resolveSession(store, sessionId, opts?.directory);
|
|
1608
|
+
await store.mergeSessionMetadata({ projectKey, sessionId }, { name });
|
|
1609
|
+
}
|
|
1610
|
+
async function tagSession(sessionId, tags, opts) {
|
|
1611
|
+
const store = openStore(opts);
|
|
1612
|
+
const { projectKey } = await resolveSession(store, sessionId, opts?.directory);
|
|
1613
|
+
await store.mergeSessionMetadata({ projectKey, sessionId }, { tags });
|
|
1614
|
+
}
|
|
1615
|
+
async function deleteSession(sessionId, opts) {
|
|
1616
|
+
const store = openStore(opts);
|
|
1617
|
+
const { projectKey } = await resolveSession(store, sessionId, opts?.directory);
|
|
1618
|
+
await store.delete({ projectKey, sessionId });
|
|
1619
|
+
}
|
|
1620
|
+
async function forkSession(sessionId, opts) {
|
|
1621
|
+
const store = openStore(opts);
|
|
1622
|
+
const { projectKey } = await resolveSession(store, sessionId, opts?.directory);
|
|
1623
|
+
return forkSessionByKey(store, { projectKey, sessionId });
|
|
1624
|
+
}
|
|
1625
|
+
var SUBAGENT_SUBPATH_PREFIX = "subagents/";
|
|
1626
|
+
async function listSubagents(sessionId, opts) {
|
|
1627
|
+
const store = openStore(opts);
|
|
1628
|
+
const { projectKey } = await resolveSession(store, sessionId, opts?.directory);
|
|
1629
|
+
const subkeys = await store.listSubkeys({ projectKey, sessionId });
|
|
1630
|
+
const agents = [];
|
|
1631
|
+
for (const subkey of subkeys) {
|
|
1632
|
+
if (subkey.startsWith(SUBAGENT_SUBPATH_PREFIX)) {
|
|
1633
|
+
agents.push({ agentId: subkey.slice(SUBAGENT_SUBPATH_PREFIX.length) });
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return agents;
|
|
1637
|
+
}
|
|
1638
|
+
async function getSubagentMessages(sessionId, agentId, opts) {
|
|
1639
|
+
const store = openStore(opts);
|
|
1640
|
+
const { projectKey } = await resolveSession(store, sessionId, opts?.directory);
|
|
1641
|
+
const entries = await store.load({ projectKey, sessionId, subpath: `${SUBAGENT_SUBPATH_PREFIX}${agentId}` });
|
|
1642
|
+
if (entries === null) {
|
|
1643
|
+
throw new SessionNotFoundError("not_found", `subagent not found: ${agentId} (session ${sessionId})`);
|
|
1644
|
+
}
|
|
1645
|
+
return entries;
|
|
1646
|
+
}
|
|
1647
|
+
// src/settings/sources.ts
|
|
1648
|
+
import { readFile } from "node:fs/promises";
|
|
1649
|
+
import { join as join4 } from "node:path";
|
|
1650
|
+
function settingsPathFor(source, opts) {
|
|
1651
|
+
const brand = opts.brand ?? WINTER_BRAND;
|
|
1652
|
+
switch (source) {
|
|
1653
|
+
case "user":
|
|
1654
|
+
return join4(opts.winterHome ?? resolveWinterHome(opts.env, brand), "settings.json");
|
|
1655
|
+
case "project":
|
|
1656
|
+
return join4(opts.cwd, brand.projectDirName, "settings.json");
|
|
1657
|
+
case "local":
|
|
1658
|
+
return join4(opts.cwd, brand.projectDirName, "settings.local.json");
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
function isPlainObject(v) {
|
|
1662
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1663
|
+
}
|
|
1664
|
+
async function loadSettingsFile(path) {
|
|
1665
|
+
let raw;
|
|
1666
|
+
try {
|
|
1667
|
+
raw = await readFile(path, "utf8");
|
|
1668
|
+
} catch (err) {
|
|
1669
|
+
const code = err?.code;
|
|
1670
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
1671
|
+
return { present: false, loaded: false, values: {} };
|
|
1672
|
+
return { present: true, loaded: false, error: `unreadable: ${err.message}`, values: {} };
|
|
1673
|
+
}
|
|
1674
|
+
let parsed;
|
|
1675
|
+
try {
|
|
1676
|
+
parsed = JSON.parse(raw);
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
return { present: true, loaded: false, error: `malformed JSON: ${err.message}`, values: {} };
|
|
1679
|
+
}
|
|
1680
|
+
if (!isPlainObject(parsed)) {
|
|
1681
|
+
return { present: true, loaded: false, error: `expected a JSON object at the top level, got ${Array.isArray(parsed) ? "an array" : typeof parsed}`, values: {} };
|
|
1682
|
+
}
|
|
1683
|
+
return { present: true, loaded: true, values: parsed };
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// src/settings/types.ts
|
|
1687
|
+
var SETTING_SOURCES = ["user", "project", "local"];
|
|
1688
|
+
function providerSettingsFrom(settings) {
|
|
1689
|
+
const raw = settings?.["providers"];
|
|
1690
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
1691
|
+
return {};
|
|
1692
|
+
const out = {};
|
|
1693
|
+
for (const [id, value] of Object.entries(raw)) {
|
|
1694
|
+
if (id.length === 0)
|
|
1695
|
+
continue;
|
|
1696
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
1697
|
+
continue;
|
|
1698
|
+
const enabled = value.enabled;
|
|
1699
|
+
out[id] = { enabled: enabled === false ? false : true };
|
|
1700
|
+
}
|
|
1701
|
+
return out;
|
|
1702
|
+
}
|
|
1703
|
+
var OVERLAY_NEVER_KEYS = ["autoMemoryDirectory", "autoMode", "outputStyle"];
|
|
1704
|
+
var ESCALATING_PERMISSION_MODES = ["bypassPermissions", "auto", "acceptEdits"];
|
|
1705
|
+
var PROJECT_PERMISSIVE_KEYS = ["allow", "additionalDirectories"];
|
|
1706
|
+
|
|
1707
|
+
// src/settings/resolve.ts
|
|
1708
|
+
function isPlainObject2(v) {
|
|
1709
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1710
|
+
}
|
|
1711
|
+
function deepMergeInto(target, overlay) {
|
|
1712
|
+
for (const [key, value] of Object.entries(overlay)) {
|
|
1713
|
+
if (value === undefined)
|
|
1714
|
+
continue;
|
|
1715
|
+
const existing = target[key];
|
|
1716
|
+
if (isPlainObject2(existing) && isPlainObject2(value)) {
|
|
1717
|
+
const merged = { ...existing };
|
|
1718
|
+
deepMergeInto(merged, value);
|
|
1719
|
+
target[key] = merged;
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
target[key] = value;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
var MAX_PLANS_DIRECTORY_LENGTH = 200;
|
|
1726
|
+
function validateProjectPlansDirectory(value) {
|
|
1727
|
+
if (typeof value !== "string")
|
|
1728
|
+
return { ok: false, reason: `"plansDirectory" must be a string, got ${typeof value}` };
|
|
1729
|
+
if (value.length === 0)
|
|
1730
|
+
return { ok: false, reason: `"plansDirectory" must not be empty` };
|
|
1731
|
+
if (value.length > MAX_PLANS_DIRECTORY_LENGTH)
|
|
1732
|
+
return { ok: false, reason: `"plansDirectory" exceeds ${MAX_PLANS_DIRECTORY_LENGTH} characters` };
|
|
1733
|
+
if (/[\u0000-\u001f\u007f]/.test(value))
|
|
1734
|
+
return { ok: false, reason: `"plansDirectory" contains control characters, which cannot appear in a path` };
|
|
1735
|
+
if (value.startsWith("/") || value.startsWith("~"))
|
|
1736
|
+
return { ok: false, reason: `"plansDirectory" from the project tier must be RELATIVE to the project root` };
|
|
1737
|
+
if (value.split("/").includes(".."))
|
|
1738
|
+
return { ok: false, reason: `"plansDirectory" from the project tier must not traverse upward` };
|
|
1739
|
+
return { ok: true };
|
|
1740
|
+
}
|
|
1741
|
+
function describeMalformedPermissionArrays(values) {
|
|
1742
|
+
const permissions = values["permissions"];
|
|
1743
|
+
if (!isPlainObject2(permissions)) {
|
|
1744
|
+
return permissions === undefined ? undefined : `"permissions" must be an object, got ${Array.isArray(permissions) ? "an array" : typeof permissions}`;
|
|
1745
|
+
}
|
|
1746
|
+
const problems = [];
|
|
1747
|
+
for (const key of ["allow", "ask", "deny", "additionalDirectories"]) {
|
|
1748
|
+
const value = permissions[key];
|
|
1749
|
+
if (value === undefined)
|
|
1750
|
+
continue;
|
|
1751
|
+
if (!Array.isArray(value)) {
|
|
1752
|
+
problems.push(`"permissions.${key}" must be an array of strings, got ${typeof value}`);
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
const bad = value.filter((item) => typeof item !== "string").length;
|
|
1756
|
+
if (bad > 0)
|
|
1757
|
+
problems.push(`"permissions.${key}" has ${bad} non-string entr${bad === 1 ? "y" : "ies"}, which are ignored`);
|
|
1758
|
+
}
|
|
1759
|
+
return problems.length > 0 ? problems.join("; ") : undefined;
|
|
1760
|
+
}
|
|
1761
|
+
function describeOverlayNeverKeys(values) {
|
|
1762
|
+
const present = OVERLAY_NEVER_KEYS.filter((key) => values[key] !== undefined);
|
|
1763
|
+
if (present.length === 0)
|
|
1764
|
+
return;
|
|
1765
|
+
return `${present.map((k) => `"${k}"`).join(", ")} ${present.length === 1 ? "is" : "are"} ignored from the project tier (a repository may not set ${present.length === 1 ? "it" : "them"}); move ${present.length === 1 ? "it" : "them"} to your user settings`;
|
|
1766
|
+
}
|
|
1767
|
+
function withoutOverlayNeverKeys(values) {
|
|
1768
|
+
const out = { ...values };
|
|
1769
|
+
for (const key of OVERLAY_NEVER_KEYS)
|
|
1770
|
+
delete out[key];
|
|
1771
|
+
if ("plansDirectory" in out && !validateProjectPlansDirectory(out["plansDirectory"]).ok)
|
|
1772
|
+
delete out["plansDirectory"];
|
|
1773
|
+
return out;
|
|
1774
|
+
}
|
|
1775
|
+
var MODEL_SLOT_KEYS = ["modelSlots", "preferredProviders", "advisor"];
|
|
1776
|
+
function describeUntrustedModelSlotKeys(values) {
|
|
1777
|
+
const present = MODEL_SLOT_KEYS.filter((key) => values[key] !== undefined);
|
|
1778
|
+
if (present.length === 0)
|
|
1779
|
+
return;
|
|
1780
|
+
return `${present.map((k) => `"${k}"`).join(", ")} ${present.length === 1 ? "is" : "are"} ignored from the project tier (untrusted workspace, WS-13c R13c-7 / D30): a repository may not choose which models the agent uses, nor which model the advisor sends this session's conversation to; declare the workspace trusted, or set ${present.length === 1 ? "it" : "them"} in your user settings`;
|
|
1781
|
+
}
|
|
1782
|
+
function withoutUntrustedModelSlotKeys(values) {
|
|
1783
|
+
const out = { ...values };
|
|
1784
|
+
for (const key of MODEL_SLOT_KEYS)
|
|
1785
|
+
delete out[key];
|
|
1786
|
+
return out;
|
|
1787
|
+
}
|
|
1788
|
+
function projectTierContribution(values, trustedWorkspace) {
|
|
1789
|
+
const withoutNever = withoutOverlayNeverKeys(values);
|
|
1790
|
+
return trustedWorkspace ? withoutNever : withoutUntrustedModelSlotKeys(withoutNever);
|
|
1791
|
+
}
|
|
1792
|
+
var DERIVED_ONLY_KEYS = ["modelSlotsIgnored"];
|
|
1793
|
+
function withoutDerivedOnlyKeys(values) {
|
|
1794
|
+
const out = { ...values };
|
|
1795
|
+
for (const key of DERIVED_ONLY_KEYS)
|
|
1796
|
+
delete out[key];
|
|
1797
|
+
return out;
|
|
1798
|
+
}
|
|
1799
|
+
function tierContribution(values, isProject, trustedWorkspace) {
|
|
1800
|
+
const projectFiltered = isProject ? projectTierContribution(values, trustedWorkspace) : values;
|
|
1801
|
+
return withoutDerivedOnlyKeys(projectFiltered);
|
|
1802
|
+
}
|
|
1803
|
+
var PERMISSION_RULE_ARRAY_KEYS = ["allow", "ask", "deny", "additionalDirectories"];
|
|
1804
|
+
function stringArrayOrUndefined(v) {
|
|
1805
|
+
if (!Array.isArray(v))
|
|
1806
|
+
return;
|
|
1807
|
+
const strings = v.filter((item) => typeof item === "string");
|
|
1808
|
+
return strings.length > 0 ? strings : undefined;
|
|
1809
|
+
}
|
|
1810
|
+
function unionRuleArray(tiersLowestFirst, key) {
|
|
1811
|
+
const seen = new Set;
|
|
1812
|
+
const out = [];
|
|
1813
|
+
for (const tier of tiersLowestFirst) {
|
|
1814
|
+
const permissions = tier.values["permissions"];
|
|
1815
|
+
if (!isPlainObject2(permissions))
|
|
1816
|
+
continue;
|
|
1817
|
+
for (const rule of stringArrayOrUndefined(permissions[key]) ?? []) {
|
|
1818
|
+
if (seen.has(rule))
|
|
1819
|
+
continue;
|
|
1820
|
+
seen.add(rule);
|
|
1821
|
+
out.push(rule);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
return out.length > 0 ? out : undefined;
|
|
1825
|
+
}
|
|
1826
|
+
function restrictProviderEnables(effective, tiersLowestFirst) {
|
|
1827
|
+
const disabled = new Set;
|
|
1828
|
+
for (const tier of tiersLowestFirst) {
|
|
1829
|
+
const block = tier.values["providers"];
|
|
1830
|
+
if (!isPlainObject2(block))
|
|
1831
|
+
continue;
|
|
1832
|
+
for (const [id, value] of Object.entries(block)) {
|
|
1833
|
+
if (isPlainObject2(value) && value.enabled === false)
|
|
1834
|
+
disabled.add(id);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
if (disabled.size === 0)
|
|
1838
|
+
return;
|
|
1839
|
+
const merged = effective["providers"];
|
|
1840
|
+
const providers = isPlainObject2(merged) ? { ...merged } : {};
|
|
1841
|
+
for (const id of disabled) {
|
|
1842
|
+
const entry = providers[id];
|
|
1843
|
+
providers[id] = { ...isPlainObject2(entry) ? entry : {}, enabled: false };
|
|
1844
|
+
}
|
|
1845
|
+
effective["providers"] = providers;
|
|
1846
|
+
}
|
|
1847
|
+
function unionPermissionRuleArrays(effective, tiersLowestFirst) {
|
|
1848
|
+
const merged = effective["permissions"];
|
|
1849
|
+
const permissions = isPlainObject2(merged) ? { ...merged } : {};
|
|
1850
|
+
let any = isPlainObject2(merged);
|
|
1851
|
+
for (const key of PERMISSION_RULE_ARRAY_KEYS) {
|
|
1852
|
+
const unioned = unionRuleArray(tiersLowestFirst, key);
|
|
1853
|
+
if (unioned === undefined)
|
|
1854
|
+
delete permissions[key];
|
|
1855
|
+
else {
|
|
1856
|
+
permissions[key] = unioned;
|
|
1857
|
+
any = true;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (any)
|
|
1861
|
+
effective["permissions"] = permissions;
|
|
1862
|
+
}
|
|
1863
|
+
var SOURCE_ORDER_LOWEST_FIRST = ["user", "project", "local"];
|
|
1864
|
+
async function resolveSettingsDetailed(opts = {}) {
|
|
1865
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1866
|
+
const selected = opts.settingSources ?? SETTING_SOURCES;
|
|
1867
|
+
const pathOpts = {
|
|
1868
|
+
cwd,
|
|
1869
|
+
...opts.winterHome !== undefined ? { winterHome: opts.winterHome } : {},
|
|
1870
|
+
...opts.env !== undefined ? { env: opts.env } : {},
|
|
1871
|
+
...opts.brand !== undefined ? { brand: opts.brand } : {}
|
|
1872
|
+
};
|
|
1873
|
+
const lowestFirst = [];
|
|
1874
|
+
for (const source of SOURCE_ORDER_LOWEST_FIRST) {
|
|
1875
|
+
if (!selected.includes(source))
|
|
1876
|
+
continue;
|
|
1877
|
+
const path = settingsPathFor(source, pathOpts);
|
|
1878
|
+
const file = await loadSettingsFile(path);
|
|
1879
|
+
if (!file.present)
|
|
1880
|
+
continue;
|
|
1881
|
+
const valueError = describeMalformedPermissionArrays(file.values);
|
|
1882
|
+
const plansCheck = source === "project" && file.values["plansDirectory"] !== undefined ? validateProjectPlansDirectory(file.values["plansDirectory"]) : { ok: true };
|
|
1883
|
+
const plansError = plansCheck.ok ? undefined : plansCheck.reason;
|
|
1884
|
+
const neverKeyError = source === "project" ? describeOverlayNeverKeys(file.values) : undefined;
|
|
1885
|
+
const untrustedModelSlotsError = source === "project" && opts.trustedWorkspace !== true ? describeUntrustedModelSlotKeys(file.values) : undefined;
|
|
1886
|
+
const mergedError = [file.error, valueError, plansError, neverKeyError, untrustedModelSlotsError].filter((e) => e !== undefined).join("; ");
|
|
1887
|
+
lowestFirst.push({
|
|
1888
|
+
source,
|
|
1889
|
+
path,
|
|
1890
|
+
settings: file.values,
|
|
1891
|
+
values: file.values,
|
|
1892
|
+
loaded: file.loaded,
|
|
1893
|
+
...mergedError.length > 0 ? { error: mergedError } : {}
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
if (opts.inline !== undefined) {
|
|
1897
|
+
lowestFirst.push({ source: "flag", settings: opts.inline, values: opts.inline, loaded: true });
|
|
1898
|
+
}
|
|
1899
|
+
if (opts.managedSettings !== undefined) {
|
|
1900
|
+
lowestFirst.push({ source: "managed", policyOrigin: "file", settings: opts.managedSettings, values: opts.managedSettings, loaded: true });
|
|
1901
|
+
}
|
|
1902
|
+
if (opts.serverManagedSettings !== undefined) {
|
|
1903
|
+
lowestFirst.push({ source: "managed", policyOrigin: "remote", settings: opts.serverManagedSettings, values: opts.serverManagedSettings, loaded: true });
|
|
1904
|
+
}
|
|
1905
|
+
const trustedWorkspace = opts.trustedWorkspace === true;
|
|
1906
|
+
const effective = {};
|
|
1907
|
+
const provenance = {};
|
|
1908
|
+
for (const entry of lowestFirst) {
|
|
1909
|
+
const contribution = tierContribution(entry.values, entry.source === "project", trustedWorkspace);
|
|
1910
|
+
deepMergeInto(effective, contribution);
|
|
1911
|
+
for (const key of Object.keys(contribution)) {
|
|
1912
|
+
if (contribution[key] === undefined)
|
|
1913
|
+
continue;
|
|
1914
|
+
provenance[key] = {
|
|
1915
|
+
source: entry.source,
|
|
1916
|
+
...entry.path !== undefined ? { path: entry.path } : {},
|
|
1917
|
+
...entry.policyOrigin !== undefined ? { policyOrigin: entry.policyOrigin } : {}
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
const overlayFilteredTiers = lowestFirst.map((entry) => ({ values: tierContribution(entry.values, entry.source === "project", trustedWorkspace) }));
|
|
1922
|
+
unionPermissionRuleArrays(effective, overlayFilteredTiers);
|
|
1923
|
+
restrictProviderEnables(effective, overlayFilteredTiers);
|
|
1924
|
+
if (!trustedWorkspace) {
|
|
1925
|
+
const projectIndex = lowestFirst.findIndex((e) => e.source === "project");
|
|
1926
|
+
if (projectIndex !== -1) {
|
|
1927
|
+
const projectValues = lowestFirst[projectIndex].values;
|
|
1928
|
+
const projectHadEither = MODEL_SLOT_KEYS.some((key) => projectValues[key] !== undefined);
|
|
1929
|
+
if (projectHadEither) {
|
|
1930
|
+
const higherProvidedModelSlots = lowestFirst.slice(projectIndex + 1).some((e) => e.values["modelSlots"] !== undefined);
|
|
1931
|
+
if (!higherProvidedModelSlots)
|
|
1932
|
+
effective["modelSlotsIgnored"] = "untrusted-project";
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
const perSource = [...lowestFirst].reverse();
|
|
1937
|
+
return {
|
|
1938
|
+
effective,
|
|
1939
|
+
provenance,
|
|
1940
|
+
sources: perSource.map((e) => ({
|
|
1941
|
+
source: e.source,
|
|
1942
|
+
settings: e.settings,
|
|
1943
|
+
...e.path !== undefined ? { path: e.path } : {},
|
|
1944
|
+
...e.policyOrigin !== undefined ? { policyOrigin: e.policyOrigin } : {}
|
|
1945
|
+
})),
|
|
1946
|
+
perSource
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
async function resolveSettings(opts) {
|
|
1950
|
+
const { effective, provenance, sources } = await resolveSettingsDetailed(opts ?? {});
|
|
1951
|
+
return { effective, provenance, sources };
|
|
1952
|
+
}
|
|
1953
|
+
function winningSourceFor(resolved, path) {
|
|
1954
|
+
for (const entry of resolved.sources) {
|
|
1955
|
+
let cursor = entry.settings;
|
|
1956
|
+
let found = true;
|
|
1957
|
+
for (const segment of path) {
|
|
1958
|
+
if (!isPlainObject2(cursor) || !(segment in cursor)) {
|
|
1959
|
+
found = false;
|
|
1960
|
+
break;
|
|
1961
|
+
}
|
|
1962
|
+
cursor = cursor[segment];
|
|
1963
|
+
}
|
|
1964
|
+
if (found && cursor !== undefined)
|
|
1965
|
+
return entry;
|
|
1966
|
+
}
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
function filterEscalatingDefaultMode(resolved) {
|
|
1970
|
+
const permissions = resolved.effective["permissions"];
|
|
1971
|
+
if (!isPlainObject2(permissions))
|
|
1972
|
+
return { ...resolved.effective };
|
|
1973
|
+
const defaultMode = permissions["defaultMode"];
|
|
1974
|
+
if (typeof defaultMode !== "string" || !ESCALATING_PERMISSION_MODES.includes(defaultMode))
|
|
1975
|
+
return { ...resolved.effective };
|
|
1976
|
+
if (winningSourceFor(resolved, ["permissions", "defaultMode"])?.source !== "project")
|
|
1977
|
+
return { ...resolved.effective };
|
|
1978
|
+
const nextPermissions = { ...permissions };
|
|
1979
|
+
delete nextPermissions["defaultMode"];
|
|
1980
|
+
return { ...resolved.effective, permissions: nextPermissions };
|
|
1981
|
+
}
|
|
1982
|
+
function applyWorkspaceTrust(resolved, opts = {}) {
|
|
1983
|
+
const afterModeFilter = filterEscalatingDefaultMode(resolved);
|
|
1984
|
+
if (opts.trustedWorkspace === true)
|
|
1985
|
+
return afterModeFilter;
|
|
1986
|
+
const permissions = afterModeFilter["permissions"];
|
|
1987
|
+
if (!isPlainObject2(permissions))
|
|
1988
|
+
return afterModeFilter;
|
|
1989
|
+
const nonProject = resolved.sources.filter((s) => s.source !== "project").map((s) => ({ values: s.settings }));
|
|
1990
|
+
const nonProjectLowestFirst = [...nonProject].reverse();
|
|
1991
|
+
const nextPermissions = { ...permissions };
|
|
1992
|
+
let changed = false;
|
|
1993
|
+
for (const key of PROJECT_PERMISSIVE_KEYS) {
|
|
1994
|
+
if (!(key in nextPermissions))
|
|
1995
|
+
continue;
|
|
1996
|
+
const withoutProject = unionRuleArray(nonProjectLowestFirst, key);
|
|
1997
|
+
const before = nextPermissions[key];
|
|
1998
|
+
if (withoutProject === undefined)
|
|
1999
|
+
delete nextPermissions[key];
|
|
2000
|
+
else
|
|
2001
|
+
nextPermissions[key] = withoutProject;
|
|
2002
|
+
if (JSON.stringify(before) !== JSON.stringify(withoutProject))
|
|
2003
|
+
changed = true;
|
|
2004
|
+
}
|
|
2005
|
+
return changed ? { ...afterModeFilter, permissions: nextPermissions } : afterModeFilter;
|
|
2006
|
+
}
|
|
2007
|
+
// src/settings/model-slots.ts
|
|
2008
|
+
import { CLAUDE_RESERVED_SLOT_NAMES, CURRENCY_RE, SLOT_NAME_RE } from "@yanlinglabs/winter-provider-catalog/families";
|
|
2009
|
+
var MAX_SLOTS = 4;
|
|
2010
|
+
var MAX_DESCRIPTION_LENGTH = 200;
|
|
2011
|
+
var EXTRA_CURRENCY_RE = /(?:[¥¢]\s?\d)|(?:\d\s?¢)|(?:\d+(?:\.\d+)?\s?(?:eur|euros?|cents?)\b)/i;
|
|
2012
|
+
function hasCurrencyAmount(text) {
|
|
2013
|
+
return CURRENCY_RE.test(text) || EXTRA_CURRENCY_RE.test(text);
|
|
2014
|
+
}
|
|
2015
|
+
function resolveCanonicalId(model, lookup) {
|
|
2016
|
+
const viaKey = lookup.keyToCanonicalId(model);
|
|
2017
|
+
if (viaKey !== undefined)
|
|
2018
|
+
return viaKey;
|
|
2019
|
+
if (lookup.rowsForCanonicalId(model).length > 0)
|
|
2020
|
+
return model;
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
function validateModelSlots(raw, lookup) {
|
|
2024
|
+
if (!Array.isArray(raw)) {
|
|
2025
|
+
return { ok: false, reason: `"modelSlots" must be an array, got ${raw === null ? "null" : typeof raw}` };
|
|
2026
|
+
}
|
|
2027
|
+
if (raw.length === 0) {
|
|
2028
|
+
return { ok: false, reason: `"modelSlots" must contain at least one entry (1–${MAX_SLOTS}, WS-13c §5)` };
|
|
2029
|
+
}
|
|
2030
|
+
if (raw.length > MAX_SLOTS) {
|
|
2031
|
+
return { ok: false, reason: `"modelSlots" must contain at most four entries (got ${raw.length})` };
|
|
2032
|
+
}
|
|
2033
|
+
const slots = [];
|
|
2034
|
+
const seenNames = new Set;
|
|
2035
|
+
for (let i = 0;i < raw.length; i++) {
|
|
2036
|
+
const entry = raw[i];
|
|
2037
|
+
const where = `modelSlots[${i}]`;
|
|
2038
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
2039
|
+
return { ok: false, reason: `${where} must be an object, got ${entry === null ? "null" : Array.isArray(entry) ? "an array" : typeof entry}` };
|
|
2040
|
+
}
|
|
2041
|
+
const record = entry;
|
|
2042
|
+
const { name, model, provider, description } = record;
|
|
2043
|
+
if (typeof name !== "string" || !SLOT_NAME_RE.test(name)) {
|
|
2044
|
+
return { ok: false, reason: `${where}.name must match the slot name grammar (${SLOT_NAME_RE.source}), got ${JSON.stringify(name)}` };
|
|
2045
|
+
}
|
|
2046
|
+
if (CLAUDE_RESERVED_SLOT_NAMES.includes(name)) {
|
|
2047
|
+
return { ok: false, reason: `${where}.name ${JSON.stringify(name)} is reserved to the claude family (${CLAUDE_RESERVED_SLOT_NAMES.join(", ")}) and cannot be used as a custom slot name` };
|
|
2048
|
+
}
|
|
2049
|
+
if (seenNames.has(name)) {
|
|
2050
|
+
return { ok: false, reason: `${where}.name ${JSON.stringify(name)} is a duplicate slot name` };
|
|
2051
|
+
}
|
|
2052
|
+
seenNames.add(name);
|
|
2053
|
+
if (typeof model !== "string" || model.length === 0) {
|
|
2054
|
+
return { ok: false, reason: `${where}.model must be a non-empty string, got ${JSON.stringify(model)}` };
|
|
2055
|
+
}
|
|
2056
|
+
const canonicalId = resolveCanonicalId(model, lookup);
|
|
2057
|
+
const rows = canonicalId !== undefined ? lookup.rowsForCanonicalId(canonicalId) : [];
|
|
2058
|
+
if (canonicalId === undefined || rows.length === 0) {
|
|
2059
|
+
return { ok: false, reason: `${where}.model ${JSON.stringify(model)} has no catalog row (not a known catalog key or canonical model id with a servable row)` };
|
|
2060
|
+
}
|
|
2061
|
+
if (provider !== undefined) {
|
|
2062
|
+
if (typeof provider !== "string" || provider.length === 0) {
|
|
2063
|
+
return { ok: false, reason: `${where}.provider must be a non-empty string, got ${JSON.stringify(provider)}` };
|
|
2064
|
+
}
|
|
2065
|
+
if (!rows.some((row) => row.providerId === provider)) {
|
|
2066
|
+
const servedBy = rows.map((row) => row.providerId);
|
|
2067
|
+
return { ok: false, reason: `${where}.provider ${JSON.stringify(provider)} does not serve ${JSON.stringify(model)} (served by: ${servedBy.length > 0 ? servedBy.join(", ") : "nobody"})` };
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
if (description !== undefined) {
|
|
2071
|
+
if (typeof description !== "string") {
|
|
2072
|
+
return { ok: false, reason: `${where}.description must be a string, got ${typeof description}` };
|
|
2073
|
+
}
|
|
2074
|
+
if (description.length > MAX_DESCRIPTION_LENGTH) {
|
|
2075
|
+
return { ok: false, reason: `${where}.description exceeds ${MAX_DESCRIPTION_LENGTH} characters (got ${description.length})` };
|
|
2076
|
+
}
|
|
2077
|
+
if (hasCurrencyAmount(description)) {
|
|
2078
|
+
return { ok: false, reason: `${where}.description must not carry a currency amount (pricing lives on catalog rows), got ${JSON.stringify(description)}` };
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
slots.push({
|
|
2082
|
+
name,
|
|
2083
|
+
model,
|
|
2084
|
+
...provider !== undefined ? { provider } : {},
|
|
2085
|
+
...description !== undefined ? { description } : {}
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
return { ok: true, slots };
|
|
2089
|
+
}
|
|
2090
|
+
// src/permissions/types.ts
|
|
2091
|
+
var HOOK_EVENTS = [
|
|
2092
|
+
"PreToolUse",
|
|
2093
|
+
"PostToolUse",
|
|
2094
|
+
"PostToolUseFailure",
|
|
2095
|
+
"UserPromptSubmit",
|
|
2096
|
+
"Stop",
|
|
2097
|
+
"SubagentStart",
|
|
2098
|
+
"SubagentStop",
|
|
2099
|
+
"PreCompact",
|
|
2100
|
+
"PermissionRequest",
|
|
2101
|
+
"Notification",
|
|
2102
|
+
"PostToolBatch",
|
|
2103
|
+
"UserPromptExpansion",
|
|
2104
|
+
"MessageDisplay",
|
|
2105
|
+
"StopFailure",
|
|
2106
|
+
"PostCompact",
|
|
2107
|
+
"PermissionDenied",
|
|
2108
|
+
"SessionStart",
|
|
2109
|
+
"SessionEnd",
|
|
2110
|
+
"Setup",
|
|
2111
|
+
"TeammateIdle",
|
|
2112
|
+
"TaskCreated",
|
|
2113
|
+
"TaskCompleted",
|
|
2114
|
+
"Elicitation",
|
|
2115
|
+
"ElicitationResult",
|
|
2116
|
+
"ConfigChange",
|
|
2117
|
+
"InstructionsLoaded",
|
|
2118
|
+
"WorktreeCreate",
|
|
2119
|
+
"WorktreeRemove",
|
|
2120
|
+
"CwdChanged",
|
|
2121
|
+
"FileChanged",
|
|
2122
|
+
"DirectoryAdded"
|
|
2123
|
+
];
|
|
2124
|
+
export {
|
|
2125
|
+
AbortError,
|
|
2126
|
+
BRAND_TOKEN_RE,
|
|
2127
|
+
CLIConnectionError,
|
|
2128
|
+
DEFAULT_COMPACTION_THRESHOLD,
|
|
2129
|
+
DEFAULT_CONTEXT_WINDOW_TOKENS,
|
|
2130
|
+
DEFAULT_KEYCHAIN_SERVICE,
|
|
2131
|
+
DEFAULT_OUTPUT_STYLE,
|
|
2132
|
+
DEFAULT_PLANS_DIRECTORY,
|
|
2133
|
+
DEFAULT_PROVIDER_STALL_TIMEOUT_MS,
|
|
2134
|
+
DIALECT_RECORD_ENTRY_TYPE,
|
|
2135
|
+
ESCALATING_PERMISSION_MODES,
|
|
2136
|
+
FIRST_PARTY_ORIGINATORS,
|
|
2137
|
+
HOOK_EVENTS,
|
|
2138
|
+
InvalidBrandError,
|
|
2139
|
+
OVERLAY_NEVER_KEYS,
|
|
2140
|
+
PROJECT_PERMISSIVE_KEYS,
|
|
2141
|
+
PROTOCOL_VERSION,
|
|
2142
|
+
PROVIDER_STATE_FILE_SUFFIX,
|
|
2143
|
+
ProcessError,
|
|
2144
|
+
ProtocolDecodeError,
|
|
2145
|
+
ProtocolError,
|
|
2146
|
+
ResultError,
|
|
2147
|
+
SETTING_SOURCES,
|
|
2148
|
+
SYSTEM_PROMPT_DYNAMIC_BOUNDARY,
|
|
2149
|
+
SessionNotFoundError,
|
|
2150
|
+
WINTER_BRAND,
|
|
2151
|
+
WinterCompatibilitySessionStore,
|
|
2152
|
+
WinterRpcError,
|
|
2153
|
+
WinterRpcTimeoutError,
|
|
2154
|
+
WinterSDKError,
|
|
2155
|
+
WinterStoreError,
|
|
2156
|
+
WinterStoreLeaseError,
|
|
2157
|
+
applyWorkspaceTrust,
|
|
2158
|
+
compatibilityKeys,
|
|
2159
|
+
decodeFrame,
|
|
2160
|
+
defaultSpawn,
|
|
2161
|
+
deleteSession,
|
|
2162
|
+
encodeFrame,
|
|
2163
|
+
envName,
|
|
2164
|
+
filterEscalatingDefaultMode,
|
|
2165
|
+
forkSession,
|
|
2166
|
+
forkSessionByKey,
|
|
2167
|
+
getSessionInfo,
|
|
2168
|
+
getSessionMessages,
|
|
2169
|
+
getSubagentMessages,
|
|
2170
|
+
isUnset,
|
|
2171
|
+
isWinterMcpServerInstance,
|
|
2172
|
+
listSessions,
|
|
2173
|
+
listSubagents,
|
|
2174
|
+
loadSettingsFile,
|
|
2175
|
+
mcpToolName,
|
|
2176
|
+
providerSettingsFrom,
|
|
2177
|
+
query,
|
|
2178
|
+
renameSession,
|
|
2179
|
+
resolveBrand,
|
|
2180
|
+
resolveKeychainServiceForProfile,
|
|
2181
|
+
resolveRuntimeExecutable,
|
|
2182
|
+
resolveSettings,
|
|
2183
|
+
resolveSettingsDetailed,
|
|
2184
|
+
resolveWinterHome,
|
|
2185
|
+
settingsPathFor,
|
|
2186
|
+
splitFrames,
|
|
2187
|
+
tagSession,
|
|
2188
|
+
transcriptProjectKey,
|
|
2189
|
+
userAgent,
|
|
2190
|
+
validateModelSlots
|
|
2191
|
+
};
|