@giovannijecha/jecode 0.8.5 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -5
- package/dist/batch-view.js +27 -2
- package/dist/batch.js +2 -0
- package/dist/cli-info.js +0 -1
- package/dist/config.js +14 -6
- package/dist/credential-commands.js +3 -3
- package/dist/openai-account-command.js +14 -12
- package/dist/openai-account.js +7 -5
- package/dist/openai-oauth-callback.js +13 -4
- package/dist/openai-oauth-tokens.js +9 -7
- package/dist/openai-oauth.js +8 -6
- package/dist/permission-command.js +1 -1
- package/dist/provider-commands.js +1 -33
- package/dist/provider-errors.js +1 -10
- package/dist/provider-label.js +3 -10
- package/dist/providers/anthropic.js +0 -1
- package/dist/providers/index.js +1 -5
- package/dist/providers/ollama-context.js +42 -0
- package/dist/providers/ollama-endpoint.js +7 -34
- package/dist/providers/ollama.js +13 -147
- package/dist/providers/openai-codex.js +4 -4
- package/dist/providers/openai.js +0 -1
- package/dist/sessions/bucket.js +55 -0
- package/dist/sessions/catalog-io.js +162 -0
- package/dist/sessions/catalog.js +3 -1
- package/dist/sessions/codec-messages.js +122 -0
- package/dist/sessions/codec-transcript.js +93 -0
- package/dist/sessions/codec-values.js +52 -0
- package/dist/sessions/codec.js +4 -257
- package/dist/sessions/files.js +158 -0
- package/dist/sessions/load.js +90 -0
- package/dist/sessions/snapshot.js +33 -0
- package/dist/sessions/store.js +42 -461
- package/dist/settings-command.js +10 -5
- package/dist/settings.js +16 -15
- package/dist/start.js +1 -2
- package/dist/tools/file-read.js +192 -0
- package/dist/tools/file-summary.js +9 -0
- package/dist/tools/{fs.js → file-write.js} +5 -193
- package/dist/tools/glob.js +107 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/search.js +1 -105
- package/dist/tui/app-workflows.js +8 -361
- package/dist/tui/approve.js +5 -3
- package/dist/tui/blocks.js +8 -7
- package/dist/tui/command-workflow.js +106 -0
- package/dist/tui/components/command-menu.js +7 -10
- package/dist/tui/components/menu.js +74 -43
- package/dist/tui/components/messages.js +16 -9
- package/dist/tui/components/tool-evidence.js +107 -0
- package/dist/tui/components/tool-motion.js +32 -0
- package/dist/tui/components/tool.js +48 -202
- package/dist/tui/help.js +1 -1
- package/dist/tui/picker-layout.js +40 -0
- package/dist/tui/picker.js +7 -71
- package/dist/tui/tool-details.js +135 -0
- package/dist/tui/transcript-grammar.js +8 -1
- package/dist/tui/transcript-view.js +26 -112
- package/dist/tui/turn-workflow.js +264 -0
- package/dist/tui/turn.js +7 -140
- package/dist/tui/workflow-types.js +2 -0
- package/package.json +12 -12
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tui/motion.js +0 -32
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Normalized provider-message records; opaque provider data never persists.
|
|
2
|
+
import { bounded, boundedText, integer, invalid, keys, record, SESSION_FILE_LIMITS, } from "./codec-values.js";
|
|
3
|
+
export function messageRecord(message) {
|
|
4
|
+
return {
|
|
5
|
+
role: message.role,
|
|
6
|
+
content: message.content.map(contentRecord),
|
|
7
|
+
usage: message.usage ?? null,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function messageFromRecord(value) {
|
|
11
|
+
if (!record(value) || !keys(value, "content,role,usage"))
|
|
12
|
+
throw invalid();
|
|
13
|
+
if ((value["role"] !== "user" && value["role"] !== "assistant") ||
|
|
14
|
+
!Array.isArray(value["content"]) || value["content"].length > SESSION_FILE_LIMITS.blocks)
|
|
15
|
+
throw invalid();
|
|
16
|
+
const usage = value["usage"] === null ? undefined : usageFromRecord(value["usage"]);
|
|
17
|
+
return {
|
|
18
|
+
role: value["role"],
|
|
19
|
+
content: value["content"].map(contentFromRecord),
|
|
20
|
+
...(usage === undefined ? {} : { usage }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function contentRecord(block) {
|
|
24
|
+
if (block.kind === "text")
|
|
25
|
+
return { kind: block.kind, text: block.text };
|
|
26
|
+
if (block.kind === "tool_call") {
|
|
27
|
+
return { kind: block.kind, id: block.id, name: block.name, input: block.input };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
kind: block.kind,
|
|
31
|
+
id: block.id,
|
|
32
|
+
output: block.output,
|
|
33
|
+
isError: block.isError,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function contentFromRecord(value) {
|
|
37
|
+
if (!record(value) || typeof value["kind"] !== "string")
|
|
38
|
+
throw invalid();
|
|
39
|
+
if (value["kind"] === "text" && keys(value, "kind,text") && boundedText(value["text"])) {
|
|
40
|
+
return { kind: "text", text: value["text"] };
|
|
41
|
+
}
|
|
42
|
+
if (value["kind"] === "tool_call" && keys(value, "id,input,kind,name") &&
|
|
43
|
+
bounded(value["id"], 512) && bounded(value["name"], 256)) {
|
|
44
|
+
const input = jsonObject(value["input"]);
|
|
45
|
+
return { kind: "tool_call", id: value["id"], name: value["name"], input };
|
|
46
|
+
}
|
|
47
|
+
if (value["kind"] === "tool_result" && keys(value, "id,isError,kind,output") &&
|
|
48
|
+
bounded(value["id"], 512) && boundedText(value["output"]) &&
|
|
49
|
+
typeof value["isError"] === "boolean") {
|
|
50
|
+
return {
|
|
51
|
+
kind: "tool_result",
|
|
52
|
+
id: value["id"],
|
|
53
|
+
output: value["output"],
|
|
54
|
+
isError: value["isError"],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
throw invalid();
|
|
58
|
+
}
|
|
59
|
+
function usageFromRecord(value) {
|
|
60
|
+
if (!record(value) || !keys(value, "cacheWriteInputTokens,cachedInputTokens,inputTokens,outputTokens,reasoningTokens"))
|
|
61
|
+
throw invalid();
|
|
62
|
+
const inputTokens = value["inputTokens"];
|
|
63
|
+
const outputTokens = value["outputTokens"];
|
|
64
|
+
const cachedInputTokens = value["cachedInputTokens"];
|
|
65
|
+
const cacheWriteInputTokens = value["cacheWriteInputTokens"];
|
|
66
|
+
const reasoningTokens = value["reasoningTokens"];
|
|
67
|
+
if (!integer(inputTokens, 0) || !integer(outputTokens, 0) ||
|
|
68
|
+
!integer(cachedInputTokens, 0) || !integer(cacheWriteInputTokens, 0) ||
|
|
69
|
+
!integer(reasoningTokens, 0))
|
|
70
|
+
throw invalid();
|
|
71
|
+
return {
|
|
72
|
+
inputTokens,
|
|
73
|
+
outputTokens,
|
|
74
|
+
cachedInputTokens,
|
|
75
|
+
cacheWriteInputTokens,
|
|
76
|
+
reasoningTokens,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function jsonObject(value) {
|
|
80
|
+
const budget = { nodes: 0 };
|
|
81
|
+
const safe = jsonValue(value, budget, 0);
|
|
82
|
+
if (!record(safe))
|
|
83
|
+
throw invalid();
|
|
84
|
+
return safe;
|
|
85
|
+
}
|
|
86
|
+
function jsonValue(value, budget, depth) {
|
|
87
|
+
budget.nodes++;
|
|
88
|
+
if (budget.nodes > SESSION_FILE_LIMITS.jsonNodes || depth > SESSION_FILE_LIMITS.jsonDepth) {
|
|
89
|
+
throw invalid();
|
|
90
|
+
}
|
|
91
|
+
if (value === null || typeof value === "boolean")
|
|
92
|
+
return value;
|
|
93
|
+
if (typeof value === "string") {
|
|
94
|
+
if (!boundedText(value))
|
|
95
|
+
throw invalid();
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === "number") {
|
|
99
|
+
if (!Number.isFinite(value))
|
|
100
|
+
throw invalid();
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(value)) {
|
|
104
|
+
if (value.length > SESSION_FILE_LIMITS.blocks)
|
|
105
|
+
throw invalid();
|
|
106
|
+
return value.map((item) => jsonValue(item, budget, depth + 1));
|
|
107
|
+
}
|
|
108
|
+
if (!record(value) || Object.keys(value).length > SESSION_FILE_LIMITS.blocks)
|
|
109
|
+
throw invalid();
|
|
110
|
+
const safe = {};
|
|
111
|
+
for (const [name, child] of Object.entries(value)) {
|
|
112
|
+
if (!boundedText(name, 1_024))
|
|
113
|
+
throw invalid();
|
|
114
|
+
Object.defineProperty(safe, name, {
|
|
115
|
+
value: jsonValue(child, budget, depth + 1),
|
|
116
|
+
enumerable: true,
|
|
117
|
+
configurable: true,
|
|
118
|
+
writable: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return safe;
|
|
122
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Settled transcript evidence across supported session schema versions.
|
|
2
|
+
import { bounded, boundedText, integer, invalid, keys, nullableInteger, record, SESSION_FILE_LIMITS, } from "./codec-values.js";
|
|
3
|
+
export function blockRecord(block) {
|
|
4
|
+
if (block.kind === "notice")
|
|
5
|
+
return [];
|
|
6
|
+
if (block.kind === "user" || block.kind === "answer" || block.kind === "reasoning") {
|
|
7
|
+
return [{ kind: block.kind, text: block.text }];
|
|
8
|
+
}
|
|
9
|
+
if (block.tone === "pending")
|
|
10
|
+
return [];
|
|
11
|
+
return [{
|
|
12
|
+
kind: block.kind,
|
|
13
|
+
name: block.name,
|
|
14
|
+
target: block.target,
|
|
15
|
+
right: block.right,
|
|
16
|
+
tone: block.tone,
|
|
17
|
+
body: block.body?.map(detailRecord) ?? null,
|
|
18
|
+
durationMs: block.durationMs ?? null,
|
|
19
|
+
}];
|
|
20
|
+
}
|
|
21
|
+
export function blockFromRecord(value, version) {
|
|
22
|
+
if (!record(value) || typeof value["kind"] !== "string")
|
|
23
|
+
throw invalid();
|
|
24
|
+
if ((value["kind"] === "user" || value["kind"] === "answer" || value["kind"] === "reasoning") &&
|
|
25
|
+
keys(value, "kind,text") && boundedText(value["text"]))
|
|
26
|
+
return { kind: value["kind"], text: value["text"] };
|
|
27
|
+
const toolKeys = version >= 4
|
|
28
|
+
? "body,durationMs,kind,name,right,target,tone"
|
|
29
|
+
: "body,kind,name,right,target,tone";
|
|
30
|
+
if (value["kind"] === "tool" && keys(value, toolKeys) &&
|
|
31
|
+
bounded(value["name"], 256) && boundedText(value["target"]) &&
|
|
32
|
+
boundedText(value["right"], 1_024) &&
|
|
33
|
+
(value["tone"] === "ok" || value["tone"] === "fail" || value["tone"] === "deny") &&
|
|
34
|
+
(version < 4 || nullableInteger(value["durationMs"], 0)) &&
|
|
35
|
+
(value["body"] === null ||
|
|
36
|
+
(Array.isArray(value["body"]) && value["body"].length <= SESSION_FILE_LIMITS.details))) {
|
|
37
|
+
const body = value["body"] === null
|
|
38
|
+
? undefined
|
|
39
|
+
: value["body"].map(detailFromRecord);
|
|
40
|
+
return {
|
|
41
|
+
kind: "tool",
|
|
42
|
+
name: value["name"],
|
|
43
|
+
target: value["target"],
|
|
44
|
+
right: value["right"],
|
|
45
|
+
tone: value["tone"],
|
|
46
|
+
...(body === undefined ? {} : { body }),
|
|
47
|
+
...(version < 4 || value["durationMs"] === null
|
|
48
|
+
? {}
|
|
49
|
+
: { durationMs: value["durationMs"] }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
throw invalid();
|
|
53
|
+
}
|
|
54
|
+
function detailRecord(detail) {
|
|
55
|
+
if (detail.kind === "out" || detail.kind === "gap")
|
|
56
|
+
return { kind: detail.kind, text: detail.text };
|
|
57
|
+
return {
|
|
58
|
+
kind: detail.kind,
|
|
59
|
+
text: detail.text,
|
|
60
|
+
oldLine: detail.oldLine ?? null,
|
|
61
|
+
newLine: detail.newLine ?? null,
|
|
62
|
+
emphasis: detail.emphasis ?? null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function detailFromRecord(value) {
|
|
66
|
+
if (!record(value) || typeof value["kind"] !== "string")
|
|
67
|
+
throw invalid();
|
|
68
|
+
if ((value["kind"] === "out" || value["kind"] === "gap") &&
|
|
69
|
+
keys(value, "kind,text") && boundedText(value["text"]))
|
|
70
|
+
return { kind: value["kind"], text: value["text"] };
|
|
71
|
+
if ((value["kind"] === "keep" || value["kind"] === "add" || value["kind"] === "del") &&
|
|
72
|
+
keys(value, "emphasis,kind,newLine,oldLine,text") && boundedText(value["text"]) &&
|
|
73
|
+
nullableInteger(value["oldLine"], 1) && nullableInteger(value["newLine"], 1)) {
|
|
74
|
+
const emphasis = emphasisFromRecord(value["emphasis"]);
|
|
75
|
+
return {
|
|
76
|
+
kind: value["kind"],
|
|
77
|
+
text: value["text"],
|
|
78
|
+
...(value["oldLine"] === null ? {} : { oldLine: value["oldLine"] }),
|
|
79
|
+
...(value["newLine"] === null ? {} : { newLine: value["newLine"] }),
|
|
80
|
+
...(emphasis === undefined ? {} : { emphasis }),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
throw invalid();
|
|
84
|
+
}
|
|
85
|
+
function emphasisFromRecord(value) {
|
|
86
|
+
if (value === null)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (!record(value) || !keys(value, "length,start"))
|
|
89
|
+
throw invalid();
|
|
90
|
+
if (!integer(value["start"], 0) || !integer(value["length"], 1))
|
|
91
|
+
throw invalid();
|
|
92
|
+
return { start: value["start"], length: value["length"] };
|
|
93
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Shared strict value checks and limits for the canonical session codec.
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { MAX_TEXT_CODE_UNITS } from "../text-boundary.js";
|
|
4
|
+
export const SESSION_FILE_LIMITS = Object.freeze({
|
|
5
|
+
text: MAX_TEXT_CODE_UNITS,
|
|
6
|
+
metadataBytes: 64 * 1_024,
|
|
7
|
+
nodeBytes: 20 * 1_024 * 1_024,
|
|
8
|
+
jsonDepth: 24,
|
|
9
|
+
jsonNodes: 32_768,
|
|
10
|
+
blocks: 8_192,
|
|
11
|
+
details: 8_192,
|
|
12
|
+
});
|
|
13
|
+
function line(value) {
|
|
14
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
15
|
+
}
|
|
16
|
+
export function boundedLine(value, maxBytes) {
|
|
17
|
+
const encoded = line(value);
|
|
18
|
+
if (Buffer.byteLength(encoded, "utf8") > maxBytes)
|
|
19
|
+
throw invalid();
|
|
20
|
+
return encoded;
|
|
21
|
+
}
|
|
22
|
+
export function keys(value, expected) {
|
|
23
|
+
return Object.keys(value).sort().join(",") === expected;
|
|
24
|
+
}
|
|
25
|
+
export function record(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
export function bounded(value, limit = SESSION_FILE_LIMITS.text) {
|
|
29
|
+
return typeof value === "string" && value.length > 0 && value.length <= limit;
|
|
30
|
+
}
|
|
31
|
+
export function boundedText(value, limit = SESSION_FILE_LIMITS.text) {
|
|
32
|
+
return typeof value === "string" && value.length <= limit;
|
|
33
|
+
}
|
|
34
|
+
export function integer(value, minimum) {
|
|
35
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
|
|
36
|
+
}
|
|
37
|
+
export function nullableInteger(value, minimum) {
|
|
38
|
+
return value === null || integer(value, minimum);
|
|
39
|
+
}
|
|
40
|
+
export function identifier(value) {
|
|
41
|
+
return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value);
|
|
42
|
+
}
|
|
43
|
+
export function digest(value) {
|
|
44
|
+
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
45
|
+
}
|
|
46
|
+
export function timestamp(value) {
|
|
47
|
+
return typeof value === "string" && value.length >= 20 && value.length <= 64 &&
|
|
48
|
+
Number.isFinite(Date.parse(value));
|
|
49
|
+
}
|
|
50
|
+
export function invalid() {
|
|
51
|
+
return new Error("session data is invalid or unsupported");
|
|
52
|
+
}
|
package/dist/sessions/codec.js
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
// Strict codecs for session files. Disk is an untrusted boundary even when
|
|
2
2
|
// the directory is owner-only: every value is bounded and re-owned before it
|
|
3
3
|
// can become conversation or provider input.
|
|
4
|
-
import { Buffer } from "node:buffer";
|
|
5
4
|
import { CONTEXT_LIMITS } from "../context/projection.js";
|
|
6
|
-
import {
|
|
5
|
+
import { messageFromRecord, messageRecord } from "./codec-messages.js";
|
|
6
|
+
import { blockFromRecord, blockRecord } from "./codec-transcript.js";
|
|
7
|
+
import { bounded, boundedLine, digest, identifier, integer, invalid, keys, record, SESSION_FILE_LIMITS, timestamp, } from "./codec-values.js";
|
|
8
|
+
export { SESSION_FILE_LIMITS } from "./codec-values.js";
|
|
7
9
|
export const SESSION_SCHEMA = 4;
|
|
8
|
-
export const SESSION_FILE_LIMITS = Object.freeze({
|
|
9
|
-
text: MAX_TEXT_CODE_UNITS,
|
|
10
|
-
metadataBytes: 64 * 1_024,
|
|
11
|
-
nodeBytes: 20 * 1_024 * 1_024,
|
|
12
|
-
jsonDepth: 24,
|
|
13
|
-
jsonNodes: 32_768,
|
|
14
|
-
blocks: 8_192,
|
|
15
|
-
details: 8_192,
|
|
16
|
-
});
|
|
17
10
|
export function encodeMeta(meta) {
|
|
18
11
|
decodeMeta(meta);
|
|
19
12
|
return boundedLine(meta, SESSION_FILE_LIMITS.metadataBytes);
|
|
@@ -176,236 +169,6 @@ function contextFromRecord(value, ownerId, ownerMessages) {
|
|
|
176
169
|
summary: value["summary"],
|
|
177
170
|
};
|
|
178
171
|
}
|
|
179
|
-
function messageRecord(message) {
|
|
180
|
-
return {
|
|
181
|
-
role: message.role,
|
|
182
|
-
content: message.content.map(contentRecord),
|
|
183
|
-
usage: message.usage ?? null,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
function messageFromRecord(value) {
|
|
187
|
-
if (!record(value) || !keys(value, "content,role,usage"))
|
|
188
|
-
throw invalid();
|
|
189
|
-
if ((value["role"] !== "user" && value["role"] !== "assistant") ||
|
|
190
|
-
!Array.isArray(value["content"]) || value["content"].length > SESSION_FILE_LIMITS.blocks)
|
|
191
|
-
throw invalid();
|
|
192
|
-
const usage = value["usage"] === null ? undefined : usageFromRecord(value["usage"]);
|
|
193
|
-
return {
|
|
194
|
-
role: value["role"],
|
|
195
|
-
content: value["content"].map(contentFromRecord),
|
|
196
|
-
...(usage === undefined ? {} : { usage }),
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
function contentRecord(block) {
|
|
200
|
-
if (block.kind === "text")
|
|
201
|
-
return { kind: block.kind, text: block.text };
|
|
202
|
-
if (block.kind === "tool_call") {
|
|
203
|
-
return { kind: block.kind, id: block.id, name: block.name, input: block.input };
|
|
204
|
-
}
|
|
205
|
-
return {
|
|
206
|
-
kind: block.kind,
|
|
207
|
-
id: block.id,
|
|
208
|
-
output: block.output,
|
|
209
|
-
isError: block.isError,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
function contentFromRecord(value) {
|
|
213
|
-
if (!record(value) || typeof value["kind"] !== "string")
|
|
214
|
-
throw invalid();
|
|
215
|
-
if (value["kind"] === "text" && keys(value, "kind,text") && boundedText(value["text"])) {
|
|
216
|
-
return { kind: "text", text: value["text"] };
|
|
217
|
-
}
|
|
218
|
-
if (value["kind"] === "tool_call" && keys(value, "id,input,kind,name") &&
|
|
219
|
-
bounded(value["id"], 512) && bounded(value["name"], 256)) {
|
|
220
|
-
const input = jsonObject(value["input"]);
|
|
221
|
-
return { kind: "tool_call", id: value["id"], name: value["name"], input };
|
|
222
|
-
}
|
|
223
|
-
if (value["kind"] === "tool_result" && keys(value, "id,isError,kind,output") &&
|
|
224
|
-
bounded(value["id"], 512) && boundedText(value["output"]) &&
|
|
225
|
-
typeof value["isError"] === "boolean") {
|
|
226
|
-
return {
|
|
227
|
-
kind: "tool_result",
|
|
228
|
-
id: value["id"],
|
|
229
|
-
output: value["output"],
|
|
230
|
-
isError: value["isError"],
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
throw invalid();
|
|
234
|
-
}
|
|
235
|
-
function usageFromRecord(value) {
|
|
236
|
-
if (!record(value) || !keys(value, "cacheWriteInputTokens,cachedInputTokens,inputTokens,outputTokens,reasoningTokens"))
|
|
237
|
-
throw invalid();
|
|
238
|
-
const inputTokens = value["inputTokens"];
|
|
239
|
-
const outputTokens = value["outputTokens"];
|
|
240
|
-
const cachedInputTokens = value["cachedInputTokens"];
|
|
241
|
-
const cacheWriteInputTokens = value["cacheWriteInputTokens"];
|
|
242
|
-
const reasoningTokens = value["reasoningTokens"];
|
|
243
|
-
if (!integer(inputTokens, 0) || !integer(outputTokens, 0) ||
|
|
244
|
-
!integer(cachedInputTokens, 0) || !integer(cacheWriteInputTokens, 0) ||
|
|
245
|
-
!integer(reasoningTokens, 0))
|
|
246
|
-
throw invalid();
|
|
247
|
-
return {
|
|
248
|
-
inputTokens,
|
|
249
|
-
outputTokens,
|
|
250
|
-
cachedInputTokens,
|
|
251
|
-
cacheWriteInputTokens,
|
|
252
|
-
reasoningTokens,
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
function blockRecord(block) {
|
|
256
|
-
if (block.kind === "notice")
|
|
257
|
-
return [];
|
|
258
|
-
if (block.kind === "user" || block.kind === "answer" || block.kind === "reasoning") {
|
|
259
|
-
return [{ kind: block.kind, text: block.text }];
|
|
260
|
-
}
|
|
261
|
-
if (block.tone === "pending")
|
|
262
|
-
return [];
|
|
263
|
-
return [{
|
|
264
|
-
kind: block.kind,
|
|
265
|
-
name: block.name,
|
|
266
|
-
target: block.target,
|
|
267
|
-
right: block.right,
|
|
268
|
-
tone: block.tone,
|
|
269
|
-
body: block.body?.map(detailRecord) ?? null,
|
|
270
|
-
durationMs: block.durationMs ?? null,
|
|
271
|
-
}];
|
|
272
|
-
}
|
|
273
|
-
function blockFromRecord(value, version) {
|
|
274
|
-
if (!record(value) || typeof value["kind"] !== "string")
|
|
275
|
-
throw invalid();
|
|
276
|
-
if ((value["kind"] === "user" || value["kind"] === "answer" || value["kind"] === "reasoning") &&
|
|
277
|
-
keys(value, "kind,text") && boundedText(value["text"]))
|
|
278
|
-
return { kind: value["kind"], text: value["text"] };
|
|
279
|
-
const toolKeys = version >= 4
|
|
280
|
-
? "body,durationMs,kind,name,right,target,tone"
|
|
281
|
-
: "body,kind,name,right,target,tone";
|
|
282
|
-
if (value["kind"] === "tool" && keys(value, toolKeys) &&
|
|
283
|
-
bounded(value["name"], 256) && boundedText(value["target"]) &&
|
|
284
|
-
boundedText(value["right"], 1_024) &&
|
|
285
|
-
(value["tone"] === "ok" || value["tone"] === "fail" || value["tone"] === "deny") &&
|
|
286
|
-
(version < 4 || nullableInteger(value["durationMs"], 0)) &&
|
|
287
|
-
(value["body"] === null ||
|
|
288
|
-
(Array.isArray(value["body"]) && value["body"].length <= SESSION_FILE_LIMITS.details))) {
|
|
289
|
-
const body = value["body"] === null
|
|
290
|
-
? undefined
|
|
291
|
-
: value["body"].map(detailFromRecord);
|
|
292
|
-
return {
|
|
293
|
-
kind: "tool",
|
|
294
|
-
name: value["name"],
|
|
295
|
-
target: value["target"],
|
|
296
|
-
right: value["right"],
|
|
297
|
-
tone: value["tone"],
|
|
298
|
-
...(body === undefined ? {} : { body }),
|
|
299
|
-
...(version < 4 || value["durationMs"] === null
|
|
300
|
-
? {}
|
|
301
|
-
: { durationMs: value["durationMs"] }),
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
throw invalid();
|
|
305
|
-
}
|
|
306
|
-
function detailRecord(detail) {
|
|
307
|
-
if (detail.kind === "out" || detail.kind === "gap")
|
|
308
|
-
return { kind: detail.kind, text: detail.text };
|
|
309
|
-
return {
|
|
310
|
-
kind: detail.kind,
|
|
311
|
-
text: detail.text,
|
|
312
|
-
oldLine: detail.oldLine ?? null,
|
|
313
|
-
newLine: detail.newLine ?? null,
|
|
314
|
-
emphasis: detail.emphasis ?? null,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
function detailFromRecord(value) {
|
|
318
|
-
if (!record(value) || typeof value["kind"] !== "string")
|
|
319
|
-
throw invalid();
|
|
320
|
-
if ((value["kind"] === "out" || value["kind"] === "gap") &&
|
|
321
|
-
keys(value, "kind,text") && boundedText(value["text"]))
|
|
322
|
-
return { kind: value["kind"], text: value["text"] };
|
|
323
|
-
if ((value["kind"] === "keep" || value["kind"] === "add" || value["kind"] === "del") &&
|
|
324
|
-
keys(value, "emphasis,kind,newLine,oldLine,text") && boundedText(value["text"]) &&
|
|
325
|
-
nullableInteger(value["oldLine"], 1) && nullableInteger(value["newLine"], 1)) {
|
|
326
|
-
const emphasis = emphasisFromRecord(value["emphasis"]);
|
|
327
|
-
return {
|
|
328
|
-
kind: value["kind"],
|
|
329
|
-
text: value["text"],
|
|
330
|
-
...(value["oldLine"] === null ? {} : { oldLine: value["oldLine"] }),
|
|
331
|
-
...(value["newLine"] === null ? {} : { newLine: value["newLine"] }),
|
|
332
|
-
...(emphasis === undefined ? {} : { emphasis }),
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
throw invalid();
|
|
336
|
-
}
|
|
337
|
-
function emphasisFromRecord(value) {
|
|
338
|
-
if (value === null)
|
|
339
|
-
return undefined;
|
|
340
|
-
if (!record(value) || !keys(value, "length,start"))
|
|
341
|
-
throw invalid();
|
|
342
|
-
if (!integer(value["start"], 0) || !integer(value["length"], 1))
|
|
343
|
-
throw invalid();
|
|
344
|
-
return { start: value["start"], length: value["length"] };
|
|
345
|
-
}
|
|
346
|
-
function jsonObject(value) {
|
|
347
|
-
const budget = { nodes: 0 };
|
|
348
|
-
const safe = jsonValue(value, budget, 0);
|
|
349
|
-
if (!record(safe))
|
|
350
|
-
throw invalid();
|
|
351
|
-
return safe;
|
|
352
|
-
}
|
|
353
|
-
function jsonValue(value, budget, depth) {
|
|
354
|
-
budget.nodes++;
|
|
355
|
-
if (budget.nodes > SESSION_FILE_LIMITS.jsonNodes || depth > SESSION_FILE_LIMITS.jsonDepth) {
|
|
356
|
-
throw invalid();
|
|
357
|
-
}
|
|
358
|
-
if (value === null || typeof value === "boolean")
|
|
359
|
-
return value;
|
|
360
|
-
if (typeof value === "string") {
|
|
361
|
-
if (!boundedText(value))
|
|
362
|
-
throw invalid();
|
|
363
|
-
return value;
|
|
364
|
-
}
|
|
365
|
-
if (typeof value === "number") {
|
|
366
|
-
if (!Number.isFinite(value))
|
|
367
|
-
throw invalid();
|
|
368
|
-
return value;
|
|
369
|
-
}
|
|
370
|
-
if (Array.isArray(value)) {
|
|
371
|
-
if (value.length > SESSION_FILE_LIMITS.blocks)
|
|
372
|
-
throw invalid();
|
|
373
|
-
return value.map((item) => jsonValue(item, budget, depth + 1));
|
|
374
|
-
}
|
|
375
|
-
if (!record(value) || Object.keys(value).length > SESSION_FILE_LIMITS.blocks)
|
|
376
|
-
throw invalid();
|
|
377
|
-
const safe = {};
|
|
378
|
-
for (const [name, child] of Object.entries(value)) {
|
|
379
|
-
if (!boundedText(name, 1_024))
|
|
380
|
-
throw invalid();
|
|
381
|
-
safe[name] = jsonValue(child, budget, depth + 1);
|
|
382
|
-
}
|
|
383
|
-
return safe;
|
|
384
|
-
}
|
|
385
|
-
function line(value) {
|
|
386
|
-
return `${JSON.stringify(value, null, 2)}\n`;
|
|
387
|
-
}
|
|
388
|
-
function boundedLine(value, maxBytes) {
|
|
389
|
-
const encoded = line(value);
|
|
390
|
-
if (Buffer.byteLength(encoded, "utf8") > maxBytes)
|
|
391
|
-
throw invalid();
|
|
392
|
-
return encoded;
|
|
393
|
-
}
|
|
394
|
-
function keys(value, expected) {
|
|
395
|
-
return Object.keys(value).sort().join(",") === expected;
|
|
396
|
-
}
|
|
397
|
-
function record(value) {
|
|
398
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
399
|
-
}
|
|
400
|
-
function bounded(value, limit = SESSION_FILE_LIMITS.text) {
|
|
401
|
-
return typeof value === "string" && value.length > 0 && value.length <= limit;
|
|
402
|
-
}
|
|
403
|
-
function boundedText(value, limit = SESSION_FILE_LIMITS.text) {
|
|
404
|
-
return typeof value === "string" && value.length <= limit;
|
|
405
|
-
}
|
|
406
|
-
function integer(value, minimum) {
|
|
407
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
|
|
408
|
-
}
|
|
409
172
|
function schema(value) {
|
|
410
173
|
return value === 1 || value === 2 || value === 3 || value === SESSION_SCHEMA;
|
|
411
174
|
}
|
|
@@ -413,19 +176,3 @@ function settlement(value, version) {
|
|
|
413
176
|
return value === "checkpointed" || value === "completed" ||
|
|
414
177
|
(version >= 3 && (value === "failed" || value === "interrupted"));
|
|
415
178
|
}
|
|
416
|
-
function nullableInteger(value, minimum) {
|
|
417
|
-
return value === null || integer(value, minimum);
|
|
418
|
-
}
|
|
419
|
-
function identifier(value) {
|
|
420
|
-
return typeof value === "string" && /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(value);
|
|
421
|
-
}
|
|
422
|
-
function digest(value) {
|
|
423
|
-
return typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
424
|
-
}
|
|
425
|
-
function timestamp(value) {
|
|
426
|
-
return typeof value === "string" && value.length >= 20 && value.length <= 64 &&
|
|
427
|
-
Number.isFinite(Date.parse(value));
|
|
428
|
-
}
|
|
429
|
-
function invalid() {
|
|
430
|
-
return new Error("session data is invalid or unsupported");
|
|
431
|
-
}
|