@timefly/opencode-plugin 0.2.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 +244 -0
- package/dist/event-handlers.d.ts +14 -0
- package/dist/event-handlers.d.ts.map +1 -0
- package/dist/event-handlers.js +145 -0
- package/dist/event-tracker.d.ts +21 -0
- package/dist/event-tracker.d.ts.map +1 -0
- package/dist/event-tracker.js +42 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/install.d.ts +3 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +98 -0
- package/dist/login.d.ts +3 -0
- package/dist/login.d.ts.map +1 -0
- package/dist/login.js +157 -0
- package/dist/map-opencode-event.d.ts +53 -0
- package/dist/map-opencode-event.d.ts.map +1 -0
- package/dist/map-opencode-event.js +179 -0
- package/dist/opencode-readers.d.ts +71 -0
- package/dist/opencode-readers.d.ts.map +1 -0
- package/dist/opencode-readers.js +150 -0
- package/dist/publish-events.d.ts +8 -0
- package/dist/publish-events.d.ts.map +1 -0
- package/dist/publish-events.js +45 -0
- package/dist/token-usage.d.ts +23 -0
- package/dist/token-usage.d.ts.map +1 -0
- package/dist/token-usage.js +28 -0
- package/package.json +73 -0
package/dist/login.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import { writeAuthFile, resolveDefaultAuthFilePath } from '@timefly/ai-sdk';
|
|
7
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
8
|
+
const PLUGIN_VERSION = packageJson.version;
|
|
9
|
+
const DEFAULT_API_BASE_URL = 'https://api.timefly.dev';
|
|
10
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
11
|
+
const toBase64Url = (value) => value.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
|
|
12
|
+
const buildDeviceFingerprint = () => createHash('sha256')
|
|
13
|
+
.update([os.hostname(), process.platform, process.arch, 'timefly-opencode-plugin'].join(':'))
|
|
14
|
+
.digest('hex');
|
|
15
|
+
const readApiBaseUrl = () => process.env.TIMEFLY_API_BASE_URL?.replace(/\/$/, '') ?? DEFAULT_API_BASE_URL;
|
|
16
|
+
const parseCallbackQuery = (requestUrl) => {
|
|
17
|
+
const parsedUrl = new URL(requestUrl, 'http://127.0.0.1');
|
|
18
|
+
return {
|
|
19
|
+
code: parsedUrl.searchParams.get('code') ?? undefined,
|
|
20
|
+
error: parsedUrl.searchParams.get('error') ?? undefined
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const exchangeAuthorizationCode = (input) => {
|
|
24
|
+
const exchangeUrl = `${input.apiBaseUrl}/auth/extension/exchange`;
|
|
25
|
+
return fetch(exchangeUrl, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
'x-device-fingerprint': input.deviceFingerprint
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
code: input.code,
|
|
33
|
+
codeVerifier: input.codeVerifier,
|
|
34
|
+
deviceFingerprint: input.deviceFingerprint,
|
|
35
|
+
deviceName: `OpenCode on ${os.hostname()}`,
|
|
36
|
+
clientName: 'TimeFly OpenCode Plugin',
|
|
37
|
+
editorName: 'OpenCode',
|
|
38
|
+
hostname: os.hostname(),
|
|
39
|
+
platform: process.platform,
|
|
40
|
+
arch: process.arch,
|
|
41
|
+
extensionVersion: PLUGIN_VERSION
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
.then((response) => response.text().then((responseBody) => ({
|
|
45
|
+
response,
|
|
46
|
+
responseBody
|
|
47
|
+
})))
|
|
48
|
+
.then(({ response, responseBody }) => {
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(`Token exchange failed (${response.status}): ${responseBody}`);
|
|
51
|
+
}
|
|
52
|
+
const envelope = JSON.parse(responseBody);
|
|
53
|
+
const tokens = envelope.data?.tokens;
|
|
54
|
+
if (!tokens?.accessToken || !tokens.refreshToken) {
|
|
55
|
+
throw new Error('Token exchange response missing tokens');
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
accessToken: tokens.accessToken,
|
|
59
|
+
refreshToken: tokens.refreshToken
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const waitForAuthorizationCode = (port) => new Promise((resolve, reject) => {
|
|
64
|
+
const server = createServer((request, response) => {
|
|
65
|
+
const callbackQuery = parseCallbackQuery(request.url ?? '/');
|
|
66
|
+
if (callbackQuery.error) {
|
|
67
|
+
response.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
68
|
+
response.end(`TimeFly login failed: ${callbackQuery.error}`);
|
|
69
|
+
clearTimeout(timeoutHandle);
|
|
70
|
+
server.close();
|
|
71
|
+
reject(new Error(`Login failed: ${callbackQuery.error}`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!callbackQuery.code) {
|
|
75
|
+
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
76
|
+
response.end('Not found');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
80
|
+
response.end('<html><body><h1>TimeFly connected</h1><p>You can close this tab and return to OpenCode.</p></body></html>');
|
|
81
|
+
clearTimeout(timeoutHandle);
|
|
82
|
+
server.close();
|
|
83
|
+
resolve(callbackQuery.code);
|
|
84
|
+
});
|
|
85
|
+
const timeoutHandle = setTimeout(() => {
|
|
86
|
+
server.close();
|
|
87
|
+
reject(new Error('Login timed out after 5 minutes'));
|
|
88
|
+
}, LOGIN_TIMEOUT_MS);
|
|
89
|
+
server.listen(port, '127.0.0.1', () => undefined);
|
|
90
|
+
server.on('error', (listenError) => {
|
|
91
|
+
clearTimeout(timeoutHandle);
|
|
92
|
+
reject(listenError);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
const openBrowser = (targetUrl) => {
|
|
96
|
+
const launchCommand = process.platform === 'win32'
|
|
97
|
+
? ['cmd.exe', '/c', 'start', '', targetUrl]
|
|
98
|
+
: process.platform === 'darwin'
|
|
99
|
+
? ['open', targetUrl]
|
|
100
|
+
: ['xdg-open', targetUrl];
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
const childProcess = spawn(launchCommand[0], launchCommand.slice(1), {
|
|
103
|
+
detached: true,
|
|
104
|
+
stdio: 'ignore'
|
|
105
|
+
});
|
|
106
|
+
childProcess.unref();
|
|
107
|
+
resolve();
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
const runLogin = () => {
|
|
111
|
+
const apiBaseUrl = readApiBaseUrl();
|
|
112
|
+
const callbackPort = 43127;
|
|
113
|
+
const redirectUri = `http://127.0.0.1:${callbackPort}/callback`;
|
|
114
|
+
const codeVerifier = toBase64Url(randomBytes(32));
|
|
115
|
+
const codeChallenge = toBase64Url(createHash('sha256').update(codeVerifier).digest());
|
|
116
|
+
const deviceFingerprint = buildDeviceFingerprint();
|
|
117
|
+
const oauthState = encodeURIComponent(JSON.stringify({
|
|
118
|
+
source: 'opencode-plugin',
|
|
119
|
+
fingerprint: deviceFingerprint,
|
|
120
|
+
redirectUri,
|
|
121
|
+
codeChallenge,
|
|
122
|
+
codeChallengeMethod: 'S256',
|
|
123
|
+
deviceName: `OpenCode on ${os.hostname()}`,
|
|
124
|
+
clientName: 'TimeFly OpenCode Plugin',
|
|
125
|
+
editorName: 'OpenCode',
|
|
126
|
+
hostname: os.hostname(),
|
|
127
|
+
platform: process.platform,
|
|
128
|
+
arch: process.arch,
|
|
129
|
+
extensionVersion: PLUGIN_VERSION
|
|
130
|
+
}));
|
|
131
|
+
const loginUrl = `${apiBaseUrl}/auth/google?state=${oauthState}`;
|
|
132
|
+
const authFilePath = resolveDefaultAuthFilePath();
|
|
133
|
+
console.log('Opening browser for TimeFly sign-in...');
|
|
134
|
+
console.log(`If the browser does not open, visit:\n${loginUrl}\n`);
|
|
135
|
+
return openBrowser(loginUrl)
|
|
136
|
+
.then(() => waitForAuthorizationCode(callbackPort))
|
|
137
|
+
.then((authorizationCode) => exchangeAuthorizationCode({
|
|
138
|
+
apiBaseUrl,
|
|
139
|
+
code: authorizationCode,
|
|
140
|
+
codeVerifier,
|
|
141
|
+
deviceFingerprint
|
|
142
|
+
}))
|
|
143
|
+
.then((tokens) => writeAuthFile(authFilePath, {
|
|
144
|
+
accessToken: tokens.accessToken,
|
|
145
|
+
refreshToken: tokens.refreshToken,
|
|
146
|
+
apiBaseUrl
|
|
147
|
+
}))
|
|
148
|
+
.then(() => {
|
|
149
|
+
console.log(`TimeFly login saved to ${authFilePath}`);
|
|
150
|
+
console.log('Restart OpenCode to start syncing telemetry.');
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
runLogin().catch((error) => {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
console.error(`[timefly-opencode-login] ${message}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CreateAiUsageEventInput } from '@timefly/ai-sdk';
|
|
2
|
+
import type { OpenCodeAssistantMessage, OpenCodeCompactionPart, OpenCodeRetryPart, OpenCodeSessionInfo, OpenCodeStepFinishPart, OpenCodeUserMessage } from './opencode-readers.js';
|
|
3
|
+
export type { OpenCodeAssistantMessage, OpenCodeCompactionPart, OpenCodeRetryPart, OpenCodeSessionInfo, OpenCodeStepFinishPart, OpenCodeUserMessage } from './opencode-readers.js';
|
|
4
|
+
export type { OpenCodeTokenUsage } from './token-usage.js';
|
|
5
|
+
export declare const readSessionIdOverride: (sessionId: string) => string;
|
|
6
|
+
export declare const buildModelId: (providerId: string, modelId: string) => string;
|
|
7
|
+
export declare const mapSessionStartInput: (sessionInfo: OpenCodeSessionInfo) => Pick<CreateAiUsageEventInput, "sessionId" | "metadata">;
|
|
8
|
+
export declare const mapSessionEndInput: (sessionId: string, sessionStats: {
|
|
9
|
+
inputTokens: number;
|
|
10
|
+
outputTokens: number;
|
|
11
|
+
totalTokens: number;
|
|
12
|
+
turnCount: number;
|
|
13
|
+
toolCallCount: number;
|
|
14
|
+
requestCount: number;
|
|
15
|
+
stepCount: number;
|
|
16
|
+
}) => Pick<CreateAiUsageEventInput, "sessionId" | "metadata">;
|
|
17
|
+
export declare const mapLlmRequestInput: (input: {
|
|
18
|
+
sessionID: string;
|
|
19
|
+
agent: string;
|
|
20
|
+
providerId: string;
|
|
21
|
+
modelId: string;
|
|
22
|
+
providerSource: string;
|
|
23
|
+
temperature: number;
|
|
24
|
+
topP: number;
|
|
25
|
+
maxOutputTokens?: number;
|
|
26
|
+
}) => CreateAiUsageEventInput;
|
|
27
|
+
export declare const mapAssistantTurnCompleteInput: (message: OpenCodeAssistantMessage) => CreateAiUsageEventInput | undefined;
|
|
28
|
+
export declare const mapAssistantLlmResponseInput: (message: OpenCodeAssistantMessage) => CreateAiUsageEventInput | undefined;
|
|
29
|
+
export declare const mapStepFinishInput: (part: OpenCodeStepFinishPart) => CreateAiUsageEventInput;
|
|
30
|
+
export declare const mapUserMessageInput: (message: OpenCodeUserMessage) => CreateAiUsageEventInput;
|
|
31
|
+
export declare const mapCompactionInput: (sessionId: string, auto?: boolean) => CreateAiUsageEventInput;
|
|
32
|
+
export declare const mapErrorInput: (sessionId: string, errorMetadata: Record<string, string | number | boolean>) => CreateAiUsageEventInput;
|
|
33
|
+
export declare const mapToolCallInput: (input: {
|
|
34
|
+
sessionID: string;
|
|
35
|
+
tool: string;
|
|
36
|
+
callID: string;
|
|
37
|
+
}) => CreateAiUsageEventInput;
|
|
38
|
+
export declare const mapToolResultInput: (input: {
|
|
39
|
+
sessionID: string;
|
|
40
|
+
tool: string;
|
|
41
|
+
callID: string;
|
|
42
|
+
hasOutput: boolean;
|
|
43
|
+
outputLength: number;
|
|
44
|
+
}) => CreateAiUsageEventInput;
|
|
45
|
+
export declare const mapCommandExecutedInput: (input: {
|
|
46
|
+
sessionID: string;
|
|
47
|
+
name: string;
|
|
48
|
+
arguments: string;
|
|
49
|
+
messageID: string;
|
|
50
|
+
}) => CreateAiUsageEventInput;
|
|
51
|
+
export declare const mapRetryPartInput: (part: OpenCodeRetryPart) => CreateAiUsageEventInput;
|
|
52
|
+
export declare const mapCompactionPartInput: (part: OpenCodeCompactionPart) => CreateAiUsageEventInput;
|
|
53
|
+
//# sourceMappingURL=map-opencode-event.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map-opencode-event.d.ts","sourceRoot":"","sources":["../src/map-opencode-event.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAA;AAC9D,OAAO,KAAK,EACX,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,MAAM,uBAAuB,CAAA;AAG9B,YAAY,EACX,wBAAwB,EACxB,sBAAsB,EACtB,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,eAAO,MAAM,qBAAqB,GAAI,WAAW,MAAM,KAAG,MAG7C,CAAA;AAEb,eAAO,MAAM,YAAY,GAAI,YAAY,MAAM,EAAE,SAAS,MAAM,KAAG,MAAoC,CAAA;AAEvG,eAAO,MAAM,oBAAoB,GAChC,aAAa,mBAAmB,KAC9B,IAAI,CAAC,uBAAuB,EAAE,WAAW,GAAG,UAAU,CAOvD,CAAA;AAEF,eAAO,MAAM,kBAAkB,GAC9B,WAAW,MAAM,EACjB,cAAc;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CACjB,KACC,IAAI,CAAC,uBAAuB,EAAE,WAAW,GAAG,UAAU,CAWvD,CAAA;AAEF,eAAO,MAAM,kBAAkB,GAAI,OAAO;IACzC,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,cAAc,EAAE,MAAM,CAAA;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,CAAC,EAAE,MAAM,CAAA;CACxB,KAAG,uBAcF,CAAA;AAEF,eAAO,MAAM,6BAA6B,GACzC,SAAS,wBAAwB,KAC/B,uBAAuB,GAAG,SA2B5B,CAAA;AAED,eAAO,MAAM,4BAA4B,GAAI,SAAS,wBAAwB,KAAG,uBAAuB,GAAG,SAyB1G,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,MAAM,sBAAsB,KAAG,uBAiBjE,CAAA;AAED,eAAO,MAAM,mBAAmB,GAAI,SAAS,mBAAmB,KAAG,uBAYjE,CAAA;AAEF,eAAO,MAAM,kBAAkB,GAAI,WAAW,MAAM,EAAE,OAAO,OAAO,KAAG,uBAMrE,CAAA;AAEF,eAAO,MAAM,aAAa,GACzB,WAAW,MAAM,EACjB,eAAe,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,KACtD,uBAID,CAAA;AAEF,eAAO,MAAM,gBAAgB,GAAI,OAAO;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACd,KAAG,uBAOF,CAAA;AAEF,eAAO,MAAM,kBAAkB,GAAI,OAAO;IACzC,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,OAAO,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;CACpB,KAAG,uBASF,CAAA;AAEF,eAAO,MAAM,uBAAuB,GAAI,OAAO;IAC9C,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CACjB,KAAG,uBASF,CAAA;AAEF,eAAO,MAAM,iBAAiB,GAAI,MAAM,iBAAiB,KAAG,uBAS1D,CAAA;AAEF,eAAO,MAAM,sBAAsB,GAAI,MAAM,sBAAsB,KAAG,uBAQpE,CAAA"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { buildTokenMetadata, buildTokenMetrics } from './token-usage.js';
|
|
2
|
+
export const readSessionIdOverride = (sessionId) => typeof process !== 'undefined' && process.env.TIMEFLY_OPENCODE_SESSION_ID
|
|
3
|
+
? process.env.TIMEFLY_OPENCODE_SESSION_ID
|
|
4
|
+
: sessionId;
|
|
5
|
+
export const buildModelId = (providerId, modelId) => `${providerId}/${modelId}`;
|
|
6
|
+
export const mapSessionStartInput = (sessionInfo) => ({
|
|
7
|
+
sessionId: readSessionIdOverride(sessionInfo.id),
|
|
8
|
+
metadata: {
|
|
9
|
+
session_title: sessionInfo.title,
|
|
10
|
+
project_id: sessionInfo.projectID,
|
|
11
|
+
directory: sessionInfo.directory
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
export const mapSessionEndInput = (sessionId, sessionStats) => ({
|
|
15
|
+
sessionId: readSessionIdOverride(sessionId),
|
|
16
|
+
metadata: {
|
|
17
|
+
session_input_tokens: sessionStats.inputTokens,
|
|
18
|
+
session_output_tokens: sessionStats.outputTokens,
|
|
19
|
+
session_total_tokens: sessionStats.totalTokens,
|
|
20
|
+
session_turn_count: sessionStats.turnCount,
|
|
21
|
+
session_tool_call_count: sessionStats.toolCallCount,
|
|
22
|
+
session_request_count: sessionStats.requestCount,
|
|
23
|
+
session_step_count: sessionStats.stepCount
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
export const mapLlmRequestInput = (input) => ({
|
|
27
|
+
sessionId: readSessionIdOverride(input.sessionID),
|
|
28
|
+
eventType: 'llm_request',
|
|
29
|
+
modelId: buildModelId(input.providerId, input.modelId),
|
|
30
|
+
planMode: input.agent,
|
|
31
|
+
metadata: {
|
|
32
|
+
provider_id: input.providerId,
|
|
33
|
+
model_id: input.modelId,
|
|
34
|
+
provider_source: input.providerSource,
|
|
35
|
+
agent: input.agent,
|
|
36
|
+
temperature: input.temperature,
|
|
37
|
+
top_p: input.topP,
|
|
38
|
+
...(input.maxOutputTokens !== undefined ? { max_output_tokens: input.maxOutputTokens } : {})
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
export const mapAssistantTurnCompleteInput = (message) => {
|
|
42
|
+
if (message.time.completed === undefined) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const durationMs = message.time.completed - message.time.created;
|
|
46
|
+
const tokenMetrics = buildTokenMetrics(message.tokens, durationMs);
|
|
47
|
+
return {
|
|
48
|
+
sessionId: readSessionIdOverride(message.sessionID),
|
|
49
|
+
eventType: 'turn_complete',
|
|
50
|
+
modelId: buildModelId(message.providerID, message.modelID),
|
|
51
|
+
planMode: message.mode,
|
|
52
|
+
inputTokens: tokenMetrics.inputTokens,
|
|
53
|
+
outputTokens: tokenMetrics.outputTokens,
|
|
54
|
+
totalTokens: tokenMetrics.totalTokens,
|
|
55
|
+
durationMs,
|
|
56
|
+
metadata: buildTokenMetadata(tokenMetrics, {
|
|
57
|
+
message_id: message.id,
|
|
58
|
+
provider_id: message.providerID,
|
|
59
|
+
model_id: message.modelID,
|
|
60
|
+
cost: message.cost,
|
|
61
|
+
...(message.finish ? { finish_reason: message.finish } : {}),
|
|
62
|
+
has_error: Boolean(message.error),
|
|
63
|
+
...(message.error ? { error_name: message.error.name } : {})
|
|
64
|
+
})
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
export const mapAssistantLlmResponseInput = (message) => {
|
|
68
|
+
if (message.time.completed === undefined) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const durationMs = message.time.completed - message.time.created;
|
|
72
|
+
const tokenMetrics = buildTokenMetrics(message.tokens, durationMs);
|
|
73
|
+
return {
|
|
74
|
+
sessionId: readSessionIdOverride(message.sessionID),
|
|
75
|
+
eventType: 'llm_response',
|
|
76
|
+
modelId: buildModelId(message.providerID, message.modelID),
|
|
77
|
+
planMode: message.mode,
|
|
78
|
+
inputTokens: tokenMetrics.inputTokens,
|
|
79
|
+
outputTokens: tokenMetrics.outputTokens,
|
|
80
|
+
totalTokens: tokenMetrics.totalTokens,
|
|
81
|
+
durationMs,
|
|
82
|
+
metadata: buildTokenMetadata(tokenMetrics, {
|
|
83
|
+
message_id: message.id,
|
|
84
|
+
provider_id: message.providerID,
|
|
85
|
+
model_id: message.modelID,
|
|
86
|
+
cost: message.cost,
|
|
87
|
+
response_scope: 'message'
|
|
88
|
+
})
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
export const mapStepFinishInput = (part) => {
|
|
92
|
+
const tokenMetrics = buildTokenMetrics(part.tokens);
|
|
93
|
+
return {
|
|
94
|
+
sessionId: readSessionIdOverride(part.sessionID),
|
|
95
|
+
eventType: 'llm_response',
|
|
96
|
+
inputTokens: tokenMetrics.inputTokens,
|
|
97
|
+
outputTokens: tokenMetrics.outputTokens,
|
|
98
|
+
totalTokens: tokenMetrics.totalTokens,
|
|
99
|
+
metadata: buildTokenMetadata(tokenMetrics, {
|
|
100
|
+
message_id: part.messageID,
|
|
101
|
+
part_id: part.id,
|
|
102
|
+
step_reason: part.reason,
|
|
103
|
+
cost: part.cost,
|
|
104
|
+
response_scope: 'step'
|
|
105
|
+
})
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
export const mapUserMessageInput = (message) => ({
|
|
109
|
+
sessionId: readSessionIdOverride(message.sessionID),
|
|
110
|
+
eventType: 'llm_request',
|
|
111
|
+
modelId: buildModelId(message.model.providerID, message.model.modelID),
|
|
112
|
+
planMode: message.agent,
|
|
113
|
+
metadata: {
|
|
114
|
+
message_id: message.id,
|
|
115
|
+
provider_id: message.model.providerID,
|
|
116
|
+
model_id: message.model.modelID,
|
|
117
|
+
agent: message.agent,
|
|
118
|
+
request_scope: 'user_message'
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
export const mapCompactionInput = (sessionId, auto) => ({
|
|
122
|
+
sessionId: readSessionIdOverride(sessionId),
|
|
123
|
+
eventType: 'compaction',
|
|
124
|
+
metadata: {
|
|
125
|
+
...(auto !== undefined ? { auto_compaction: auto } : {})
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
export const mapErrorInput = (sessionId, errorMetadata) => ({
|
|
129
|
+
sessionId: readSessionIdOverride(sessionId),
|
|
130
|
+
eventType: 'error',
|
|
131
|
+
metadata: errorMetadata
|
|
132
|
+
});
|
|
133
|
+
export const mapToolCallInput = (input) => ({
|
|
134
|
+
sessionId: readSessionIdOverride(input.sessionID),
|
|
135
|
+
eventType: 'tool_call',
|
|
136
|
+
toolName: input.tool,
|
|
137
|
+
metadata: {
|
|
138
|
+
call_id: input.callID
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
export const mapToolResultInput = (input) => ({
|
|
142
|
+
sessionId: readSessionIdOverride(input.sessionID),
|
|
143
|
+
eventType: 'tool_result',
|
|
144
|
+
toolName: input.tool,
|
|
145
|
+
metadata: {
|
|
146
|
+
call_id: input.callID,
|
|
147
|
+
has_output: input.hasOutput,
|
|
148
|
+
output_length: input.outputLength
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
export const mapCommandExecutedInput = (input) => ({
|
|
152
|
+
sessionId: readSessionIdOverride(input.sessionID),
|
|
153
|
+
eventType: 'tool_call',
|
|
154
|
+
toolName: `command:${input.name}`,
|
|
155
|
+
metadata: {
|
|
156
|
+
command_name: input.name,
|
|
157
|
+
command_arguments: input.arguments,
|
|
158
|
+
message_id: input.messageID
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
export const mapRetryPartInput = (part) => ({
|
|
162
|
+
sessionId: readSessionIdOverride(part.sessionID),
|
|
163
|
+
eventType: 'error',
|
|
164
|
+
metadata: {
|
|
165
|
+
message_id: part.messageID,
|
|
166
|
+
part_id: part.id,
|
|
167
|
+
retry_attempt: part.attempt,
|
|
168
|
+
error_scope: 'retry'
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
export const mapCompactionPartInput = (part) => ({
|
|
172
|
+
sessionId: readSessionIdOverride(part.sessionID),
|
|
173
|
+
eventType: 'compaction',
|
|
174
|
+
metadata: {
|
|
175
|
+
message_id: part.messageID,
|
|
176
|
+
part_id: part.id,
|
|
177
|
+
auto_compaction: part.auto
|
|
178
|
+
}
|
|
179
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { OpenCodeTokenUsage } from './token-usage.js';
|
|
2
|
+
export type OpenCodeBusEvent = {
|
|
3
|
+
type: string;
|
|
4
|
+
properties?: Record<string, unknown>;
|
|
5
|
+
};
|
|
6
|
+
export type OpenCodeAssistantMessage = {
|
|
7
|
+
id: string;
|
|
8
|
+
sessionID: string;
|
|
9
|
+
role: 'assistant';
|
|
10
|
+
time: {
|
|
11
|
+
created: number;
|
|
12
|
+
completed?: number;
|
|
13
|
+
};
|
|
14
|
+
modelID: string;
|
|
15
|
+
providerID: string;
|
|
16
|
+
mode: string;
|
|
17
|
+
cost: number;
|
|
18
|
+
tokens: OpenCodeTokenUsage;
|
|
19
|
+
finish?: string;
|
|
20
|
+
error?: {
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export type OpenCodeUserMessage = {
|
|
25
|
+
id: string;
|
|
26
|
+
sessionID: string;
|
|
27
|
+
role: 'user';
|
|
28
|
+
agent: string;
|
|
29
|
+
model: {
|
|
30
|
+
providerID: string;
|
|
31
|
+
modelID: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export type OpenCodeSessionInfo = {
|
|
35
|
+
id: string;
|
|
36
|
+
projectID: string;
|
|
37
|
+
title: string;
|
|
38
|
+
directory: string;
|
|
39
|
+
};
|
|
40
|
+
export type OpenCodeStepFinishPart = {
|
|
41
|
+
id: string;
|
|
42
|
+
sessionID: string;
|
|
43
|
+
messageID: string;
|
|
44
|
+
type: 'step-finish';
|
|
45
|
+
reason: string;
|
|
46
|
+
cost: number;
|
|
47
|
+
tokens: OpenCodeTokenUsage;
|
|
48
|
+
};
|
|
49
|
+
export type OpenCodeRetryPart = {
|
|
50
|
+
id: string;
|
|
51
|
+
sessionID: string;
|
|
52
|
+
messageID: string;
|
|
53
|
+
type: 'retry';
|
|
54
|
+
attempt: number;
|
|
55
|
+
};
|
|
56
|
+
export type OpenCodeCompactionPart = {
|
|
57
|
+
id: string;
|
|
58
|
+
sessionID: string;
|
|
59
|
+
messageID: string;
|
|
60
|
+
type: 'compaction';
|
|
61
|
+
auto: boolean;
|
|
62
|
+
};
|
|
63
|
+
export declare const readEventProperties: (event: OpenCodeBusEvent) => Record<string, unknown> | undefined;
|
|
64
|
+
export declare const readSessionIdFromProperties: (properties: Record<string, unknown>) => string | undefined;
|
|
65
|
+
export declare const readAssistantMessage: (message: unknown) => OpenCodeAssistantMessage | undefined;
|
|
66
|
+
export declare const readUserMessage: (message: unknown) => OpenCodeUserMessage | undefined;
|
|
67
|
+
export declare const readSessionInfo: (info: unknown) => OpenCodeSessionInfo | undefined;
|
|
68
|
+
export declare const readStepFinishPart: (part: unknown) => OpenCodeStepFinishPart | undefined;
|
|
69
|
+
export declare const readRetryPart: (part: unknown) => OpenCodeRetryPart | undefined;
|
|
70
|
+
export declare const readCompactionPart: (part: unknown) => OpenCodeCompactionPart | undefined;
|
|
71
|
+
//# sourceMappingURL=opencode-readers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-readers.d.ts","sourceRoot":"","sources":["../src/opencode-readers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
2
|
+
const readTokenUsage = (value) => {
|
|
3
|
+
if (!isRecord(value)) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const cacheRecord = isRecord(value.cache) ? value.cache : undefined;
|
|
7
|
+
if (typeof value.input !== 'number' ||
|
|
8
|
+
typeof value.output !== 'number' ||
|
|
9
|
+
typeof value.reasoning !== 'number' ||
|
|
10
|
+
typeof cacheRecord?.read !== 'number' ||
|
|
11
|
+
typeof cacheRecord?.write !== 'number') {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
input: value.input,
|
|
16
|
+
output: value.output,
|
|
17
|
+
reasoning: value.reasoning,
|
|
18
|
+
cache: {
|
|
19
|
+
read: cacheRecord.read,
|
|
20
|
+
write: cacheRecord.write
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export const readEventProperties = (event) => {
|
|
25
|
+
if (!isRecord(event.properties)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
return event.properties;
|
|
29
|
+
};
|
|
30
|
+
export const readSessionIdFromProperties = (properties) => typeof properties.sessionID === 'string' ? properties.sessionID : undefined;
|
|
31
|
+
export const readAssistantMessage = (message) => {
|
|
32
|
+
if (!isRecord(message) || message.role !== 'assistant') {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const tokenUsage = readTokenUsage(message.tokens);
|
|
36
|
+
const timeRecord = isRecord(message.time) ? message.time : undefined;
|
|
37
|
+
if (typeof message.id !== 'string' ||
|
|
38
|
+
typeof message.sessionID !== 'string' ||
|
|
39
|
+
typeof message.modelID !== 'string' ||
|
|
40
|
+
typeof message.providerID !== 'string' ||
|
|
41
|
+
typeof message.mode !== 'string' ||
|
|
42
|
+
typeof message.cost !== 'number' ||
|
|
43
|
+
typeof timeRecord?.created !== 'number' ||
|
|
44
|
+
!tokenUsage) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
id: message.id,
|
|
49
|
+
sessionID: message.sessionID,
|
|
50
|
+
role: 'assistant',
|
|
51
|
+
time: {
|
|
52
|
+
created: timeRecord.created,
|
|
53
|
+
completed: typeof timeRecord.completed === 'number' ? timeRecord.completed : undefined
|
|
54
|
+
},
|
|
55
|
+
modelID: message.modelID,
|
|
56
|
+
providerID: message.providerID,
|
|
57
|
+
mode: message.mode,
|
|
58
|
+
cost: message.cost,
|
|
59
|
+
tokens: tokenUsage,
|
|
60
|
+
finish: typeof message.finish === 'string' ? message.finish : undefined,
|
|
61
|
+
error: isRecord(message.error) && typeof message.error.name === 'string' ? { name: message.error.name } : undefined
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
export const readUserMessage = (message) => {
|
|
65
|
+
if (!isRecord(message) || message.role !== 'user') {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const modelRecord = isRecord(message.model) ? message.model : undefined;
|
|
69
|
+
if (typeof message.id !== 'string' ||
|
|
70
|
+
typeof message.sessionID !== 'string' ||
|
|
71
|
+
typeof message.agent !== 'string' ||
|
|
72
|
+
typeof modelRecord?.providerID !== 'string' ||
|
|
73
|
+
typeof modelRecord?.modelID !== 'string') {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
id: message.id,
|
|
78
|
+
sessionID: message.sessionID,
|
|
79
|
+
role: 'user',
|
|
80
|
+
agent: message.agent,
|
|
81
|
+
model: {
|
|
82
|
+
providerID: modelRecord.providerID,
|
|
83
|
+
modelID: modelRecord.modelID
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
export const readSessionInfo = (info) => {
|
|
88
|
+
if (!isRecord(info) || typeof info.id !== 'string') {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
id: info.id,
|
|
93
|
+
projectID: typeof info.projectID === 'string' ? info.projectID : 'unknown',
|
|
94
|
+
title: typeof info.title === 'string' ? info.title : 'untitled',
|
|
95
|
+
directory: typeof info.directory === 'string' ? info.directory : 'unknown'
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
export const readStepFinishPart = (part) => {
|
|
99
|
+
if (!isRecord(part) || part.type !== 'step-finish') {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const tokenUsage = readTokenUsage(part.tokens);
|
|
103
|
+
if (typeof part.id !== 'string' ||
|
|
104
|
+
typeof part.sessionID !== 'string' ||
|
|
105
|
+
typeof part.messageID !== 'string' ||
|
|
106
|
+
typeof part.reason !== 'string' ||
|
|
107
|
+
typeof part.cost !== 'number' ||
|
|
108
|
+
!tokenUsage) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
id: part.id,
|
|
113
|
+
sessionID: part.sessionID,
|
|
114
|
+
messageID: part.messageID,
|
|
115
|
+
type: 'step-finish',
|
|
116
|
+
reason: part.reason,
|
|
117
|
+
cost: part.cost,
|
|
118
|
+
tokens: tokenUsage
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
export const readRetryPart = (part) => {
|
|
122
|
+
if (!isRecord(part) || part.type !== 'retry') {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
if (typeof part.id !== 'string' || typeof part.sessionID !== 'string' || typeof part.attempt !== 'number') {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
id: part.id,
|
|
130
|
+
sessionID: part.sessionID,
|
|
131
|
+
messageID: typeof part.messageID === 'string' ? part.messageID : 'unknown',
|
|
132
|
+
type: 'retry',
|
|
133
|
+
attempt: part.attempt
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
export const readCompactionPart = (part) => {
|
|
137
|
+
if (!isRecord(part) || part.type !== 'compaction') {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
if (typeof part.id !== 'string' || typeof part.sessionID !== 'string') {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
id: part.id,
|
|
145
|
+
sessionID: part.sessionID,
|
|
146
|
+
messageID: typeof part.messageID === 'string' ? part.messageID : 'unknown',
|
|
147
|
+
type: 'compaction',
|
|
148
|
+
auto: Boolean(part.auto)
|
|
149
|
+
};
|
|
150
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type CreateAiUsageEventInput } from '@timefly/ai-sdk';
|
|
2
|
+
import type { PluginInput } from '@opencode-ai/plugin';
|
|
3
|
+
type EventPublisher = {
|
|
4
|
+
publish: (events: CreateAiUsageEventInput[]) => Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export declare const createEventPublisher: (client: PluginInput["client"], sourceVersion: string) => EventPublisher;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=publish-events.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-events.d.ts","sourceRoot":"","sources":["../src/publish-events.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4D,KAAK,uBAAuB,EAAoB,MAAM,iBAAiB,CAAA;AAC1I,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAItD,KAAK,cAAc,GAAG;IACrB,OAAO,EAAE,CAAC,MAAM,EAAE,uBAAuB,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7D,CAAA;AAkCD,eAAO,MAAM,oBAAoB,GAAI,QAAQ,WAAW,CAAC,QAAQ,CAAC,EAAE,eAAe,MAAM,KAAG,cAoB3F,CAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createAiUsageEvent, createTimeFlyAiClient, isSyncFailure } from '@timefly/ai-sdk';
|
|
2
|
+
const PRICING_URL = 'https://timefly.dev/pricing';
|
|
3
|
+
const buildSyncFailureMessage = (error) => {
|
|
4
|
+
if (error.isSupporterRequired) {
|
|
5
|
+
return `TimeFly sync blocked: Supporter plan required. Upgrade at ${PRICING_URL}`;
|
|
6
|
+
}
|
|
7
|
+
if (error.isUnauthorized) {
|
|
8
|
+
return 'TimeFly sync blocked: not signed in. Run `bunx @timefly/opencode-plugin login` or set TIMEFLY_ACCESS_TOKEN.';
|
|
9
|
+
}
|
|
10
|
+
return `TimeFly sync failed (${error.statusCode}): ${error.message}`;
|
|
11
|
+
};
|
|
12
|
+
const logPublishFailure = (client, error) => {
|
|
13
|
+
const message = isSyncFailure(error) ? buildSyncFailureMessage(error) : error instanceof Error ? error.message : String(error);
|
|
14
|
+
const level = isSyncFailure(error) && (error.isSupporterRequired || error.isUnauthorized) ? 'warn' : 'error';
|
|
15
|
+
return client.app
|
|
16
|
+
.log({
|
|
17
|
+
body: {
|
|
18
|
+
service: 'timefly-opencode-plugin',
|
|
19
|
+
level,
|
|
20
|
+
message,
|
|
21
|
+
extra: isSyncFailure(error) ? { statusCode: error.statusCode } : undefined
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.then(() => undefined)
|
|
25
|
+
.catch(() => undefined);
|
|
26
|
+
};
|
|
27
|
+
const buildUsageEvents = (sourceVersion, inputs) => inputs.map((input) => createAiUsageEvent('opencode', sourceVersion, input));
|
|
28
|
+
export const createEventPublisher = (client, sourceVersion) => {
|
|
29
|
+
const timeflyClient = createTimeFlyAiClient({
|
|
30
|
+
source: 'opencode',
|
|
31
|
+
sourceVersion
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
publish: (events) => {
|
|
35
|
+
if (!events.length) {
|
|
36
|
+
return Promise.resolve();
|
|
37
|
+
}
|
|
38
|
+
const usageEvents = buildUsageEvents(sourceVersion, events);
|
|
39
|
+
return timeflyClient
|
|
40
|
+
.recordEvents(usageEvents)
|
|
41
|
+
.then(() => undefined)
|
|
42
|
+
.catch((error) => logPublishFailure(client, error));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|