sidekick-shared 0.23.0 → 0.24.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 +139 -57
- package/dist/accountManager.js +10 -0
- package/dist/aggregation/EventAggregator.d.ts +8 -1
- package/dist/aggregation/EventAggregator.js +82 -136
- package/dist/aggregation/snapshot.d.ts +1 -1
- package/dist/aggregation/snapshot.js +2 -1
- package/dist/aggregation/types.d.ts +5 -0
- package/dist/autoSwitch.d.ts +1 -1
- package/dist/autoSwitch.js +14 -3
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +10 -1
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +27 -0
- package/dist/codexQuotaWatcher.d.ts +7 -0
- package/dist/codexQuotaWatcher.js +37 -19
- package/dist/context/sessionContext.js +43 -15
- package/dist/credentialIO.js +19 -5
- package/dist/errorHistory.d.ts +1 -0
- package/dist/errorHistory.js +3 -2
- package/dist/extractors/tokenUsage.js +1 -1
- package/dist/extractors/toolCall.js +2 -2
- package/dist/index.d.ts +14 -4
- package/dist/index.js +25 -7
- package/dist/modelInfo.d.ts +17 -7
- package/dist/modelInfo.js +11 -2
- package/dist/multiProviderQuotaService.js +13 -2
- package/dist/node.d.ts +4 -0
- package/dist/node.js +8 -1
- package/dist/observedSessionCollector.d.ts +68 -0
- package/dist/observedSessionCollector.js +178 -0
- package/dist/parsers/codexParser.js +24 -6
- package/dist/parsers/openCodeParser.js +22 -1
- package/dist/parsers/sessionActivityDetector.js +2 -0
- package/dist/parsers/sessionPathResolver.js +19 -16
- package/dist/parsers/subagentScanner.d.ts +1 -1
- package/dist/parsers/subagentScanner.js +4 -5
- package/dist/parsers/subagentTraceParser.js +15 -12
- package/dist/providerStatus.js +13 -2
- package/dist/providers/claudeCode.js +63 -5
- package/dist/providers/codex.js +56 -17
- package/dist/providers/detect.js +32 -3
- package/dist/providers/openCodeDatabase.js +12 -3
- package/dist/quota.js +8 -0
- package/dist/quotaPoller.js +17 -6
- package/dist/quotaSnapshots.js +80 -26
- package/dist/readers/decisions.js +3 -1
- package/dist/readers/handoff.js +1 -1
- package/dist/readers/notes.js +9 -4
- package/dist/readers/plans.js +2 -4
- package/dist/readers/tasks.js +3 -1
- package/dist/report/htmlHelpers.js +6 -1
- package/dist/report/openBrowser.js +3 -3
- package/dist/report/transcriptParser.d.ts +3 -11
- package/dist/report/transcriptParser.js +69 -154
- package/dist/schemas/index.d.ts +7 -1
- package/dist/schemas/index.js +18 -2
- package/dist/schemas/observedSessionV1.d.ts +32 -1
- package/dist/schemas/observedSessionV1.js +11 -1
- package/dist/schemas/sessionEvent.d.ts +353 -12
- package/dist/schemas/sessionEvent.js +76 -14
- package/dist/schemas/tokenEstimation.d.ts +14 -0
- package/dist/schemas/tokenEstimation.js +10 -0
- package/dist/schemas/transcript.d.ts +24 -0
- package/dist/schemas/transcript.js +81 -0
- package/dist/schemas/usageNormalization.d.ts +55 -0
- package/dist/schemas/usageNormalization.js +41 -0
- package/dist/sessionTranscripts.d.ts +16 -0
- package/dist/sessionTranscripts.js +47 -0
- package/dist/statusline/formatter.js +8 -1
- package/dist/terminalSync.js +16 -7
- package/dist/tokenEstimation.d.ts +24 -0
- package/dist/tokenEstimation.js +62 -0
- package/dist/transcript.d.ts +92 -0
- package/dist/transcript.js +240 -0
- package/dist/types/historicalData.d.ts +14 -0
- package/dist/types/historicalData.js +22 -0
- package/dist/types/observedSessionV1.d.ts +3 -1
- package/dist/types/observedSessionV1.js +20 -6
- package/dist/types/sessionEvent.d.ts +66 -7
- package/dist/usageNormalization.d.ts +69 -0
- package/dist/usageNormalization.js +100 -0
- package/dist/watchers/factory.js +14 -3
- package/dist/watchers/jsonlTail.js +34 -14
- package/dist/watchers/jsonlWatcher.js +5 -3
- package/dist/watchers/providerReaderWatcher.js +4 -2
- package/dist/watchers/sqliteWatcher.d.ts +2 -0
- package/dist/watchers/sqliteWatcher.js +27 -14
- package/dist/writers/atomic.js +100 -13
- package/dist/zaiQuotaWatcher.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const normalizedUsageSchema: z.ZodObject<{
|
|
3
|
+
uncachedInputTokens: z.ZodNumber;
|
|
4
|
+
cacheReadTokens: z.ZodNumber;
|
|
5
|
+
cacheWriteTokens: z.ZodNumber;
|
|
6
|
+
outputTokens: z.ZodNumber;
|
|
7
|
+
reasoningTokens: z.ZodNumber;
|
|
8
|
+
reasoningIncludedInOutput: z.ZodBoolean;
|
|
9
|
+
cacheInclusiveInputTokens: z.ZodNumber;
|
|
10
|
+
billableOutputTokens: z.ZodNumber;
|
|
11
|
+
totalTokens: z.ZodNumber;
|
|
12
|
+
model: z.ZodOptional<z.ZodString>;
|
|
13
|
+
reportedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
provenance: z.ZodObject<{
|
|
15
|
+
provider: z.ZodOptional<z.ZodString>;
|
|
16
|
+
semantics: z.ZodEnum<{
|
|
17
|
+
"anthropic-additive-cache": "anthropic-additive-cache";
|
|
18
|
+
"openai-inclusive-cache": "openai-inclusive-cache";
|
|
19
|
+
"sidekick-disjoint": "sidekick-disjoint";
|
|
20
|
+
}>;
|
|
21
|
+
source: z.ZodOptional<z.ZodString>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export declare const normalizedUsageCostSchema: z.ZodObject<{
|
|
25
|
+
costUsd: z.ZodNullable<z.ZodNumber>;
|
|
26
|
+
source: z.ZodEnum<{
|
|
27
|
+
unpriced: "unpriced";
|
|
28
|
+
"provider-reported": "provider-reported";
|
|
29
|
+
"explicit-pricing": "explicit-pricing";
|
|
30
|
+
"model-catalog": "model-catalog";
|
|
31
|
+
}>;
|
|
32
|
+
modelId: z.ZodOptional<z.ZodString>;
|
|
33
|
+
usage: z.ZodObject<{
|
|
34
|
+
uncachedInputTokens: z.ZodNumber;
|
|
35
|
+
cacheReadTokens: z.ZodNumber;
|
|
36
|
+
cacheWriteTokens: z.ZodNumber;
|
|
37
|
+
outputTokens: z.ZodNumber;
|
|
38
|
+
reasoningTokens: z.ZodNumber;
|
|
39
|
+
reasoningIncludedInOutput: z.ZodBoolean;
|
|
40
|
+
cacheInclusiveInputTokens: z.ZodNumber;
|
|
41
|
+
billableOutputTokens: z.ZodNumber;
|
|
42
|
+
totalTokens: z.ZodNumber;
|
|
43
|
+
model: z.ZodOptional<z.ZodString>;
|
|
44
|
+
reportedCostUsd: z.ZodOptional<z.ZodNumber>;
|
|
45
|
+
provenance: z.ZodObject<{
|
|
46
|
+
provider: z.ZodOptional<z.ZodString>;
|
|
47
|
+
semantics: z.ZodEnum<{
|
|
48
|
+
"anthropic-additive-cache": "anthropic-additive-cache";
|
|
49
|
+
"openai-inclusive-cache": "openai-inclusive-cache";
|
|
50
|
+
"sidekick-disjoint": "sidekick-disjoint";
|
|
51
|
+
}>;
|
|
52
|
+
source: z.ZodOptional<z.ZodString>;
|
|
53
|
+
}, z.core.$strip>;
|
|
54
|
+
}, z.core.$strip>;
|
|
55
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizedUsageCostSchema = exports.normalizedUsageSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const normalizedUsageObjectSchema = zod_1.z.object({
|
|
6
|
+
uncachedInputTokens: zod_1.z.number().finite().nonnegative(),
|
|
7
|
+
cacheReadTokens: zod_1.z.number().finite().nonnegative(),
|
|
8
|
+
cacheWriteTokens: zod_1.z.number().finite().nonnegative(),
|
|
9
|
+
outputTokens: zod_1.z.number().finite().nonnegative(),
|
|
10
|
+
reasoningTokens: zod_1.z.number().finite().nonnegative(),
|
|
11
|
+
reasoningIncludedInOutput: zod_1.z.boolean(),
|
|
12
|
+
cacheInclusiveInputTokens: zod_1.z.number().finite().nonnegative(),
|
|
13
|
+
billableOutputTokens: zod_1.z.number().finite().nonnegative(),
|
|
14
|
+
totalTokens: zod_1.z.number().finite().nonnegative(),
|
|
15
|
+
model: zod_1.z.string().optional(),
|
|
16
|
+
reportedCostUsd: zod_1.z.number().finite().nonnegative().optional(),
|
|
17
|
+
provenance: zod_1.z.object({
|
|
18
|
+
provider: zod_1.z.string().optional(),
|
|
19
|
+
semantics: zod_1.z.enum(['anthropic-additive-cache', 'openai-inclusive-cache', 'sidekick-disjoint']),
|
|
20
|
+
source: zod_1.z.string().optional(),
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
exports.normalizedUsageSchema = normalizedUsageObjectSchema.superRefine((usage, context) => {
|
|
24
|
+
const expectedInput = usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
|
|
25
|
+
const expectedOutput = usage.outputTokens + (usage.reasoningIncludedInOutput ? 0 : usage.reasoningTokens);
|
|
26
|
+
if (usage.cacheInclusiveInputTokens !== expectedInput) {
|
|
27
|
+
context.addIssue({ code: 'custom', message: 'cacheInclusiveInputTokens is inconsistent' });
|
|
28
|
+
}
|
|
29
|
+
if (usage.billableOutputTokens !== expectedOutput) {
|
|
30
|
+
context.addIssue({ code: 'custom', message: 'billableOutputTokens is inconsistent' });
|
|
31
|
+
}
|
|
32
|
+
if (usage.totalTokens !== expectedInput + expectedOutput) {
|
|
33
|
+
context.addIssue({ code: 'custom', message: 'totalTokens is inconsistent' });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
exports.normalizedUsageCostSchema = zod_1.z.object({
|
|
37
|
+
costUsd: zod_1.z.number().finite().nonnegative().nullable(),
|
|
38
|
+
source: zod_1.z.enum(['provider-reported', 'explicit-pricing', 'model-catalog', 'unpriced']),
|
|
39
|
+
modelId: zod_1.z.string().optional(),
|
|
40
|
+
usage: exports.normalizedUsageSchema,
|
|
41
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SessionProviderBase, ProviderId } from './providers/types';
|
|
2
|
+
import { type CanonicalSessionTranscript, type ProjectSessionTranscriptOptions } from './transcript';
|
|
3
|
+
export interface ProviderSessionIndex {
|
|
4
|
+
provider: ProviderId;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
sessionPath: string;
|
|
7
|
+
label: string | null;
|
|
8
|
+
modifiedAt: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ListRecentSessionsOptions {
|
|
11
|
+
limit?: number;
|
|
12
|
+
}
|
|
13
|
+
/** List recent sessions using the provider's existing discovery and metadata APIs. */
|
|
14
|
+
export declare function listRecentSessions(provider: SessionProviderBase, workspacePath: string, options?: ListRecentSessionsOptions): ProviderSessionIndex[];
|
|
15
|
+
/** Read a canonical transcript through a provider's existing reader. */
|
|
16
|
+
export declare function readSessionTranscript(provider: SessionProviderBase, sessionPath: string, options?: Omit<ProjectSessionTranscriptOptions, 'provider' | 'sessionId' | 'sourcePath'>): CanonicalSessionTranscript;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listRecentSessions = listRecentSessions;
|
|
4
|
+
exports.readSessionTranscript = readSessionTranscript;
|
|
5
|
+
const node_fs_1 = require("node:fs");
|
|
6
|
+
const transcript_1 = require("./transcript");
|
|
7
|
+
/** List recent sessions using the provider's existing discovery and metadata APIs. */
|
|
8
|
+
function listRecentSessions(provider, workspacePath, options = {}) {
|
|
9
|
+
const limit = Math.max(0, options.limit ?? 20);
|
|
10
|
+
return provider
|
|
11
|
+
.findAllSessions(workspacePath)
|
|
12
|
+
.map((sessionPath) => ({
|
|
13
|
+
provider: provider.id,
|
|
14
|
+
sessionId: provider.getSessionId(sessionPath),
|
|
15
|
+
sessionPath,
|
|
16
|
+
label: provider.extractSessionLabel(sessionPath),
|
|
17
|
+
modifiedAt: sessionModifiedAt(provider, sessionPath).toISOString(),
|
|
18
|
+
}))
|
|
19
|
+
.sort((left, right) => right.modifiedAt.localeCompare(left.modifiedAt))
|
|
20
|
+
.slice(0, limit);
|
|
21
|
+
}
|
|
22
|
+
function sessionModifiedAt(provider, sessionPath) {
|
|
23
|
+
const metadata = provider.getSessionMetadata?.(sessionPath);
|
|
24
|
+
if (metadata)
|
|
25
|
+
return metadata.mtime;
|
|
26
|
+
try {
|
|
27
|
+
return (0, node_fs_1.statSync)(sessionPath).mtime;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return new Date(0);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Read a canonical transcript through a provider's existing reader. */
|
|
34
|
+
function readSessionTranscript(provider, sessionPath, options = {}) {
|
|
35
|
+
const reader = provider.createReader(sessionPath);
|
|
36
|
+
try {
|
|
37
|
+
return (0, transcript_1.projectSessionTranscript)(reader.readAll(), {
|
|
38
|
+
...options,
|
|
39
|
+
provider: provider.id,
|
|
40
|
+
sessionId: provider.getSessionId(sessionPath),
|
|
41
|
+
sourcePath: sessionPath,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
reader.flush();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -42,13 +42,20 @@ function estimateWindowEta(quota, now) {
|
|
|
42
42
|
const resetAt = Date.parse(quota.fiveHour.resetsAt);
|
|
43
43
|
if (!Number.isFinite(resetAt))
|
|
44
44
|
return null;
|
|
45
|
+
const nowMs = now.getTime();
|
|
46
|
+
if (resetAt <= nowMs)
|
|
47
|
+
return null;
|
|
45
48
|
const capturedAt = quota.capturedAt ? Date.parse(quota.capturedAt) : now.getTime();
|
|
46
49
|
const sampleAt = Number.isFinite(capturedAt) ? capturedAt : now.getTime();
|
|
50
|
+
if (sampleAt >= resetAt)
|
|
51
|
+
return null;
|
|
47
52
|
const elapsedMinutes = (sampleAt - (resetAt - quota_1.FIVE_HOUR_WINDOW_MS)) / 60_000;
|
|
48
53
|
if (elapsedMinutes <= 0 || quota.fiveHour.utilization <= 0)
|
|
49
54
|
return null;
|
|
50
55
|
const burnRate = quota.fiveHour.utilization / elapsedMinutes;
|
|
51
|
-
|
|
56
|
+
const eta = (0, BurnRateCalculator_1.estimateTimeToQuota)(quota.fiveHour.utilization, 100, burnRate);
|
|
57
|
+
const minutesUntilReset = (resetAt - nowMs) / 60_000;
|
|
58
|
+
return eta !== null && eta < minutesUntilReset ? eta : null;
|
|
52
59
|
}
|
|
53
60
|
function compactLabel(value) {
|
|
54
61
|
const trimmed = value.trim();
|
package/dist/terminalSync.js
CHANGED
|
@@ -107,20 +107,28 @@ function buildShellHookBlock() {
|
|
|
107
107
|
function getShellRcPaths() {
|
|
108
108
|
const zshrc = path.join(os.homedir(), '.zshrc');
|
|
109
109
|
const bashrc = path.join(os.homedir(), '.bashrc');
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
const existing = [zshrc, bashrc].filter((filePath) => fs.existsSync(filePath));
|
|
111
|
+
if (existing.length > 0)
|
|
112
|
+
return existing;
|
|
113
|
+
const shell = path.basename(process.env.SHELL ?? 'zsh');
|
|
114
|
+
return shell === 'bash' ? [bashrc] : [zshrc];
|
|
115
|
+
}
|
|
116
|
+
function resolveShellRcTarget(filePath) {
|
|
117
|
+
if (!fs.existsSync(filePath))
|
|
118
|
+
return { filePath, mode: 0o600 };
|
|
119
|
+
const realPath = fs.realpathSync(filePath);
|
|
120
|
+
return { filePath: realPath, mode: fs.statSync(realPath).mode & 0o777 };
|
|
114
121
|
}
|
|
115
122
|
function stripShellHook(content) {
|
|
116
123
|
const pattern = new RegExp(`${HOOK_START}[\\s\\S]*?${HOOK_END}\\n?`, 'g');
|
|
117
124
|
return content.replace(pattern, '').replace(/\n{3,}/g, '\n\n');
|
|
118
125
|
}
|
|
119
126
|
function installHookInFile(filePath) {
|
|
120
|
-
const
|
|
127
|
+
const target = resolveShellRcTarget(filePath);
|
|
128
|
+
const existing = fs.existsSync(target.filePath) ? fs.readFileSync(target.filePath, 'utf8') : '';
|
|
121
129
|
const cleaned = stripShellHook(existing).replace(/\s*$/, '');
|
|
122
130
|
const next = cleaned ? `${cleaned}\n\n${buildShellHookBlock()}` : buildShellHookBlock();
|
|
123
|
-
atomicWriteFile(filePath, next,
|
|
131
|
+
atomicWriteFile(target.filePath, next, target.mode);
|
|
124
132
|
}
|
|
125
133
|
function installShellHook() {
|
|
126
134
|
for (const rcPath of getShellRcPaths()) {
|
|
@@ -130,7 +138,8 @@ function installShellHook() {
|
|
|
130
138
|
function uninstallHookInFile(filePath) {
|
|
131
139
|
if (!fs.existsSync(filePath))
|
|
132
140
|
return;
|
|
133
|
-
|
|
141
|
+
const target = resolveShellRcTarget(filePath);
|
|
142
|
+
atomicWriteFile(target.filePath, stripShellHook(fs.readFileSync(target.filePath, 'utf8')), target.mode);
|
|
134
143
|
}
|
|
135
144
|
function uninstallShellHook() {
|
|
136
145
|
for (const rcPath of getShellRcPaths()) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type TokenEstimationMethod = 'exact' | 'sidekick-fallback-v1';
|
|
2
|
+
export type TokenEstimationConfidence = 'exact' | 'medium' | 'low';
|
|
3
|
+
export interface ExactTokenCounterContext {
|
|
4
|
+
model?: string;
|
|
5
|
+
}
|
|
6
|
+
export type ExactTokenCounter = (text: string, context: ExactTokenCounterContext) => number | null | undefined;
|
|
7
|
+
export interface TokenEstimationOptions extends ExactTokenCounterContext {
|
|
8
|
+
exactCounter?: ExactTokenCounter;
|
|
9
|
+
}
|
|
10
|
+
export interface TokenEstimate {
|
|
11
|
+
count: number;
|
|
12
|
+
method: TokenEstimationMethod;
|
|
13
|
+
confidence: TokenEstimationConfidence;
|
|
14
|
+
provenance: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Estimate tokens without bundling a tokenizer.
|
|
18
|
+
*
|
|
19
|
+
* Fallback v1 counts Latin/source-code runs at 3.5 UTF-16 characters per
|
|
20
|
+
* token, CJK code points at one token, emoji at two, and other non-ASCII code
|
|
21
|
+
* points at one. The result is deliberately conservative for context budgets.
|
|
22
|
+
*/
|
|
23
|
+
export declare function estimateTextTokens(text: string, options?: TokenEstimationOptions): TokenEstimate;
|
|
24
|
+
export declare function estimateSerializedTokens(value: unknown, options?: TokenEstimationOptions): TokenEstimate;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.estimateTextTokens = estimateTextTokens;
|
|
4
|
+
exports.estimateSerializedTokens = estimateSerializedTokens;
|
|
5
|
+
const CJK = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
6
|
+
const EMOJI = /\p{Extended_Pictographic}/u;
|
|
7
|
+
/**
|
|
8
|
+
* Estimate tokens without bundling a tokenizer.
|
|
9
|
+
*
|
|
10
|
+
* Fallback v1 counts Latin/source-code runs at 3.5 UTF-16 characters per
|
|
11
|
+
* token, CJK code points at one token, emoji at two, and other non-ASCII code
|
|
12
|
+
* points at one. The result is deliberately conservative for context budgets.
|
|
13
|
+
*/
|
|
14
|
+
function estimateTextTokens(text, options = {}) {
|
|
15
|
+
if (text.length === 0) {
|
|
16
|
+
return { count: 0, method: 'sidekick-fallback-v1', confidence: 'medium', provenance: 'empty' };
|
|
17
|
+
}
|
|
18
|
+
let exact;
|
|
19
|
+
try {
|
|
20
|
+
exact = options.exactCounter?.(text, { model: options.model });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
exact = undefined;
|
|
24
|
+
}
|
|
25
|
+
if (typeof exact === 'number' && Number.isFinite(exact) && exact >= 0) {
|
|
26
|
+
return {
|
|
27
|
+
count: Math.ceil(exact),
|
|
28
|
+
method: 'exact',
|
|
29
|
+
confidence: 'exact',
|
|
30
|
+
provenance: options.model ?? 'injected-counter',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
let asciiChars = 0;
|
|
34
|
+
let conservativeTokens = 0;
|
|
35
|
+
for (const codePoint of text) {
|
|
36
|
+
if (EMOJI.test(codePoint))
|
|
37
|
+
conservativeTokens += 2;
|
|
38
|
+
else if (CJK.test(codePoint))
|
|
39
|
+
conservativeTokens += 1;
|
|
40
|
+
else if ((codePoint.codePointAt(0) ?? 128) <= 127)
|
|
41
|
+
asciiChars += codePoint.length;
|
|
42
|
+
else
|
|
43
|
+
conservativeTokens += 1;
|
|
44
|
+
}
|
|
45
|
+
const count = Math.ceil(asciiChars / 3.5) + conservativeTokens;
|
|
46
|
+
return {
|
|
47
|
+
count: Number.isFinite(count) && count >= 0 ? count : 0,
|
|
48
|
+
method: 'sidekick-fallback-v1',
|
|
49
|
+
confidence: conservativeTokens > 0 ? 'low' : 'medium',
|
|
50
|
+
provenance: 'chars-per-token-3.5+cjk1+emoji2',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function estimateSerializedTokens(value, options = {}) {
|
|
54
|
+
let serialized;
|
|
55
|
+
try {
|
|
56
|
+
serialized = JSON.stringify(value) ?? '';
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
serialized = String(value);
|
|
60
|
+
}
|
|
61
|
+
return estimateTextTokens(serialized, options);
|
|
62
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { ProviderId } from './providers/types';
|
|
2
|
+
import type { SessionEvent } from './types/sessionEvent';
|
|
3
|
+
import { type NormalizedUsage, type NormalizedUsageCost } from './usageNormalization';
|
|
4
|
+
export type TranscriptFidelity = 'snippet' | 'full';
|
|
5
|
+
export interface TranscriptSourceProvenance {
|
|
6
|
+
provider?: ProviderId | string;
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
sourcePath?: string;
|
|
9
|
+
source?: string;
|
|
10
|
+
eventIndex: number;
|
|
11
|
+
eventType: SessionEvent['type'];
|
|
12
|
+
eventId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CanonicalTranscriptBlock {
|
|
15
|
+
type: 'text' | 'thinking' | 'tool_use' | 'tool_result' | 'image' | 'unknown';
|
|
16
|
+
text?: string;
|
|
17
|
+
toolName?: string;
|
|
18
|
+
toolUseId?: string;
|
|
19
|
+
input?: unknown;
|
|
20
|
+
output?: unknown;
|
|
21
|
+
isError?: boolean;
|
|
22
|
+
command?: string;
|
|
23
|
+
truncated?: boolean;
|
|
24
|
+
raw?: unknown;
|
|
25
|
+
}
|
|
26
|
+
export interface CanonicalTranscriptMessage {
|
|
27
|
+
id?: string;
|
|
28
|
+
role: 'user' | 'assistant' | 'system' | 'summary' | 'tool';
|
|
29
|
+
timestamp: string;
|
|
30
|
+
model?: string;
|
|
31
|
+
sourceLabel?: string;
|
|
32
|
+
content: CanonicalTranscriptBlock[];
|
|
33
|
+
text: string;
|
|
34
|
+
usage?: NormalizedUsage;
|
|
35
|
+
cost?: NormalizedUsageCost;
|
|
36
|
+
tools: CanonicalToolCall[];
|
|
37
|
+
commands: string[];
|
|
38
|
+
source: TranscriptSourceProvenance;
|
|
39
|
+
}
|
|
40
|
+
export interface CanonicalToolCall {
|
|
41
|
+
id?: string;
|
|
42
|
+
name: string;
|
|
43
|
+
timestamp: string;
|
|
44
|
+
input?: unknown;
|
|
45
|
+
output?: unknown;
|
|
46
|
+
isError?: boolean;
|
|
47
|
+
command?: string;
|
|
48
|
+
source: TranscriptSourceProvenance;
|
|
49
|
+
}
|
|
50
|
+
export interface CanonicalTranscriptUsageTotals {
|
|
51
|
+
uncachedInputTokens: number;
|
|
52
|
+
cacheReadTokens: number;
|
|
53
|
+
cacheWriteTokens: number;
|
|
54
|
+
outputTokens: number;
|
|
55
|
+
reasoningTokens: number;
|
|
56
|
+
cacheInclusiveInputTokens: number;
|
|
57
|
+
billableOutputTokens: number;
|
|
58
|
+
totalTokens: number;
|
|
59
|
+
}
|
|
60
|
+
export interface CanonicalSessionTranscript {
|
|
61
|
+
provider?: ProviderId | string;
|
|
62
|
+
sessionId?: string;
|
|
63
|
+
sourcePath?: string;
|
|
64
|
+
startedAt?: string;
|
|
65
|
+
endedAt?: string;
|
|
66
|
+
models: string[];
|
|
67
|
+
messages: CanonicalTranscriptMessage[];
|
|
68
|
+
tools: CanonicalToolCall[];
|
|
69
|
+
commands: string[];
|
|
70
|
+
usage: {
|
|
71
|
+
events: NormalizedUsage[];
|
|
72
|
+
costs: NormalizedUsageCost[];
|
|
73
|
+
totals: CanonicalTranscriptUsageTotals;
|
|
74
|
+
totalCostUsd: number | null;
|
|
75
|
+
};
|
|
76
|
+
provenance: {
|
|
77
|
+
source: 'session-events';
|
|
78
|
+
fidelity: TranscriptFidelity;
|
|
79
|
+
eventCount: number;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export interface ProjectSessionTranscriptOptions {
|
|
83
|
+
provider?: ProviderId | string;
|
|
84
|
+
sessionId?: string;
|
|
85
|
+
sourcePath?: string;
|
|
86
|
+
fidelity?: TranscriptFidelity;
|
|
87
|
+
/** Maximum characters per text/serialized block in snippet mode. Default 2000. */
|
|
88
|
+
maxContentChars?: number;
|
|
89
|
+
includeUnknownBlocks?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/** Project canonical provider events into a bounded or full-fidelity transcript. */
|
|
92
|
+
export declare function projectSessionTranscript(events: SessionEvent[], options?: ProjectSessionTranscriptOptions): CanonicalSessionTranscript;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.projectSessionTranscript = projectSessionTranscript;
|
|
4
|
+
const usageNormalization_1 = require("./usageNormalization");
|
|
5
|
+
const EMPTY_TOTALS = {
|
|
6
|
+
uncachedInputTokens: 0,
|
|
7
|
+
cacheReadTokens: 0,
|
|
8
|
+
cacheWriteTokens: 0,
|
|
9
|
+
outputTokens: 0,
|
|
10
|
+
reasoningTokens: 0,
|
|
11
|
+
cacheInclusiveInputTokens: 0,
|
|
12
|
+
billableOutputTokens: 0,
|
|
13
|
+
totalTokens: 0,
|
|
14
|
+
};
|
|
15
|
+
/** Project canonical provider events into a bounded or full-fidelity transcript. */
|
|
16
|
+
function projectSessionTranscript(events, options = {}) {
|
|
17
|
+
const fidelity = options.fidelity ?? 'snippet';
|
|
18
|
+
const requestedMax = options.maxContentChars ?? 2_000;
|
|
19
|
+
const maxContentChars = Number.isFinite(requestedMax) ? Math.max(0, requestedMax) : 2_000;
|
|
20
|
+
const messages = [];
|
|
21
|
+
const tools = [];
|
|
22
|
+
const commands = [];
|
|
23
|
+
const models = new Set();
|
|
24
|
+
const usageEvents = [];
|
|
25
|
+
const usageCosts = [];
|
|
26
|
+
const totals = { ...EMPTY_TOTALS };
|
|
27
|
+
const toolsById = new Map();
|
|
28
|
+
events.forEach((event, eventIndex) => {
|
|
29
|
+
const message = event.message;
|
|
30
|
+
const source = {
|
|
31
|
+
provider: options.provider ?? event.providerMetadata?.providerId,
|
|
32
|
+
sessionId: options.sessionId,
|
|
33
|
+
sourcePath: options.sourcePath,
|
|
34
|
+
source: event.providerMetadata?.source,
|
|
35
|
+
eventIndex,
|
|
36
|
+
eventType: event.type,
|
|
37
|
+
eventId: message?.id,
|
|
38
|
+
};
|
|
39
|
+
const usage = (0, usageNormalization_1.extractNormalizedUsage)(event) ?? undefined;
|
|
40
|
+
if (usage) {
|
|
41
|
+
usageEvents.push(usage);
|
|
42
|
+
addUsage(totals, usage);
|
|
43
|
+
}
|
|
44
|
+
const cost = usage
|
|
45
|
+
? (0, usageNormalization_1.calculateNormalizedUsageCost)({ usage, modelId: message?.model })
|
|
46
|
+
: undefined;
|
|
47
|
+
if (cost)
|
|
48
|
+
usageCosts.push(cost);
|
|
49
|
+
if (message?.model)
|
|
50
|
+
models.add(message.model);
|
|
51
|
+
const blocks = contentBlocks(message?.content, fidelity, maxContentChars, options.includeUnknownBlocks);
|
|
52
|
+
if (event.type === 'summary' && event.summary && blocks.length === 0) {
|
|
53
|
+
blocks.push({ type: 'text', ...boundedText(event.summary, fidelity, maxContentChars) });
|
|
54
|
+
}
|
|
55
|
+
if (event.type === 'tool_use') {
|
|
56
|
+
blocks.push({
|
|
57
|
+
type: 'tool_use',
|
|
58
|
+
toolName: event.tool.name,
|
|
59
|
+
input: boundedValue(event.tool.input, fidelity, maxContentChars),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (event.type === 'tool_result') {
|
|
63
|
+
blocks.push({
|
|
64
|
+
type: 'tool_result',
|
|
65
|
+
toolUseId: event.result.tool_use_id,
|
|
66
|
+
output: boundedValue(event.result.output, fidelity, maxContentChars),
|
|
67
|
+
isError: event.result.is_error,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const messageTools = [];
|
|
71
|
+
for (const block of blocks) {
|
|
72
|
+
if (block.type === 'tool_use' && block.toolName) {
|
|
73
|
+
const command = block.command ?? commandFromTool(block.toolName, block.input);
|
|
74
|
+
const call = {
|
|
75
|
+
id: block.toolUseId,
|
|
76
|
+
name: block.toolName,
|
|
77
|
+
timestamp: event.timestamp,
|
|
78
|
+
input: block.input,
|
|
79
|
+
command,
|
|
80
|
+
source,
|
|
81
|
+
};
|
|
82
|
+
tools.push(call);
|
|
83
|
+
messageTools.push(call);
|
|
84
|
+
if (call.id)
|
|
85
|
+
toolsById.set(call.id, call);
|
|
86
|
+
if (command)
|
|
87
|
+
commands.push(command);
|
|
88
|
+
}
|
|
89
|
+
else if (block.type === 'tool_result' && block.toolUseId) {
|
|
90
|
+
const call = toolsById.get(block.toolUseId);
|
|
91
|
+
if (call) {
|
|
92
|
+
call.output = block.output;
|
|
93
|
+
call.isError = block.isError;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (blocks.length === 0 && !usage && event.type !== 'summary')
|
|
98
|
+
return;
|
|
99
|
+
messages.push({
|
|
100
|
+
id: message?.id,
|
|
101
|
+
role: roleForEvent(event),
|
|
102
|
+
timestamp: event.timestamp,
|
|
103
|
+
model: message?.model,
|
|
104
|
+
sourceLabel: message?.sourceLabel,
|
|
105
|
+
content: blocks,
|
|
106
|
+
text: blocks
|
|
107
|
+
.filter((block) => block.type === 'text' || block.type === 'thinking')
|
|
108
|
+
.map((block) => block.text ?? '')
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.join('\n'),
|
|
111
|
+
usage,
|
|
112
|
+
cost,
|
|
113
|
+
tools: messageTools,
|
|
114
|
+
commands: messageTools.flatMap((tool) => (tool.command ? [tool.command] : [])),
|
|
115
|
+
source,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
provider: options.provider,
|
|
120
|
+
sessionId: options.sessionId,
|
|
121
|
+
sourcePath: options.sourcePath,
|
|
122
|
+
startedAt: events[0]?.timestamp,
|
|
123
|
+
endedAt: events.at(-1)?.timestamp,
|
|
124
|
+
models: [...models],
|
|
125
|
+
messages,
|
|
126
|
+
tools,
|
|
127
|
+
commands: [...new Set(commands)],
|
|
128
|
+
usage: {
|
|
129
|
+
events: usageEvents,
|
|
130
|
+
costs: usageCosts,
|
|
131
|
+
totals,
|
|
132
|
+
totalCostUsd: usageCosts.length > 0 && usageCosts.every((cost) => cost.costUsd !== null)
|
|
133
|
+
? usageCosts.reduce((sum, cost) => sum + (cost.costUsd ?? 0), 0)
|
|
134
|
+
: null,
|
|
135
|
+
},
|
|
136
|
+
provenance: { source: 'session-events', fidelity, eventCount: events.length },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function roleForEvent(event) {
|
|
140
|
+
if (event.type === 'summary')
|
|
141
|
+
return 'summary';
|
|
142
|
+
if (event.type === 'system')
|
|
143
|
+
return 'system';
|
|
144
|
+
if (event.type === 'tool_use' || event.type === 'tool_result')
|
|
145
|
+
return 'tool';
|
|
146
|
+
return event.type;
|
|
147
|
+
}
|
|
148
|
+
function contentBlocks(content, fidelity, maxChars, includeUnknown = false) {
|
|
149
|
+
if (typeof content === 'string')
|
|
150
|
+
return [{ type: 'text', ...boundedText(content, fidelity, maxChars) }];
|
|
151
|
+
if (!Array.isArray(content))
|
|
152
|
+
return [];
|
|
153
|
+
const result = [];
|
|
154
|
+
for (const raw of content) {
|
|
155
|
+
if (!raw || typeof raw !== 'object')
|
|
156
|
+
continue;
|
|
157
|
+
const block = raw;
|
|
158
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
159
|
+
result.push({ type: 'text', ...boundedText(block.text, fidelity, maxChars) });
|
|
160
|
+
}
|
|
161
|
+
else if ((block.type === 'thinking' || block.type === 'reasoning') &&
|
|
162
|
+
(typeof block.thinking === 'string' || typeof block.text === 'string')) {
|
|
163
|
+
result.push({
|
|
164
|
+
type: 'thinking',
|
|
165
|
+
...boundedText(String(block.thinking ?? block.text), fidelity, maxChars),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
169
|
+
result.push({
|
|
170
|
+
type: 'tool_use',
|
|
171
|
+
toolName: block.name,
|
|
172
|
+
toolUseId: typeof block.id === 'string' ? block.id : undefined,
|
|
173
|
+
input: boundedValue(block.input, fidelity, maxChars),
|
|
174
|
+
command: commandFromTool(block.name, block.input),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
else if (block.type === 'tool_result') {
|
|
178
|
+
result.push({
|
|
179
|
+
type: 'tool_result',
|
|
180
|
+
toolUseId: typeof block.tool_use_id === 'string' ? block.tool_use_id : undefined,
|
|
181
|
+
output: boundedValue(block.content, fidelity, maxChars),
|
|
182
|
+
isError: block.is_error === true,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
else if (block.type === 'image') {
|
|
186
|
+
result.push({
|
|
187
|
+
type: 'image',
|
|
188
|
+
text: '[Image content]',
|
|
189
|
+
...(fidelity === 'full' ? { raw } : {}),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else if (includeUnknown) {
|
|
193
|
+
result.push({ type: 'unknown', raw: boundedValue(raw, fidelity, maxChars) });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
function boundedText(text, fidelity, maxChars) {
|
|
199
|
+
if (fidelity === 'full' || text.length <= maxChars)
|
|
200
|
+
return { text, truncated: false };
|
|
201
|
+
return { text: text.slice(0, maxChars), truncated: true };
|
|
202
|
+
}
|
|
203
|
+
function boundedValue(value, fidelity, maxChars) {
|
|
204
|
+
if (fidelity === 'full' || value === undefined)
|
|
205
|
+
return value;
|
|
206
|
+
if (typeof value === 'string')
|
|
207
|
+
return boundedText(value, fidelity, maxChars).text;
|
|
208
|
+
try {
|
|
209
|
+
const json = JSON.stringify(value);
|
|
210
|
+
return json.length <= maxChars ? value : json.slice(0, maxChars);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return String(value).slice(0, maxChars);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function commandFromTool(name, input) {
|
|
217
|
+
if (!input || typeof input !== 'object')
|
|
218
|
+
return undefined;
|
|
219
|
+
const value = input;
|
|
220
|
+
if (!['bash', 'shell', 'execcommand', 'exec_command', 'local_shell'].includes(name.toLowerCase())) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
if (typeof value.command === 'string')
|
|
224
|
+
return value.command;
|
|
225
|
+
if (Array.isArray(value.command))
|
|
226
|
+
return value.command.map(String).join(' ');
|
|
227
|
+
if (typeof value.cmd === 'string')
|
|
228
|
+
return value.cmd;
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
function addUsage(total, usage) {
|
|
232
|
+
total.uncachedInputTokens += usage.uncachedInputTokens;
|
|
233
|
+
total.cacheReadTokens += usage.cacheReadTokens;
|
|
234
|
+
total.cacheWriteTokens += usage.cacheWriteTokens;
|
|
235
|
+
total.outputTokens += usage.outputTokens;
|
|
236
|
+
total.reasoningTokens += usage.reasoningTokens;
|
|
237
|
+
total.cacheInclusiveInputTokens += usage.cacheInclusiveInputTokens;
|
|
238
|
+
total.billableOutputTokens += usage.billableOutputTokens;
|
|
239
|
+
total.totalTokens += usage.totalTokens;
|
|
240
|
+
}
|