@tokenwarden/opencode 0.1.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/README.md +53 -0
- package/dist/src/cli/index.js +105 -0
- package/dist/src/core/billing.js +252 -0
- package/dist/src/core/build-config.js +45 -0
- package/dist/src/core/code-summary.js +162 -0
- package/dist/src/core/format.js +71 -0
- package/dist/src/core/log-reducer.js +97 -0
- package/dist/src/core/protection/engine.js +226 -0
- package/dist/src/core/protection/license-public-key.generated.js +2 -0
- package/dist/src/core/protection/license.js +27 -0
- package/dist/src/core/protection/rule-pack.js +38 -0
- package/dist/src/core/protection/sidecar-public-key.generated.js +2 -0
- package/dist/src/core/protection/sidecar.js +197 -0
- package/dist/src/core/protection/signing.js +28 -0
- package/dist/src/core/protection/tamper.js +18 -0
- package/dist/src/core/savings.js +91 -0
- package/dist/src/core/storage.js +123 -0
- package/dist/src/core/tokens.js +40 -0
- package/dist/src/plugin/index.js +492 -0
- package/native/bin/tokenwarden-build-config.json +4 -0
- package/native/bin/tokenwarden-engine-darwin-arm64 +0 -0
- package/native/bin/tokenwarden-engine-darwin-arm64.manifest.json +17 -0
- package/native/bin/tokenwarden-engine-darwin-x64 +0 -0
- package/native/bin/tokenwarden-engine-darwin-x64.manifest.json +17 -0
- package/native/bin/tokenwarden-engine-linux-arm64 +0 -0
- package/native/bin/tokenwarden-engine-linux-arm64.manifest.json +17 -0
- package/native/bin/tokenwarden-engine-linux-x64 +0 -0
- package/native/bin/tokenwarden-engine-linux-x64.manifest.json +17 -0
- package/native/bin/tokenwarden-engine-win32-x64.exe +0 -0
- package/native/bin/tokenwarden-engine-win32-x64.exe.manifest.json +17 -0
- package/package.json +33 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
|
|
2
|
+
export function canonicalJson(value) {
|
|
3
|
+
return JSON.stringify(sortForCanonicalJson(value));
|
|
4
|
+
}
|
|
5
|
+
export function signEnvelope(payload, privateKeyPem) {
|
|
6
|
+
const key = createPrivateKey(privateKeyPem);
|
|
7
|
+
const signature = sign(null, Buffer.from(canonicalJson(payload), "utf8"), key).toString("base64url");
|
|
8
|
+
return {
|
|
9
|
+
alg: "Ed25519",
|
|
10
|
+
payload,
|
|
11
|
+
signature,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function verifyEnvelope(envelope, publicKeyPem) {
|
|
15
|
+
if (envelope.alg !== "Ed25519")
|
|
16
|
+
return false;
|
|
17
|
+
const key = createPublicKey(publicKeyPem);
|
|
18
|
+
return verify(null, Buffer.from(canonicalJson(envelope.payload), "utf8"), key, Buffer.from(envelope.signature, "base64url"));
|
|
19
|
+
}
|
|
20
|
+
function sortForCanonicalJson(value) {
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return value.map(sortForCanonicalJson);
|
|
23
|
+
if (!value || typeof value !== "object")
|
|
24
|
+
return value;
|
|
25
|
+
return Object.fromEntries(Object.entries(value)
|
|
26
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
27
|
+
.map(([key, item]) => [key, sortForCanonicalJson(item)]));
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
export async function sha256File(filePath) {
|
|
4
|
+
return createHash("sha256").update(await readFile(filePath)).digest("hex");
|
|
5
|
+
}
|
|
6
|
+
export async function verifyFileChecksum(filePath, expectedSha256) {
|
|
7
|
+
let actual;
|
|
8
|
+
try {
|
|
9
|
+
actual = await sha256File(filePath);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
return { ok: false, reason: `could not read file: ${error.message}` };
|
|
13
|
+
}
|
|
14
|
+
if (actual !== expectedSha256) {
|
|
15
|
+
return { ok: false, sha256: actual, reason: "checksum mismatch" };
|
|
16
|
+
}
|
|
17
|
+
return { ok: true, sha256: actual };
|
|
18
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const SAVING_SOURCES = new Set(["tool-output", "smart-read", "smart-pack", "chat-history", "manual"]);
|
|
2
|
+
export function createSavingsEvent(input) {
|
|
3
|
+
const wouldHaveUsedTokens = tokenCount(input.wouldHaveUsedTokens);
|
|
4
|
+
const usedTokens = tokenCount(input.usedTokens);
|
|
5
|
+
const savedTokens = Math.max(0, wouldHaveUsedTokens - usedTokens);
|
|
6
|
+
const percentSaved = calculatePercent(savedTokens, wouldHaveUsedTokens);
|
|
7
|
+
return {
|
|
8
|
+
timestamp: new Date().toISOString(),
|
|
9
|
+
sessionID: input.sessionID ?? "unknown",
|
|
10
|
+
source: input.source,
|
|
11
|
+
label: input.label,
|
|
12
|
+
wouldHaveUsedTokens,
|
|
13
|
+
usedTokens,
|
|
14
|
+
savedTokens,
|
|
15
|
+
percentSaved,
|
|
16
|
+
metadata: input.metadata,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function normalizeSavingsEvent(value) {
|
|
20
|
+
if (!value || typeof value !== "object")
|
|
21
|
+
return undefined;
|
|
22
|
+
const event = value;
|
|
23
|
+
const savedFromEvent = tokenCount(event.savedTokens);
|
|
24
|
+
const usedTokens = tokenCount(event.usedTokens);
|
|
25
|
+
let wouldHaveUsedTokens = tokenCount(event.wouldHaveUsedTokens ?? event.rawTokens);
|
|
26
|
+
if (wouldHaveUsedTokens === 0 && savedFromEvent > 0)
|
|
27
|
+
wouldHaveUsedTokens = usedTokens + savedFromEvent;
|
|
28
|
+
const savedTokens = Math.max(0, wouldHaveUsedTokens - usedTokens);
|
|
29
|
+
const source = SAVING_SOURCES.has(event.source) ? event.source : "manual";
|
|
30
|
+
return {
|
|
31
|
+
timestamp: typeof event.timestamp === "string" && event.timestamp ? event.timestamp : "1970-01-01T00:00:00.000Z",
|
|
32
|
+
sessionID: typeof event.sessionID === "string" && event.sessionID ? event.sessionID : "unknown",
|
|
33
|
+
source,
|
|
34
|
+
label: typeof event.label === "string" && event.label ? event.label : "unknown",
|
|
35
|
+
wouldHaveUsedTokens,
|
|
36
|
+
usedTokens,
|
|
37
|
+
savedTokens,
|
|
38
|
+
percentSaved: calculatePercent(savedTokens, wouldHaveUsedTokens),
|
|
39
|
+
metadata: event.metadata && typeof event.metadata === "object" ? event.metadata : undefined,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function summarizeSavings(events) {
|
|
43
|
+
const normalizedEvents = events.flatMap((event) => {
|
|
44
|
+
const normalized = normalizeSavingsEvent(event);
|
|
45
|
+
return normalized ? [normalized] : [];
|
|
46
|
+
});
|
|
47
|
+
const summary = {
|
|
48
|
+
events: normalizedEvents.length,
|
|
49
|
+
sessions: 0,
|
|
50
|
+
wouldHaveUsedTokens: 0,
|
|
51
|
+
usedTokens: 0,
|
|
52
|
+
savedTokens: 0,
|
|
53
|
+
percentSaved: 0,
|
|
54
|
+
bySource: {},
|
|
55
|
+
};
|
|
56
|
+
const sessionIDs = new Set();
|
|
57
|
+
for (const event of normalizedEvents) {
|
|
58
|
+
sessionIDs.add(event.sessionID);
|
|
59
|
+
summary.wouldHaveUsedTokens += event.wouldHaveUsedTokens;
|
|
60
|
+
summary.usedTokens += event.usedTokens;
|
|
61
|
+
summary.savedTokens += event.savedTokens;
|
|
62
|
+
const source = (summary.bySource[event.source] ??= {
|
|
63
|
+
events: 0,
|
|
64
|
+
wouldHaveUsedTokens: 0,
|
|
65
|
+
usedTokens: 0,
|
|
66
|
+
savedTokens: 0,
|
|
67
|
+
percentSaved: 0,
|
|
68
|
+
});
|
|
69
|
+
source.events += 1;
|
|
70
|
+
source.wouldHaveUsedTokens += event.wouldHaveUsedTokens;
|
|
71
|
+
source.usedTokens += event.usedTokens;
|
|
72
|
+
source.savedTokens += event.savedTokens;
|
|
73
|
+
}
|
|
74
|
+
summary.sessions = sessionIDs.size;
|
|
75
|
+
summary.percentSaved = calculatePercent(summary.savedTokens, summary.wouldHaveUsedTokens);
|
|
76
|
+
for (const source of Object.values(summary.bySource)) {
|
|
77
|
+
source.percentSaved = calculatePercent(source.savedTokens, source.wouldHaveUsedTokens);
|
|
78
|
+
}
|
|
79
|
+
return summary;
|
|
80
|
+
}
|
|
81
|
+
function calculatePercent(saved, wouldHaveUsed) {
|
|
82
|
+
if (!Number.isFinite(saved) || !Number.isFinite(wouldHaveUsed) || wouldHaveUsed <= 0)
|
|
83
|
+
return 0;
|
|
84
|
+
return Math.round((saved / wouldHaveUsed) * 1000) / 10;
|
|
85
|
+
}
|
|
86
|
+
function tokenCount(value) {
|
|
87
|
+
const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : 0;
|
|
88
|
+
if (!Number.isFinite(parsed))
|
|
89
|
+
return 0;
|
|
90
|
+
return Math.max(0, Math.round(parsed));
|
|
91
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { appendFile, mkdir, open, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
5
|
+
import { normalizeSavingsEvent, summarizeSavings } from "./savings.js";
|
|
6
|
+
import { encryptedAppendFile, encryptedReadFile, encryptedWriteFileFromPath } from "./protection/sidecar.js";
|
|
7
|
+
const ENCRYPTED_APPEND_LOCK_TIMEOUT_MS = 15_000;
|
|
8
|
+
const ENCRYPTED_APPEND_LOCK_RETRY_MS = 25;
|
|
9
|
+
export function createStore(dataDir = defaultDataDir()) {
|
|
10
|
+
const encryptedDir = join(dataDir, "encrypted");
|
|
11
|
+
return {
|
|
12
|
+
dataDir,
|
|
13
|
+
eventsPath: join(encryptedDir, "usage.jsonl.enc"),
|
|
14
|
+
rawOutputDir: join(encryptedDir, "raw-tool-output"),
|
|
15
|
+
failureLogPath: join(dataDir, "logs", "failures.jsonl"),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function defaultDataDir() {
|
|
19
|
+
if (process.env.TOKENWARDEN_HOME)
|
|
20
|
+
return process.env.TOKENWARDEN_HOME;
|
|
21
|
+
if (process.env.XDG_CACHE_HOME)
|
|
22
|
+
return join(process.env.XDG_CACHE_HOME, "tokenwarden");
|
|
23
|
+
return join(homedir(), ".cache", "tokenwarden");
|
|
24
|
+
}
|
|
25
|
+
export async function appendSavingsEvent(store, event) {
|
|
26
|
+
await withFileLock(`${store.eventsPath}.lock`, async () => {
|
|
27
|
+
await encryptedAppendFile(store.eventsPath, `${JSON.stringify(event)}\n`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export async function readSavingsEvents(store, filter = {}) {
|
|
31
|
+
let content;
|
|
32
|
+
try {
|
|
33
|
+
content = await encryptedReadFile(store.eventsPath);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
if (!content)
|
|
39
|
+
return [];
|
|
40
|
+
const events = content
|
|
41
|
+
.split(/\r?\n/)
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.flatMap((line) => {
|
|
44
|
+
try {
|
|
45
|
+
const event = normalizeSavingsEvent(JSON.parse(line));
|
|
46
|
+
return event ? [event] : [];
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (!filter.sessionID)
|
|
53
|
+
return events;
|
|
54
|
+
return events.filter((event) => event.sessionID === filter.sessionID);
|
|
55
|
+
}
|
|
56
|
+
export async function summarizeStore(store, filter = {}) {
|
|
57
|
+
return summarizeSavings(await readSavingsEvents(store, filter));
|
|
58
|
+
}
|
|
59
|
+
export async function appendFailureLog(store, log) {
|
|
60
|
+
const entry = {
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
...log,
|
|
63
|
+
};
|
|
64
|
+
await mkdir(dirname(store.failureLogPath), { recursive: true });
|
|
65
|
+
await appendFile(store.failureLogPath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
66
|
+
}
|
|
67
|
+
export async function writeRawOutput(store, input) {
|
|
68
|
+
const safeSession = safePathPart(input.sessionID);
|
|
69
|
+
const safeCall = safePathPart(input.callID);
|
|
70
|
+
const filePath = join(store.rawOutputDir, safeSession, `${safeCall}.txt.enc`);
|
|
71
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
72
|
+
await mkdir(dirname(tempPath), { recursive: true });
|
|
73
|
+
try {
|
|
74
|
+
await writeFile(tempPath, input.output, "utf8");
|
|
75
|
+
await encryptedWriteFileFromPath(filePath, tempPath);
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await rm(tempPath, { force: true });
|
|
79
|
+
}
|
|
80
|
+
return filePath;
|
|
81
|
+
}
|
|
82
|
+
function safePathPart(value) {
|
|
83
|
+
return value.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 120) || "unknown";
|
|
84
|
+
}
|
|
85
|
+
async function withFileLock(lockPath, run) {
|
|
86
|
+
const handle = await acquireFileLock(lockPath);
|
|
87
|
+
try {
|
|
88
|
+
return await run();
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
try {
|
|
92
|
+
await handle.close();
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await rm(lockPath, { force: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function acquireFileLock(lockPath) {
|
|
100
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
101
|
+
const deadline = Date.now() + ENCRYPTED_APPEND_LOCK_TIMEOUT_MS;
|
|
102
|
+
for (;;) {
|
|
103
|
+
try {
|
|
104
|
+
const handle = await open(lockPath, "wx");
|
|
105
|
+
try {
|
|
106
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })}\n`, "utf8");
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
await handle.close();
|
|
110
|
+
await rm(lockPath, { force: true });
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
return handle;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error.code !== "EEXIST")
|
|
117
|
+
throw error;
|
|
118
|
+
if (Date.now() >= deadline)
|
|
119
|
+
throw new Error(`timed out waiting for encrypted storage lock: ${lockPath}`);
|
|
120
|
+
await sleep(ENCRYPTED_APPEND_LOCK_RETRY_MS);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function countTokens(value) {
|
|
2
|
+
const text = stringifyForTokens(value);
|
|
3
|
+
if (text.length === 0)
|
|
4
|
+
return 0;
|
|
5
|
+
const words = text.match(/[\p{L}\p{N}_]+|[^\s\p{L}\p{N}_]/gu) ?? [];
|
|
6
|
+
const charEstimate = Math.ceil(text.length / 4);
|
|
7
|
+
const wordEstimate = Math.ceil(words.length * 0.75);
|
|
8
|
+
return Math.max(1, Math.max(charEstimate, wordEstimate));
|
|
9
|
+
}
|
|
10
|
+
export function stringifyForTokens(value) {
|
|
11
|
+
if (typeof value === "string")
|
|
12
|
+
return value;
|
|
13
|
+
if (value === undefined || value === null)
|
|
14
|
+
return "";
|
|
15
|
+
try {
|
|
16
|
+
return JSON.stringify(value);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return String(value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function trimToTokenBudget(text, budget) {
|
|
23
|
+
if (budget <= 0)
|
|
24
|
+
return "";
|
|
25
|
+
if (countTokens(text) <= budget)
|
|
26
|
+
return text;
|
|
27
|
+
const lines = text.split(/\r?\n/);
|
|
28
|
+
const kept = [];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
const next = [...kept, line].join("\n");
|
|
31
|
+
if (countTokens(next) > budget)
|
|
32
|
+
break;
|
|
33
|
+
kept.push(line);
|
|
34
|
+
}
|
|
35
|
+
if (kept.length === 0) {
|
|
36
|
+
const approximateChars = Math.max(1, budget * 4);
|
|
37
|
+
return text.slice(0, approximateChars);
|
|
38
|
+
}
|
|
39
|
+
return kept.join("\n");
|
|
40
|
+
}
|