@hadooppei/hwcode 0.1.0 → 0.2.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.
@@ -0,0 +1,263 @@
1
+ export type CloudVendorId = "aws" | "azure" | "gcp" | "huawei" | "alibaba" | "tencent";
2
+ export type CloudOperation = "read" | "change" | "delete";
3
+
4
+ export interface CredentialField {
5
+ key: string;
6
+ label: string;
7
+ secret?: boolean;
8
+ optional?: boolean;
9
+ fileContents?: boolean;
10
+ placeholder?: string;
11
+ }
12
+
13
+ export interface CloudProvider {
14
+ id: CloudVendorId;
15
+ label: string;
16
+ cli: string;
17
+ versionArgs: string[];
18
+ credentialFields: CredentialField[];
19
+ }
20
+
21
+ export type CloudCredentials = Record<string, string>;
22
+
23
+ export const CLOUD_PROVIDERS: readonly CloudProvider[] = [
24
+ {
25
+ id: "aws",
26
+ label: "Amazon Web Services (AWS)",
27
+ cli: "aws",
28
+ versionArgs: ["--version"],
29
+ credentialFields: [
30
+ { key: "accessKeyId", label: "Access Key ID" },
31
+ { key: "secretAccessKey", label: "Secret Access Key", secret: true },
32
+ { key: "sessionToken", label: "Session Token(临时凭据可选)", secret: true, optional: true },
33
+ { key: "region", label: "默认 Region", placeholder: "us-east-1" },
34
+ ],
35
+ },
36
+ {
37
+ id: "azure",
38
+ label: "Microsoft Azure",
39
+ cli: "az",
40
+ versionArgs: ["version"],
41
+ credentialFields: [
42
+ { key: "tenantId", label: "Tenant ID" },
43
+ { key: "subscriptionId", label: "Subscription ID" },
44
+ { key: "clientId", label: "Service Principal Client ID" },
45
+ { key: "clientSecret", label: "Service Principal Client Secret", secret: true },
46
+ ],
47
+ },
48
+ {
49
+ id: "gcp",
50
+ label: "Google Cloud",
51
+ cli: "gcloud",
52
+ versionArgs: ["version"],
53
+ credentialFields: [
54
+ { key: "serviceAccountJson", label: "Service Account JSON 文件路径", fileContents: true },
55
+ { key: "projectId", label: "Project ID(留空则读取 JSON)", optional: true },
56
+ ],
57
+ },
58
+ {
59
+ id: "huawei",
60
+ label: "华为云 (Huawei Cloud)",
61
+ cli: "hcloud",
62
+ versionArgs: ["version"],
63
+ credentialFields: [
64
+ { key: "accessKey", label: "Access Key (AK)" },
65
+ { key: "secretKey", label: "Secret Access Key (SK)", secret: true },
66
+ { key: "securityToken", label: "Security Token(临时凭据可选)", secret: true, optional: true },
67
+ { key: "region", label: "Region", placeholder: "cn-north-4" },
68
+ { key: "projectId", label: "Project ID(可选)", optional: true },
69
+ { key: "domainId", label: "Domain ID(全局服务可选)", optional: true },
70
+ ],
71
+ },
72
+ {
73
+ id: "alibaba",
74
+ label: "阿里云 (Alibaba Cloud)",
75
+ cli: "aliyun",
76
+ versionArgs: ["version"],
77
+ credentialFields: [
78
+ { key: "accessKeyId", label: "AccessKey ID" },
79
+ { key: "accessKeySecret", label: "AccessKey Secret", secret: true },
80
+ { key: "securityToken", label: "STS Security Token(可选)", secret: true, optional: true },
81
+ { key: "region", label: "默认 Region", placeholder: "cn-hangzhou" },
82
+ ],
83
+ },
84
+ {
85
+ id: "tencent",
86
+ label: "腾讯云 (Tencent Cloud)",
87
+ cli: "tccli",
88
+ versionArgs: ["version"],
89
+ credentialFields: [
90
+ { key: "secretId", label: "SecretId" },
91
+ { key: "secretKey", label: "SecretKey", secret: true },
92
+ { key: "token", label: "临时凭据 Token(可选)", secret: true, optional: true },
93
+ { key: "region", label: "默认 Region", placeholder: "ap-guangzhou" },
94
+ ],
95
+ },
96
+ ] as const;
97
+
98
+ const PROVIDER_BY_ID = new Map(CLOUD_PROVIDERS.map((provider) => [provider.id, provider]));
99
+ const OPERATION_RANK: Record<CloudOperation, number> = { read: 0, change: 1, delete: 2 };
100
+
101
+ const DELETE_PATTERNS = [
102
+ /^(delete|destroy|terminate|remove|revoke|purge|uninstall|decommission)/u,
103
+ /^(delete|destroy|terminate|remove|revoke|purge|uninstall)[-_]/u,
104
+ ];
105
+ const CHANGE_PATTERNS = [
106
+ /^(create|update|set|apply|deploy|put|add|attach|detach|start|stop|restart|scale|patch|replace|run|launch|authorize|import|move|copy|enable|disable|install|upgrade)/u,
107
+ /^(create|update|set|apply|deploy|put|add|attach|detach|start|stop|restart|scale|patch|replace|run|launch|authorize|import|move|copy|enable|disable|install|upgrade)[-_]/u,
108
+ ];
109
+
110
+ export const CLOUD_EXECUTABLES = new Set([
111
+ ...CLOUD_PROVIDERS.map((provider) => provider.cli),
112
+ "terraform",
113
+ "tofu",
114
+ "pulumi",
115
+ "kubectl",
116
+ "helm",
117
+ ]);
118
+
119
+ export function getCloudProvider(id: CloudVendorId): CloudProvider {
120
+ const provider = PROVIDER_BY_ID.get(id);
121
+ if (!provider) throw new Error(`Unsupported cloud provider: ${id}`);
122
+ return provider;
123
+ }
124
+
125
+ export function missingCloudCliMessage(id: CloudVendorId): string {
126
+ const provider = getCloudProvider(id);
127
+ const base = `未找到 ${provider.label} CLI 可执行文件 \"${provider.cli}\"。请先安装它并确保其位于 PATH 中,然后重新启动 HWCode。`;
128
+ if (id !== "huawei") return `${base}\n可先在终端运行 \`${provider.cli} ${provider.versionArgs.join(" ")}\` 验证安装。`;
129
+ return [
130
+ base,
131
+ "华为云官方工具名称为 KooCLI;macOS 安装文档:https://support.huaweicloud.com/qs-hcli/hcli_02_003_03.html",
132
+ "安装后请先在终端运行 `hcloud version`,确认成功后再执行 `/hwcode-cloud`。",
133
+ ].join("\n");
134
+ }
135
+
136
+ export function inaccessibleCloudCliMessage(id: CloudVendorId): string {
137
+ const provider = getCloudProvider(id);
138
+ const base = `无法执行 ${provider.label} CLI \"${provider.cli}\":文件存在,但当前用户没有执行权限。`;
139
+ if (id !== "huawei") return `${base}\n请检查 \`command -v ${provider.cli}\` 返回文件的所有者和执行权限。`;
140
+ return [
141
+ base,
142
+ "如果 KooCLI 位于 /usr/local/bin/hcloud,请在系统终端执行:",
143
+ "`sudo chmod 755 /usr/local/bin/hcloud`",
144
+ "然后运行 `hcloud version` 验证,再重新执行 `/hwcode-cloud`。",
145
+ ].join("\n");
146
+ }
147
+
148
+ export function isCloudVendorId(value: string): value is CloudVendorId {
149
+ return PROVIDER_BY_ID.has(value as CloudVendorId);
150
+ }
151
+
152
+ export function cloudEnvironment(vendor: CloudVendorId, credentials: CloudCredentials): Record<string, string> {
153
+ switch (vendor) {
154
+ case "aws":
155
+ return compactEnvironment({
156
+ AWS_ACCESS_KEY_ID: credentials.accessKeyId,
157
+ AWS_SECRET_ACCESS_KEY: credentials.secretAccessKey,
158
+ AWS_SESSION_TOKEN: credentials.sessionToken,
159
+ AWS_DEFAULT_REGION: credentials.region,
160
+ AWS_REGION: credentials.region,
161
+ });
162
+ case "azure":
163
+ return compactEnvironment({
164
+ AZURE_TENANT_ID: credentials.tenantId,
165
+ AZURE_SUBSCRIPTION_ID: credentials.subscriptionId,
166
+ AZURE_CLIENT_ID: credentials.clientId,
167
+ AZURE_CLIENT_SECRET: credentials.clientSecret,
168
+ ARM_TENANT_ID: credentials.tenantId,
169
+ ARM_SUBSCRIPTION_ID: credentials.subscriptionId,
170
+ ARM_CLIENT_ID: credentials.clientId,
171
+ ARM_CLIENT_SECRET: credentials.clientSecret,
172
+ });
173
+ case "gcp":
174
+ return compactEnvironment({
175
+ GOOGLE_CLOUD_PROJECT: credentials.projectId,
176
+ CLOUDSDK_CORE_PROJECT: credentials.projectId,
177
+ });
178
+ case "huawei":
179
+ return compactEnvironment({
180
+ HW_ACCESS_KEY: credentials.accessKey,
181
+ HW_SECRET_KEY: credentials.secretKey,
182
+ HW_REGION_NAME: credentials.region,
183
+ HW_PROJECT_ID: credentials.projectId,
184
+ HW_DOMAIN_ID: credentials.domainId,
185
+ HUAWEICLOUD_SDK_AK: credentials.accessKey,
186
+ HUAWEICLOUD_SDK_SK: credentials.secretKey,
187
+ HUAWEICLOUD_SDK_SECURITY_TOKEN: credentials.securityToken,
188
+ });
189
+ case "alibaba":
190
+ return compactEnvironment({
191
+ ALIBABA_CLOUD_ACCESS_KEY_ID: credentials.accessKeyId,
192
+ ALIBABA_CLOUD_ACCESS_KEY_SECRET: credentials.accessKeySecret,
193
+ ALIBABA_CLOUD_SECURITY_TOKEN: credentials.securityToken,
194
+ ALIBABA_CLOUD_REGION_ID: credentials.region,
195
+ ALIBABA_CLOUD_IGNORE_PROFILE: "TRUE",
196
+ });
197
+ case "tencent":
198
+ return compactEnvironment({
199
+ TENCENTCLOUD_SECRET_ID: credentials.secretId,
200
+ TENCENTCLOUD_SECRET_KEY: credentials.secretKey,
201
+ TENCENTCLOUD_TOKEN: credentials.token,
202
+ TENCENTCLOUD_REGION: credentials.region,
203
+ });
204
+ }
205
+ }
206
+
207
+ export function classifyCloudOperation(
208
+ command: string,
209
+ args: readonly string[],
210
+ requested: CloudOperation,
211
+ ): CloudOperation {
212
+ let detected = requested;
213
+ for (const token of [command, ...args]) {
214
+ const normalized = token.toLowerCase().replace(/[^a-z0-9_-]/gu, "");
215
+ if (DELETE_PATTERNS.some((pattern) => pattern.test(normalized))) return "delete";
216
+ if (CHANGE_PATTERNS.some((pattern) => pattern.test(normalized))
217
+ && OPERATION_RANK[detected] < OPERATION_RANK.change) detected = "change";
218
+ }
219
+ return detected;
220
+ }
221
+
222
+ export function containsCloudCommand(shellCommand: string): boolean {
223
+ const executablePattern = [...CLOUD_EXECUTABLES]
224
+ .map((name) => name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"))
225
+ .join("|");
226
+ return new RegExp(`(?:^|[;&|()\\s])(?:[^\\s;&|()]+/)?(?:${executablePattern})(?=$|\\s)`, "u").test(shellCommand);
227
+ }
228
+
229
+ export function redactCredentialValues(text: string, credentials: CloudCredentials): string {
230
+ let redacted = text;
231
+ const sensitiveValues = new Set(Object.values(credentials));
232
+ for (const value of Object.values(credentials)) {
233
+ if (!value.trim().startsWith("{")) continue;
234
+ try {
235
+ collectStringValues(JSON.parse(value), sensitiveValues);
236
+ } catch {
237
+ // Non-JSON credential values are already included verbatim.
238
+ }
239
+ }
240
+ for (const value of [...sensitiveValues].sort((left, right) => right.length - left.length)) {
241
+ if (value.length < 4) continue;
242
+ redacted = redacted.split(value).join("[REDACTED]");
243
+ }
244
+ return redacted;
245
+ }
246
+
247
+ function collectStringValues(value: unknown, output: Set<string>): void {
248
+ if (typeof value === "string") {
249
+ output.add(value);
250
+ return;
251
+ }
252
+ if (Array.isArray(value)) {
253
+ for (const entry of value) collectStringValues(entry, output);
254
+ return;
255
+ }
256
+ if (value && typeof value === "object") {
257
+ for (const entry of Object.values(value)) collectStringValues(entry, output);
258
+ }
259
+ }
260
+
261
+ function compactEnvironment(values: Record<string, string | undefined>): Record<string, string> {
262
+ return Object.fromEntries(Object.entries(values).filter((entry): entry is [string, string] => Boolean(entry[1])));
263
+ }
@@ -0,0 +1,209 @@
1
+ import {
2
+ createCipheriv,
3
+ createDecipheriv,
4
+ randomBytes,
5
+ randomUUID,
6
+ scryptSync,
7
+ } from "node:crypto";
8
+ import {
9
+ chmodSync,
10
+ existsSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ renameSync,
14
+ writeFileSync,
15
+ } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { dirname, join } from "node:path";
18
+
19
+ import { isCloudVendorId, type CloudCredentials, type CloudVendorId } from "./cloud-providers.ts";
20
+
21
+ const AAD = Buffer.from("hwcode-cloud-credentials-v1", "utf8");
22
+ const KEY_LENGTH = 32;
23
+ const SCRYPT_OPTIONS = { N: 16_384, r: 8, p: 1, maxmem: 64 * 1024 * 1024 } as const;
24
+
25
+ export interface CloudCredentialProfile {
26
+ id: string;
27
+ credentials: CloudCredentials;
28
+ createdAt: string;
29
+ updatedAt: string;
30
+ }
31
+
32
+ export interface VaultPayload {
33
+ version: 2;
34
+ providers: Partial<Record<CloudVendorId, CloudCredentialProfile[]>>;
35
+ }
36
+
37
+ interface EncryptedVault {
38
+ version: 1;
39
+ kdf: "scrypt";
40
+ cipher: "aes-256-gcm";
41
+ salt: string;
42
+ iv: string;
43
+ tag: string;
44
+ ciphertext: string;
45
+ }
46
+
47
+ export function defaultCloudVaultPath(home = homedir()): string {
48
+ return join(home, ".hwcode", "cloud", "credentials.enc");
49
+ }
50
+
51
+ export function cloudVaultExists(path = defaultCloudVaultPath()): boolean {
52
+ return existsSync(path);
53
+ }
54
+
55
+ export function createEmptyVault(): VaultPayload {
56
+ return { version: 2, providers: {} };
57
+ }
58
+
59
+ function cloneCredentials(value: unknown): CloudCredentials | undefined {
60
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
61
+ const entries = Object.entries(value);
62
+ if (entries.some(([, field]) => typeof field !== "string")) return undefined;
63
+ return Object.fromEntries(entries) as CloudCredentials;
64
+ }
65
+
66
+ export function normalizeCloudVaultPayload(value: unknown): VaultPayload {
67
+ if (!value || typeof value !== "object") throw new Error("invalid vault payload");
68
+ const payload = value as Record<string, unknown>;
69
+ if (!payload.providers || typeof payload.providers !== "object" || Array.isArray(payload.providers)) {
70
+ throw new Error("invalid vault payload");
71
+ }
72
+ const providers: VaultPayload["providers"] = {};
73
+ for (const [vendor, stored] of Object.entries(payload.providers)) {
74
+ if (!isCloudVendorId(vendor)) throw new Error("unsupported cloud provider in vault");
75
+ if (payload.version === 1) {
76
+ const credentials = cloneCredentials(stored);
77
+ if (!credentials) throw new Error("invalid legacy credential profile");
78
+ providers[vendor] = [{
79
+ id: `legacy-${vendor}`,
80
+ credentials,
81
+ createdAt: "1970-01-01T00:00:00.000Z",
82
+ updatedAt: "1970-01-01T00:00:00.000Z",
83
+ }];
84
+ continue;
85
+ }
86
+ if (payload.version !== 2 || !Array.isArray(stored)) throw new Error("unsupported vault payload");
87
+ providers[vendor] = stored.map((profile) => {
88
+ if (!profile || typeof profile !== "object") throw new Error("invalid credential profile");
89
+ const data = profile as Record<string, unknown>;
90
+ const credentials = cloneCredentials(data.credentials);
91
+ if (typeof data.id !== "string" || !data.id || !credentials
92
+ || typeof data.createdAt !== "string" || typeof data.updatedAt !== "string") {
93
+ throw new Error("invalid credential profile");
94
+ }
95
+ return { id: data.id, credentials, createdAt: data.createdAt, updatedAt: data.updatedAt };
96
+ });
97
+ }
98
+ if (payload.version !== 1 && payload.version !== 2) throw new Error("unsupported vault payload");
99
+ return { version: 2, providers };
100
+ }
101
+
102
+ export function readCloudVault(password: string, path = defaultCloudVaultPath()): VaultPayload {
103
+ try {
104
+ const envelope = JSON.parse(readFileSync(path, "utf8")) as EncryptedVault;
105
+ if (envelope.version !== 1 || envelope.kdf !== "scrypt" || envelope.cipher !== "aes-256-gcm") {
106
+ throw new Error("unsupported vault format");
107
+ }
108
+ const salt = Buffer.from(envelope.salt, "base64");
109
+ const key = scryptSync(password, salt, KEY_LENGTH, SCRYPT_OPTIONS);
110
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(envelope.iv, "base64"));
111
+ decipher.setAAD(AAD);
112
+ decipher.setAuthTag(Buffer.from(envelope.tag, "base64"));
113
+ const plaintext = Buffer.concat([
114
+ decipher.update(Buffer.from(envelope.ciphertext, "base64")),
115
+ decipher.final(),
116
+ ]);
117
+ return normalizeCloudVaultPayload(JSON.parse(plaintext.toString("utf8")));
118
+ } catch {
119
+ throw new Error("无法解锁云凭据:主密码错误或凭据文件已损坏");
120
+ }
121
+ }
122
+
123
+ export function writeCloudVault(payload: VaultPayload, password: string, path = defaultCloudVaultPath()): void {
124
+ const directory = dirname(path);
125
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
126
+ chmodSync(directory, 0o700);
127
+
128
+ const salt = randomBytes(16);
129
+ const iv = randomBytes(12);
130
+ const key = scryptSync(password, salt, KEY_LENGTH, SCRYPT_OPTIONS);
131
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
132
+ cipher.setAAD(AAD);
133
+ const ciphertext = Buffer.concat([
134
+ cipher.update(JSON.stringify(payload), "utf8"),
135
+ cipher.final(),
136
+ ]);
137
+ const envelope: EncryptedVault = {
138
+ version: 1,
139
+ kdf: "scrypt",
140
+ cipher: "aes-256-gcm",
141
+ salt: salt.toString("base64"),
142
+ iv: iv.toString("base64"),
143
+ tag: cipher.getAuthTag().toString("base64"),
144
+ ciphertext: ciphertext.toString("base64"),
145
+ };
146
+
147
+ const temporaryPath = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
148
+ writeFileSync(temporaryPath, `${JSON.stringify(envelope, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
149
+ chmodSync(temporaryPath, 0o600);
150
+ renameSync(temporaryPath, path);
151
+ chmodSync(path, 0o600);
152
+ }
153
+
154
+ export function setCloudCredentials(
155
+ payload: VaultPayload,
156
+ vendor: CloudVendorId,
157
+ credentials: CloudCredentials,
158
+ ): VaultPayload {
159
+ const first = payload.providers[vendor]?.[0];
160
+ return saveCloudCredentialProfile(payload, vendor, credentials, first?.id);
161
+ }
162
+
163
+ export function getCloudCredentials(
164
+ payload: VaultPayload,
165
+ vendor: CloudVendorId,
166
+ ): CloudCredentials | undefined {
167
+ const credentials = payload.providers[vendor]?.[0]?.credentials;
168
+ return credentials ? { ...credentials } : undefined;
169
+ }
170
+
171
+ export function listCloudCredentialProfiles(
172
+ payload: VaultPayload,
173
+ vendor: CloudVendorId,
174
+ ): CloudCredentialProfile[] {
175
+ return (payload.providers[vendor] ?? []).map((profile) => ({
176
+ ...profile,
177
+ credentials: { ...profile.credentials },
178
+ }));
179
+ }
180
+
181
+ export function saveCloudCredentialProfile(
182
+ payload: VaultPayload,
183
+ vendor: CloudVendorId,
184
+ credentials: CloudCredentials,
185
+ profileId?: string,
186
+ ): VaultPayload {
187
+ const now = new Date().toISOString();
188
+ const profiles = listCloudCredentialProfiles(payload, vendor);
189
+ const existingIndex = profileId ? profiles.findIndex((profile) => profile.id === profileId) : -1;
190
+ const profile: CloudCredentialProfile = {
191
+ id: existingIndex >= 0 ? profiles[existingIndex].id : randomUUID(),
192
+ credentials: { ...credentials },
193
+ createdAt: existingIndex >= 0 ? profiles[existingIndex].createdAt : now,
194
+ updatedAt: now,
195
+ };
196
+ if (existingIndex >= 0) profiles[existingIndex] = profile;
197
+ else profiles.push(profile);
198
+ return { version: 2, providers: { ...payload.providers, [vendor]: profiles } };
199
+ }
200
+
201
+ export function cloudCredentialProfileLabel(
202
+ profile: CloudCredentialProfile,
203
+ index: number,
204
+ total: number,
205
+ ): string {
206
+ const number = total > 1 ? ` ${index + 1}` : "";
207
+ const region = profile.credentials.region?.trim() || "未配置";
208
+ return `使用已有凭据${number} · Region: ${region}`;
209
+ }
@@ -13,6 +13,7 @@ export const DEFAULT_HIDDEN_COMMANDS = [
13
13
  "llama",
14
14
  "skill:hwcode-vibe",
15
15
  "skill:hwcode-sdd",
16
+ "skill:hwcode-cloud",
16
17
  ] as const;
17
18
 
18
19
  interface ProjectSettings {
@@ -0,0 +1,113 @@
1
+ import {
2
+ estimateTokens,
3
+ findCutPoint,
4
+ sessionEntryToContextMessages,
5
+ type SessionBeforeCompactEvent,
6
+ type SessionEntry,
7
+ } from "@earendil-works/pi-coding-agent";
8
+
9
+ import { calculateCompactionBudgets, type ContextPolicy } from "../context-policy.ts";
10
+
11
+ function entryMessage(entry: SessionEntry) {
12
+ if (entry.type === "compaction") return undefined;
13
+ return sessionEntryToContextMessages(entry)[0];
14
+ }
15
+
16
+ function addFileOperations(
17
+ message: ReturnType<typeof sessionEntryToContextMessages>[number],
18
+ fileOps: SessionBeforeCompactEvent["preparation"]["fileOps"],
19
+ ): void {
20
+ if (message.role !== "assistant" || !Array.isArray(message.content)) return;
21
+ for (const block of message.content) {
22
+ if (block.type !== "toolCall") continue;
23
+ const path = typeof block.arguments?.path === "string" ? block.arguments.path : undefined;
24
+ if (!path) continue;
25
+ if (block.name === "read") fileOps.read.add(path);
26
+ if (block.name === "write") fileOps.written.add(path);
27
+ if (block.name === "edit") fileOps.edited.add(path);
28
+ }
29
+ }
30
+
31
+ function prepareWithKeepBudget(
32
+ event: SessionBeforeCompactEvent,
33
+ reserveTokens: number,
34
+ keepRecentTokens: number,
35
+ ): SessionBeforeCompactEvent["preparation"] | undefined {
36
+ const entries = event.branchEntries;
37
+ let previousCompactionIndex = -1;
38
+ for (let index = entries.length - 1; index >= 0; index--) {
39
+ if (entries[index].type === "compaction") { previousCompactionIndex = index; break; }
40
+ }
41
+
42
+ let boundaryStart = 0;
43
+ if (previousCompactionIndex >= 0) {
44
+ const previousCompaction = entries[previousCompactionIndex];
45
+ if (previousCompaction.type === "compaction") {
46
+ const previousFirstKept = entries.findIndex((entry) => entry.id === previousCompaction.firstKeptEntryId);
47
+ boundaryStart = previousFirstKept >= 0 ? previousFirstKept : previousCompactionIndex + 1;
48
+ }
49
+ }
50
+
51
+ const cutPoint = findCutPoint(entries, boundaryStart, entries.length, keepRecentTokens);
52
+ const firstKeptEntry = entries[cutPoint.firstKeptEntryIndex];
53
+ if (!firstKeptEntry?.id) return undefined;
54
+ const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
55
+ const messagesToSummarize = entries.slice(boundaryStart, historyEnd)
56
+ .map(entryMessage).filter((message): message is NonNullable<typeof message> => Boolean(message));
57
+ const turnPrefixMessages = cutPoint.isSplitTurn
58
+ ? entries.slice(cutPoint.turnStartIndex, cutPoint.firstKeptEntryIndex)
59
+ .map(entryMessage).filter((message): message is NonNullable<typeof message> => Boolean(message))
60
+ : [];
61
+ if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) return undefined;
62
+
63
+ const fileOps = {
64
+ read: new Set(event.preparation.fileOps.read),
65
+ written: new Set(event.preparation.fileOps.written),
66
+ edited: new Set(event.preparation.fileOps.edited),
67
+ };
68
+ for (const message of [...messagesToSummarize, ...turnPrefixMessages]) addFileOperations(message, fileOps);
69
+ return {
70
+ ...event.preparation,
71
+ firstKeptEntryId: firstKeptEntry.id,
72
+ messagesToSummarize,
73
+ turnPrefixMessages,
74
+ isSplitTurn: cutPoint.isSplitTurn,
75
+ fileOps,
76
+ settings: { ...event.preparation.settings, reserveTokens, keepRecentTokens },
77
+ };
78
+ }
79
+
80
+ function estimateKeptTokens(entries: SessionEntry[], firstKeptEntryId: string): number {
81
+ const firstKeptIndex = entries.findIndex((entry) => entry.id === firstKeptEntryId);
82
+ if (firstKeptIndex < 0) return Number.POSITIVE_INFINITY;
83
+ return entries.slice(firstKeptIndex).reduce(
84
+ (total, entry) => total + sessionEntryToContextMessages(entry)
85
+ .reduce((entryTotal, message) => entryTotal + estimateTokens(message), 0),
86
+ 0,
87
+ );
88
+ }
89
+
90
+ export function tightenPreparation(
91
+ event: SessionBeforeCompactEvent,
92
+ policy: ContextPolicy,
93
+ contextWindow: number,
94
+ systemPrompt: string,
95
+ ): { estimatedTokensAfter: number; targetTokens: number } | undefined {
96
+ const budgets = calculateCompactionBudgets(policy, contextWindow, systemPrompt);
97
+ let keepRecentTokens = budgets.keepRecentTokens;
98
+ let selected = prepareWithKeepBudget(event, budgets.reserveTokens, keepRecentTokens);
99
+ if (!selected) return undefined;
100
+
101
+ let keptTokens = estimateKeptTokens(event.branchEntries, selected.firstKeptEntryId);
102
+ for (let attempt = 0; keptTokens > budgets.keepRecentTokens && keepRecentTokens > 1 && attempt < 16; attempt++) {
103
+ const next = Math.max(1, keepRecentTokens - (keptTokens - budgets.keepRecentTokens) - 1);
104
+ if (next === keepRecentTokens) break;
105
+ const candidate = prepareWithKeepBudget(event, budgets.reserveTokens, next);
106
+ if (!candidate) break;
107
+ selected = candidate;
108
+ keepRecentTokens = next;
109
+ keptTokens = estimateKeptTokens(event.branchEntries, selected.firstKeptEntryId);
110
+ }
111
+ Object.assign(event.preparation, selected);
112
+ return { estimatedTokensAfter: keptTokens + budgets.overheadTokens, targetTokens: budgets.targetTokens };
113
+ }