@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/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":"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"}
@@ -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
+ };