tinker-agent 2.5.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/README.md +30 -7
- package/package.json +1 -1
- package/src/cli/main.ts +2 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/tui-runner.tsx +2 -0
- package/src/session/session-clone-helpers.ts +249 -0
- package/src/session/session-compatibility-codec.ts +401 -0
- package/src/session/session-store-contracts.ts +304 -0
- package/src/session/session-store-filesystem.ts +245 -0
- package/src/session/session-store-record-codecs.ts +1064 -0
- package/src/session/session-store-value-codecs.ts +156 -0
- package/src/session/session-store.ts +234 -3023
- package/src/session/session-tool-result-codec.ts +590 -0
- package/src/tools/grep.ts +1 -3
- package/src/tui/app.tsx +2 -0
- package/src/tui/components/prompt-input.tsx +4 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parseMessageId, type MessageId } from "../ids/runtime-id";
|
|
3
|
+
import { sha256 } from "../model/model-request-preflight";
|
|
4
|
+
import type { ToolRawResult } from "../tools/types";
|
|
5
|
+
import { immutableCanonicalClone, immutableRecord } from "../context/protocol-frame";
|
|
6
|
+
import {
|
|
7
|
+
parseImageAssetId,
|
|
8
|
+
validateImageAssetRef,
|
|
9
|
+
validateOriginalImageName,
|
|
10
|
+
type ImageAssetRef,
|
|
11
|
+
} from "../image/image-types";
|
|
12
|
+
import {
|
|
13
|
+
SKILL_FILE_MAX_BYTES,
|
|
14
|
+
SKILL_RESOURCE_MAX_DEPTH,
|
|
15
|
+
SKILL_RESOURCE_MAX_ENTRIES,
|
|
16
|
+
} from "../skills/skill-loader";
|
|
17
|
+
import {
|
|
18
|
+
assertObjectKeys,
|
|
19
|
+
enumFromSql,
|
|
20
|
+
nonEmptyStringFromJson,
|
|
21
|
+
nonNegativeJsonInteger,
|
|
22
|
+
numberFromJson,
|
|
23
|
+
positiveJsonInteger,
|
|
24
|
+
recordFromSql,
|
|
25
|
+
sha256FromSql,
|
|
26
|
+
stringFromSql,
|
|
27
|
+
} from "./session-store-value-codecs";
|
|
28
|
+
|
|
29
|
+
export function decodeStoredToolRawResult(value: unknown): ToolRawResult {
|
|
30
|
+
const raw = recordFromSql(value, "tool raw result");
|
|
31
|
+
const kind = enumFromSql(
|
|
32
|
+
raw.kind,
|
|
33
|
+
[
|
|
34
|
+
"read",
|
|
35
|
+
"view_image",
|
|
36
|
+
"write",
|
|
37
|
+
"edit",
|
|
38
|
+
"delete",
|
|
39
|
+
"glob",
|
|
40
|
+
"grep",
|
|
41
|
+
"bash",
|
|
42
|
+
"update_plan",
|
|
43
|
+
"task_list",
|
|
44
|
+
"task_output",
|
|
45
|
+
"task_input",
|
|
46
|
+
"task_stop",
|
|
47
|
+
"web_search",
|
|
48
|
+
"web_fetch",
|
|
49
|
+
"recall",
|
|
50
|
+
"context_maintenance",
|
|
51
|
+
"memory_search",
|
|
52
|
+
"memory_get",
|
|
53
|
+
"wait",
|
|
54
|
+
"skill",
|
|
55
|
+
"mcp",
|
|
56
|
+
"generic",
|
|
57
|
+
] as const,
|
|
58
|
+
"tool raw result kind",
|
|
59
|
+
);
|
|
60
|
+
if (typeof raw.ok !== "boolean") {
|
|
61
|
+
throw new Error("tool raw result ok must be a boolean.");
|
|
62
|
+
}
|
|
63
|
+
if (kind === "skill") {
|
|
64
|
+
return decodeStoredSkillRawResult(raw);
|
|
65
|
+
}
|
|
66
|
+
if (kind === "view_image") {
|
|
67
|
+
return decodeStoredViewImageRawResult(raw);
|
|
68
|
+
}
|
|
69
|
+
if (kind === "context_maintenance") {
|
|
70
|
+
return decodeStoredContextMaintenanceRawResult(raw);
|
|
71
|
+
}
|
|
72
|
+
return immutableCanonicalClone(raw) as ToolRawResult;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function decodeStoredContextMaintenanceRawResult(
|
|
76
|
+
raw: Record<string, unknown>,
|
|
77
|
+
): Extract<ToolRawResult, { kind: "context_maintenance" }> {
|
|
78
|
+
const operation = enumFromSql(
|
|
79
|
+
raw.operation,
|
|
80
|
+
["status", "candidates", "swap"] as const,
|
|
81
|
+
"context maintenance operation",
|
|
82
|
+
);
|
|
83
|
+
if (raw.ok === false) {
|
|
84
|
+
if (operation !== "swap") {
|
|
85
|
+
assertObjectKeys(
|
|
86
|
+
raw,
|
|
87
|
+
["kind", "ok", "operation", "error"],
|
|
88
|
+
["kind", "ok", "operation", "error"],
|
|
89
|
+
`failed context ${operation} result`,
|
|
90
|
+
);
|
|
91
|
+
return immutableRecord({
|
|
92
|
+
kind: "context_maintenance" as const,
|
|
93
|
+
ok: false as const,
|
|
94
|
+
operation,
|
|
95
|
+
error: nonEmptyStringFromJson(raw.error, `context ${operation} error`),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
assertObjectKeys(
|
|
99
|
+
raw,
|
|
100
|
+
["kind", "ok", "operation", "scheduled", "rejected", "error"],
|
|
101
|
+
["kind", "ok", "operation", "scheduled", "rejected"],
|
|
102
|
+
"failed context swap result",
|
|
103
|
+
);
|
|
104
|
+
if (!Array.isArray(raw.scheduled) || raw.scheduled.length !== 0) {
|
|
105
|
+
throw new Error("Failed context swap result must schedule no candidates.");
|
|
106
|
+
}
|
|
107
|
+
const rejected = decodeContextSwapRejected(raw.rejected);
|
|
108
|
+
const error =
|
|
109
|
+
raw.error === undefined
|
|
110
|
+
? undefined
|
|
111
|
+
: nonEmptyStringFromJson(raw.error, "context swap error");
|
|
112
|
+
if (rejected.length === 0 && error === undefined) {
|
|
113
|
+
throw new Error("Failed context swap result must explain its failure.");
|
|
114
|
+
}
|
|
115
|
+
return immutableRecord({
|
|
116
|
+
kind: "context_maintenance" as const,
|
|
117
|
+
ok: false as const,
|
|
118
|
+
operation,
|
|
119
|
+
scheduled: Object.freeze([]),
|
|
120
|
+
rejected,
|
|
121
|
+
...(error === undefined ? {} : { error }),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (raw.ok !== true) {
|
|
125
|
+
throw new Error("Context maintenance raw result ok must be a boolean.");
|
|
126
|
+
}
|
|
127
|
+
if (operation === "status") {
|
|
128
|
+
assertObjectKeys(
|
|
129
|
+
raw,
|
|
130
|
+
[
|
|
131
|
+
"kind",
|
|
132
|
+
"ok",
|
|
133
|
+
"operation",
|
|
134
|
+
"usedInputTokens",
|
|
135
|
+
"inputBudgetTokens",
|
|
136
|
+
"pressure",
|
|
137
|
+
"triggerTokens",
|
|
138
|
+
"source",
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
"kind",
|
|
142
|
+
"ok",
|
|
143
|
+
"operation",
|
|
144
|
+
"usedInputTokens",
|
|
145
|
+
"inputBudgetTokens",
|
|
146
|
+
"pressure",
|
|
147
|
+
"triggerTokens",
|
|
148
|
+
"source",
|
|
149
|
+
],
|
|
150
|
+
"context status result",
|
|
151
|
+
);
|
|
152
|
+
return immutableRecord({
|
|
153
|
+
kind: "context_maintenance" as const,
|
|
154
|
+
ok: true as const,
|
|
155
|
+
operation,
|
|
156
|
+
usedInputTokens: nonNegativeJsonInteger(
|
|
157
|
+
raw.usedInputTokens,
|
|
158
|
+
"context status usedInputTokens",
|
|
159
|
+
),
|
|
160
|
+
inputBudgetTokens: positiveJsonInteger(
|
|
161
|
+
raw.inputBudgetTokens,
|
|
162
|
+
"context status inputBudgetTokens",
|
|
163
|
+
),
|
|
164
|
+
pressure: enumFromSql(
|
|
165
|
+
raw.pressure,
|
|
166
|
+
["normal", "high", "critical"] as const,
|
|
167
|
+
"context status pressure",
|
|
168
|
+
),
|
|
169
|
+
triggerTokens: positiveJsonInteger(
|
|
170
|
+
raw.triggerTokens,
|
|
171
|
+
"context status triggerTokens",
|
|
172
|
+
),
|
|
173
|
+
source: enumFromSql(
|
|
174
|
+
raw.source,
|
|
175
|
+
[
|
|
176
|
+
"estimated_full",
|
|
177
|
+
"provider_measured",
|
|
178
|
+
"measured_plus_estimated_delta",
|
|
179
|
+
] as const,
|
|
180
|
+
"context status source",
|
|
181
|
+
),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
if (operation === "candidates") {
|
|
185
|
+
assertObjectKeys(
|
|
186
|
+
raw,
|
|
187
|
+
["kind", "ok", "operation", "total", "candidates"],
|
|
188
|
+
["kind", "ok", "operation", "total", "candidates"],
|
|
189
|
+
"context swap candidates result",
|
|
190
|
+
);
|
|
191
|
+
if (!Array.isArray(raw.candidates) || raw.candidates.length > 50) {
|
|
192
|
+
throw new Error("Context swap candidates result has an invalid page.");
|
|
193
|
+
}
|
|
194
|
+
const candidates = raw.candidates.map((value, index) => {
|
|
195
|
+
const candidate = recordFromSql(value, `context candidate ${index}`);
|
|
196
|
+
assertObjectKeys(
|
|
197
|
+
candidate,
|
|
198
|
+
["candidateId", "label", "ordinal", "savingsBytes"],
|
|
199
|
+
["candidateId", "label", "ordinal", "savingsBytes"],
|
|
200
|
+
`context candidate ${index}`,
|
|
201
|
+
);
|
|
202
|
+
const label = stringFromSql(candidate.label, `context candidate ${index} label`);
|
|
203
|
+
if (
|
|
204
|
+
label === "" ||
|
|
205
|
+
label !== label.replace(/[\p{Cc}\p{Cf}\s]+/gu, " ").trim() ||
|
|
206
|
+
Buffer.byteLength(label, "utf8") > 80
|
|
207
|
+
) {
|
|
208
|
+
throw new Error(`Context candidate ${index} label is invalid or too large.`);
|
|
209
|
+
}
|
|
210
|
+
return immutableRecord({
|
|
211
|
+
candidateId: parseMessageId(
|
|
212
|
+
stringFromSql(candidate.candidateId, `context candidate ${index} ID`),
|
|
213
|
+
),
|
|
214
|
+
label,
|
|
215
|
+
ordinal: positiveJsonInteger(
|
|
216
|
+
candidate.ordinal,
|
|
217
|
+
`context candidate ${index} ordinal`,
|
|
218
|
+
),
|
|
219
|
+
savingsBytes: positiveJsonInteger(
|
|
220
|
+
candidate.savingsBytes,
|
|
221
|
+
`context candidate ${index} savingsBytes`,
|
|
222
|
+
),
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
if (
|
|
226
|
+
new Set(candidates.map((candidate) => candidate.candidateId)).size !==
|
|
227
|
+
candidates.length ||
|
|
228
|
+
candidates.some(
|
|
229
|
+
(candidate, index) =>
|
|
230
|
+
index > 0 &&
|
|
231
|
+
candidate.ordinal <= (candidates[index - 1]?.ordinal ?? candidate.ordinal),
|
|
232
|
+
)
|
|
233
|
+
) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"Context swap candidates must have unique IDs and ascending ordinals.",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
const total = nonNegativeJsonInteger(raw.total, "context candidates total");
|
|
239
|
+
if (total < candidates.length) {
|
|
240
|
+
throw new Error("Context candidates total is smaller than its page.");
|
|
241
|
+
}
|
|
242
|
+
return immutableRecord({
|
|
243
|
+
kind: "context_maintenance" as const,
|
|
244
|
+
ok: true as const,
|
|
245
|
+
operation,
|
|
246
|
+
total,
|
|
247
|
+
candidates: Object.freeze(candidates),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
assertObjectKeys(
|
|
252
|
+
raw,
|
|
253
|
+
["kind", "ok", "operation", "scheduled", "rejected", "note"],
|
|
254
|
+
["kind", "ok", "operation", "scheduled", "rejected", "note"],
|
|
255
|
+
"context swap result",
|
|
256
|
+
);
|
|
257
|
+
if (!Array.isArray(raw.scheduled) || raw.scheduled.length < 1) {
|
|
258
|
+
throw new Error("Successful context swap result must schedule candidates.");
|
|
259
|
+
}
|
|
260
|
+
const scheduled = raw.scheduled.map((value, index) => {
|
|
261
|
+
const candidate = recordFromSql(value, `scheduled context candidate ${index}`);
|
|
262
|
+
assertObjectKeys(
|
|
263
|
+
candidate,
|
|
264
|
+
["candidateId", "savingsBytes"],
|
|
265
|
+
["candidateId", "savingsBytes"],
|
|
266
|
+
`scheduled context candidate ${index}`,
|
|
267
|
+
);
|
|
268
|
+
return immutableRecord({
|
|
269
|
+
candidateId: parseMessageId(
|
|
270
|
+
stringFromSql(candidate.candidateId, `scheduled candidate ${index} ID`),
|
|
271
|
+
),
|
|
272
|
+
savingsBytes: positiveJsonInteger(
|
|
273
|
+
candidate.savingsBytes,
|
|
274
|
+
`scheduled candidate ${index} savingsBytes`,
|
|
275
|
+
),
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
if (
|
|
279
|
+
scheduled.length > 16 ||
|
|
280
|
+
new Set(scheduled.map((candidate) => candidate.candidateId)).size !==
|
|
281
|
+
scheduled.length
|
|
282
|
+
) {
|
|
283
|
+
throw new Error("Successful context swap result has invalid scheduled IDs.");
|
|
284
|
+
}
|
|
285
|
+
const rejected = decodeContextSwapRejected(raw.rejected);
|
|
286
|
+
if (
|
|
287
|
+
scheduled.length + rejected.length > 16 ||
|
|
288
|
+
scheduled.some((scheduledCandidate) =>
|
|
289
|
+
rejected.some(
|
|
290
|
+
(rejectedCandidate) =>
|
|
291
|
+
rejectedCandidate.candidateId === scheduledCandidate.candidateId,
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
) {
|
|
295
|
+
throw new Error("Context swap result candidate partitions are invalid.");
|
|
296
|
+
}
|
|
297
|
+
const note = stringFromSql(raw.note, "context swap note");
|
|
298
|
+
return immutableRecord({
|
|
299
|
+
kind: "context_maintenance" as const,
|
|
300
|
+
ok: true as const,
|
|
301
|
+
operation,
|
|
302
|
+
scheduled: Object.freeze(scheduled),
|
|
303
|
+
rejected,
|
|
304
|
+
note,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function decodeContextSwapRejected(value: unknown): readonly {
|
|
309
|
+
readonly candidateId: MessageId;
|
|
310
|
+
readonly reason: string;
|
|
311
|
+
}[] {
|
|
312
|
+
if (!Array.isArray(value) || value.length > 16) {
|
|
313
|
+
throw new Error("Context swap rejected candidates must be an array of at most 16.");
|
|
314
|
+
}
|
|
315
|
+
const rejected = value.map((entry, index) => {
|
|
316
|
+
const candidate = recordFromSql(entry, `rejected context candidate ${index}`);
|
|
317
|
+
assertObjectKeys(
|
|
318
|
+
candidate,
|
|
319
|
+
["candidateId", "reason"],
|
|
320
|
+
["candidateId", "reason"],
|
|
321
|
+
`rejected context candidate ${index}`,
|
|
322
|
+
);
|
|
323
|
+
const reason = stringFromSql(
|
|
324
|
+
candidate.reason,
|
|
325
|
+
`rejected context candidate ${index} reason`,
|
|
326
|
+
);
|
|
327
|
+
if (!/^[a-z][a-z0-9_]{0,79}$/.test(reason)) {
|
|
328
|
+
throw new Error(`Rejected context candidate ${index} reason is invalid.`);
|
|
329
|
+
}
|
|
330
|
+
return immutableRecord({
|
|
331
|
+
candidateId: parseMessageId(
|
|
332
|
+
stringFromSql(candidate.candidateId, `rejected candidate ${index} ID`),
|
|
333
|
+
),
|
|
334
|
+
reason,
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
if (
|
|
338
|
+
new Set(rejected.map((candidate) => candidate.candidateId)).size !== rejected.length
|
|
339
|
+
) {
|
|
340
|
+
throw new Error("Context swap rejected candidate IDs must be unique.");
|
|
341
|
+
}
|
|
342
|
+
return Object.freeze(rejected);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function decodeStoredViewImageRawResult(
|
|
346
|
+
raw: Record<string, unknown>,
|
|
347
|
+
): Extract<ToolRawResult, { kind: "view_image" }> {
|
|
348
|
+
assertObjectKeys(
|
|
349
|
+
raw,
|
|
350
|
+
["kind", "ok", "filePath", "originalName", "asset", "error"],
|
|
351
|
+
["kind", "ok", "filePath"],
|
|
352
|
+
"ViewImage raw result",
|
|
353
|
+
);
|
|
354
|
+
const filePath = stringFromSql(raw.filePath, "ViewImage filePath");
|
|
355
|
+
if (raw.ok === false) {
|
|
356
|
+
if (raw.originalName !== undefined || raw.asset !== undefined) {
|
|
357
|
+
throw new Error("Failed ViewImage raw result cannot contain image metadata.");
|
|
358
|
+
}
|
|
359
|
+
const error = stringFromSql(raw.error, "ViewImage error");
|
|
360
|
+
if (error.trim() === "") {
|
|
361
|
+
throw new Error("Failed ViewImage raw result error must not be empty.");
|
|
362
|
+
}
|
|
363
|
+
return immutableRecord({ kind: "view_image", ok: false, filePath, error });
|
|
364
|
+
}
|
|
365
|
+
if (raw.ok !== true || raw.error !== undefined || filePath.trim() === "") {
|
|
366
|
+
throw new Error("Successful ViewImage raw result is invalid.");
|
|
367
|
+
}
|
|
368
|
+
const originalName = stringFromSql(raw.originalName, "ViewImage originalName");
|
|
369
|
+
validateOriginalImageName(originalName);
|
|
370
|
+
const storedAsset = recordFromSql(raw.asset, "ViewImage asset");
|
|
371
|
+
assertObjectKeys(
|
|
372
|
+
storedAsset,
|
|
373
|
+
["assetId", "mimeType", "byteLength", "width", "height"],
|
|
374
|
+
["assetId", "mimeType", "byteLength", "width", "height"],
|
|
375
|
+
"ViewImage asset",
|
|
376
|
+
);
|
|
377
|
+
const asset: ImageAssetRef = immutableRecord({
|
|
378
|
+
assetId: parseImageAssetId(stringFromSql(storedAsset.assetId, "assetId")),
|
|
379
|
+
mimeType: enumFromSql(
|
|
380
|
+
storedAsset.mimeType,
|
|
381
|
+
["image/png", "image/jpeg", "image/webp"] as const,
|
|
382
|
+
"ViewImage asset mimeType",
|
|
383
|
+
),
|
|
384
|
+
byteLength: numberFromJson(storedAsset.byteLength, "ViewImage asset byteLength"),
|
|
385
|
+
width: numberFromJson(storedAsset.width, "ViewImage asset width"),
|
|
386
|
+
height: numberFromJson(storedAsset.height, "ViewImage asset height"),
|
|
387
|
+
});
|
|
388
|
+
validateImageAssetRef(asset);
|
|
389
|
+
return immutableRecord({
|
|
390
|
+
kind: "view_image",
|
|
391
|
+
ok: true,
|
|
392
|
+
filePath,
|
|
393
|
+
originalName,
|
|
394
|
+
asset,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function decodeStoredSkillRawResult(
|
|
399
|
+
raw: Record<string, unknown>,
|
|
400
|
+
): Extract<ToolRawResult, { kind: "skill" }> {
|
|
401
|
+
const status = enumFromSql(
|
|
402
|
+
raw.status,
|
|
403
|
+
["loaded", "already_loaded", "already_active", "failed"] as const,
|
|
404
|
+
"Skill raw result status",
|
|
405
|
+
);
|
|
406
|
+
const name = stringFromSql(raw.name, "Skill raw result name");
|
|
407
|
+
if (status === "failed") {
|
|
408
|
+
if (raw.ok !== false) {
|
|
409
|
+
throw new Error("Failed Skill raw result must have ok=false.");
|
|
410
|
+
}
|
|
411
|
+
assertObjectKeys(
|
|
412
|
+
raw,
|
|
413
|
+
["kind", "ok", "status", "name", "errorCode", "error"],
|
|
414
|
+
["kind", "ok", "status", "name", "errorCode", "error"],
|
|
415
|
+
"failed Skill raw result",
|
|
416
|
+
);
|
|
417
|
+
const errorCode = stringFromSql(raw.errorCode, "Skill errorCode");
|
|
418
|
+
const error = stringFromSql(raw.error, "Skill error");
|
|
419
|
+
if (
|
|
420
|
+
(name !== "" && !isValidSkillName(name)) ||
|
|
421
|
+
!/^[A-Z][A-Z0-9_]{0,79}$/.test(errorCode) ||
|
|
422
|
+
error.trim() === ""
|
|
423
|
+
) {
|
|
424
|
+
throw new Error("Failed Skill raw result fields are invalid.");
|
|
425
|
+
}
|
|
426
|
+
return immutableRecord({
|
|
427
|
+
kind: "skill" as const,
|
|
428
|
+
ok: false as const,
|
|
429
|
+
status,
|
|
430
|
+
name,
|
|
431
|
+
errorCode,
|
|
432
|
+
error,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (raw.ok !== true) {
|
|
436
|
+
throw new Error("Successful Skill raw result must have ok=true.");
|
|
437
|
+
}
|
|
438
|
+
const scope = enumFromSql(
|
|
439
|
+
raw.scope,
|
|
440
|
+
["project", "user"] as const,
|
|
441
|
+
"Skill raw result scope",
|
|
442
|
+
);
|
|
443
|
+
const skillFileSha256 = sha256FromSql(raw.sha256, "Skill raw result sha256");
|
|
444
|
+
if (!isValidSkillName(name)) {
|
|
445
|
+
throw new Error("Successful Skill raw result name is invalid.");
|
|
446
|
+
}
|
|
447
|
+
if (status === "already_loaded") {
|
|
448
|
+
assertObjectKeys(
|
|
449
|
+
raw,
|
|
450
|
+
["kind", "ok", "status", "name", "scope", "lifecycle", "sha256"],
|
|
451
|
+
["kind", "ok", "status", "name", "scope", "lifecycle", "sha256"],
|
|
452
|
+
"already loaded Skill raw result",
|
|
453
|
+
);
|
|
454
|
+
return immutableRecord({
|
|
455
|
+
kind: "skill" as const,
|
|
456
|
+
ok: true as const,
|
|
457
|
+
status,
|
|
458
|
+
name,
|
|
459
|
+
scope,
|
|
460
|
+
lifecycle: enumFromSql(
|
|
461
|
+
raw.lifecycle,
|
|
462
|
+
["pending", "dispatched"] as const,
|
|
463
|
+
"Skill lifecycle",
|
|
464
|
+
),
|
|
465
|
+
sha256: skillFileSha256,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (status === "already_active") {
|
|
469
|
+
assertObjectKeys(
|
|
470
|
+
raw,
|
|
471
|
+
["kind", "ok", "status", "name", "scope", "sha256"],
|
|
472
|
+
["kind", "ok", "status", "name", "scope", "sha256"],
|
|
473
|
+
"already active Skill raw result",
|
|
474
|
+
);
|
|
475
|
+
return immutableRecord({
|
|
476
|
+
kind: "skill" as const,
|
|
477
|
+
ok: true as const,
|
|
478
|
+
status,
|
|
479
|
+
name,
|
|
480
|
+
scope,
|
|
481
|
+
sha256: skillFileSha256,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
assertObjectKeys(
|
|
485
|
+
raw,
|
|
486
|
+
[
|
|
487
|
+
"kind",
|
|
488
|
+
"ok",
|
|
489
|
+
"status",
|
|
490
|
+
"name",
|
|
491
|
+
"scope",
|
|
492
|
+
"directory",
|
|
493
|
+
"skillFilePath",
|
|
494
|
+
"content",
|
|
495
|
+
"byteLength",
|
|
496
|
+
"sha256",
|
|
497
|
+
"resources",
|
|
498
|
+
"resourcesTruncated",
|
|
499
|
+
],
|
|
500
|
+
[
|
|
501
|
+
"kind",
|
|
502
|
+
"ok",
|
|
503
|
+
"status",
|
|
504
|
+
"name",
|
|
505
|
+
"scope",
|
|
506
|
+
"directory",
|
|
507
|
+
"skillFilePath",
|
|
508
|
+
"content",
|
|
509
|
+
"byteLength",
|
|
510
|
+
"sha256",
|
|
511
|
+
"resources",
|
|
512
|
+
"resourcesTruncated",
|
|
513
|
+
],
|
|
514
|
+
"loaded Skill raw result",
|
|
515
|
+
);
|
|
516
|
+
if (
|
|
517
|
+
!Array.isArray(raw.resources) ||
|
|
518
|
+
raw.resources.some((entry) => typeof entry !== "string") ||
|
|
519
|
+
raw.resources.length > SKILL_RESOURCE_MAX_ENTRIES ||
|
|
520
|
+
typeof raw.resourcesTruncated !== "boolean"
|
|
521
|
+
) {
|
|
522
|
+
throw new Error("Loaded Skill resource manifest is invalid.");
|
|
523
|
+
}
|
|
524
|
+
const directory = stringFromSql(raw.directory, "Skill directory");
|
|
525
|
+
const skillFilePath = stringFromSql(raw.skillFilePath, "Skill file path");
|
|
526
|
+
const content = stringFromSql(raw.content, "Skill content");
|
|
527
|
+
const byteLength = numberFromJson(raw.byteLength, "Skill byteLength");
|
|
528
|
+
const resources = raw.resources as string[];
|
|
529
|
+
if (
|
|
530
|
+
!path.isAbsolute(directory) ||
|
|
531
|
+
!path.isAbsolute(skillFilePath) ||
|
|
532
|
+
!isPathWithin(directory, skillFilePath) ||
|
|
533
|
+
byteLength < 1 ||
|
|
534
|
+
byteLength > SKILL_FILE_MAX_BYTES ||
|
|
535
|
+
Buffer.byteLength(content, "utf8") !== byteLength ||
|
|
536
|
+
sha256(content) !== skillFileSha256 ||
|
|
537
|
+
!isValidResourceManifest(resources)
|
|
538
|
+
) {
|
|
539
|
+
throw new Error("Loaded Skill raw result snapshot is invalid.");
|
|
540
|
+
}
|
|
541
|
+
return immutableRecord({
|
|
542
|
+
kind: "skill" as const,
|
|
543
|
+
ok: true as const,
|
|
544
|
+
status,
|
|
545
|
+
name,
|
|
546
|
+
scope,
|
|
547
|
+
directory,
|
|
548
|
+
skillFilePath,
|
|
549
|
+
content,
|
|
550
|
+
byteLength,
|
|
551
|
+
sha256: skillFileSha256,
|
|
552
|
+
resources: Object.freeze([...resources]),
|
|
553
|
+
resourcesTruncated: raw.resourcesTruncated,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isValidSkillName(value: string): boolean {
|
|
558
|
+
return value.length <= 64 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function isPathWithin(root: string, candidate: string): boolean {
|
|
562
|
+
const relative = path.relative(root, candidate);
|
|
563
|
+
return (
|
|
564
|
+
relative !== "" &&
|
|
565
|
+
relative !== ".." &&
|
|
566
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
567
|
+
!path.isAbsolute(relative)
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function isValidResourceManifest(resources: readonly string[]): boolean {
|
|
572
|
+
let previous: string | undefined;
|
|
573
|
+
for (const resource of resources) {
|
|
574
|
+
const parts = resource.split("/");
|
|
575
|
+
if (
|
|
576
|
+
resource === "" ||
|
|
577
|
+
resource.includes("\\") ||
|
|
578
|
+
path.posix.isAbsolute(resource) ||
|
|
579
|
+
path.posix.normalize(resource) !== resource ||
|
|
580
|
+
!["assets", "references", "scripts"].includes(parts[0] ?? "") ||
|
|
581
|
+
parts.length < 2 ||
|
|
582
|
+
parts.length > SKILL_RESOURCE_MAX_DEPTH + 1 ||
|
|
583
|
+
(previous !== undefined && previous >= resource)
|
|
584
|
+
) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
previous = resource;
|
|
588
|
+
}
|
|
589
|
+
return true;
|
|
590
|
+
}
|
package/src/tools/grep.ts
CHANGED
|
@@ -315,9 +315,7 @@ export function buildRipgrepArgs(
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
if (input.glob !== undefined) {
|
|
318
|
-
|
|
319
|
-
args.push("--glob", pattern);
|
|
320
|
-
}
|
|
318
|
+
args.push("--glob", input.glob);
|
|
321
319
|
}
|
|
322
320
|
|
|
323
321
|
args.push("-e", input.pattern, absoluteSearchPath);
|
package/src/tui/app.tsx
CHANGED
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
|
|
68
68
|
export type AppProps = {
|
|
69
69
|
sessionController: TuiSessionController;
|
|
70
|
+
version?: string;
|
|
70
71
|
readGitBranch?: (workspaceRoot: string) => Promise<string | undefined>;
|
|
71
72
|
history?: PromptHistory;
|
|
72
73
|
projectSlashCommands?: readonly ProjectSlashCommand[];
|
|
@@ -940,6 +941,7 @@ export function App(props: AppProps) {
|
|
|
940
941
|
<PromptInput
|
|
941
942
|
modelName={binding.modelName}
|
|
942
943
|
reasoningEffort={reasoningEffort?.effort}
|
|
944
|
+
version={props.version}
|
|
943
945
|
workspaceRoot={binding.workspaceRoot}
|
|
944
946
|
gitBranch={gitBranch}
|
|
945
947
|
contextUsage={state.contextUsage}
|
|
@@ -61,6 +61,7 @@ export type PromptSubmissionOutcome =
|
|
|
61
61
|
export type PromptInputProps = {
|
|
62
62
|
modelName: string;
|
|
63
63
|
reasoningEffort?: string;
|
|
64
|
+
version?: string;
|
|
64
65
|
workspaceRoot: string;
|
|
65
66
|
gitBranch?: string;
|
|
66
67
|
contextUsage?: ContextUsageSnapshot;
|
|
@@ -776,6 +777,9 @@ export function PromptInput(props: PromptInputProps) {
|
|
|
776
777
|
<Text color={FOOTER_COLORS.cacheRate}>{cacheRate}</Text>
|
|
777
778
|
</>
|
|
778
779
|
)}
|
|
780
|
+
{props.version === undefined ? null : (
|
|
781
|
+
<Text dimColor> · tinker {props.version}</Text>
|
|
782
|
+
)}
|
|
779
783
|
</Text>
|
|
780
784
|
</Box>
|
|
781
785
|
)}
|