@timefly/ai-sdk 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 +74 -0
- package/dist/auth-credentials.d.ts +22 -0
- package/dist/auth-credentials.d.ts.map +1 -0
- package/dist/auth-credentials.js +46 -0
- package/dist/create-ai-usage-event.d.ts +3 -0
- package/dist/create-ai-usage-event.d.ts.map +1 -0
- package/dist/create-ai-usage-event.js +27 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/local-queue.d.ts +9 -0
- package/dist/local-queue.d.ts.map +1 -0
- package/dist/local-queue.js +24 -0
- package/dist/sync-error.d.ts +9 -0
- package/dist/sync-error.d.ts.map +1 -0
- package/dist/sync-error.js +10 -0
- package/dist/timefly-ai-client.d.ts +29 -0
- package/dist/timefly-ai-client.d.ts.map +1 -0
- package/dist/timefly-ai-client.js +158 -0
- package/dist/token-refresh.d.ts +16 -0
- package/dist/token-refresh.d.ts.map +1 -0
- package/dist/token-refresh.js +37 -0
- package/dist/types.d.ts +49 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @timefly/ai-sdk
|
|
2
|
+
|
|
3
|
+
Public SDK for sending **AI usage telemetry** to TimeFly from any integration.
|
|
4
|
+
|
|
5
|
+
No prompt content is sent — only metadata (model, tokens, tool names, session lifecycle).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add @timefly/ai-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
From this monorepo:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@timefly/ai-sdk": "workspace:*"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick example
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { createAiUsageEvent, createTimeFlyAiClient } from '@timefly/ai-sdk'
|
|
27
|
+
|
|
28
|
+
const client = createTimeFlyAiClient({
|
|
29
|
+
source: 'opencode',
|
|
30
|
+
sourceVersion: '1.0.0',
|
|
31
|
+
accessToken: process.env.TIMEFLY_ACCESS_TOKEN
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const event = createAiUsageEvent('opencode', '1.0.0', {
|
|
35
|
+
sessionId: 'session-abc',
|
|
36
|
+
eventType: 'llm_request',
|
|
37
|
+
modelId: 'claude-sonnet-4',
|
|
38
|
+
inputTokens: 1200,
|
|
39
|
+
outputTokens: 340
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
client.recordEvents([event]).then((result) => {
|
|
43
|
+
console.log(result.synced ? 'synced' : 'queued locally', result.accepted)
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
| Export | Description |
|
|
50
|
+
|--------|-------------|
|
|
51
|
+
| `createAiUsageEvent` | Build a typed `AiUsageEvent` |
|
|
52
|
+
| `createTimeFlyAiClient` | HTTP client with local offline queue |
|
|
53
|
+
| `TimeFlyAiClient` | Class with `recordEvents` / `flushPendingQueue` |
|
|
54
|
+
|
|
55
|
+
## Endpoint
|
|
56
|
+
|
|
57
|
+
`POST {apiBaseUrl}/ai/sync` — gzip JSON body `{ events: AiUsageEvent[] }`
|
|
58
|
+
|
|
59
|
+
Stored in ClickHouse table `ai_usage_events`.
|
|
60
|
+
|
|
61
|
+
## Environment
|
|
62
|
+
|
|
63
|
+
| Variable | Description |
|
|
64
|
+
|----------|-------------|
|
|
65
|
+
| `TIMEFLY_API_BASE_URL` | Default `https://api.timefly.dev` |
|
|
66
|
+
| `TIMEFLY_ACCESS_TOKEN` | Bearer token from TimeFly account |
|
|
67
|
+
|
|
68
|
+
## Build your own integration
|
|
69
|
+
|
|
70
|
+
1. Map your tool's lifecycle hooks to `AiUsageEvent` fields.
|
|
71
|
+
2. Call `client.recordEvents([...])` — failures are queued to `.timefly-ai-queue.json`.
|
|
72
|
+
3. Document install steps in your package README.
|
|
73
|
+
|
|
74
|
+
See [docs/INTEGRATIONS.md](../../docs/INTEGRATIONS.md) for OpenCode and Claude Code examples.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type TimeFlyAuthFile = {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
refreshToken: string;
|
|
4
|
+
apiBaseUrl?: string;
|
|
5
|
+
savedAt?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ResolvedAuthCredentials = {
|
|
8
|
+
accessToken?: string;
|
|
9
|
+
refreshToken?: string;
|
|
10
|
+
apiBaseUrl?: string;
|
|
11
|
+
authFilePath?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const resolveDefaultAuthFilePath: () => string;
|
|
14
|
+
export declare const readAuthFile: (authFilePath: string) => Promise<TimeFlyAuthFile | undefined>;
|
|
15
|
+
export declare const writeAuthFile: (authFilePath: string, authFile: TimeFlyAuthFile) => Promise<void>;
|
|
16
|
+
export declare const resolveAuthCredentials: (config: {
|
|
17
|
+
accessToken?: string;
|
|
18
|
+
refreshToken?: string;
|
|
19
|
+
apiBaseUrl?: string;
|
|
20
|
+
authFilePath?: string;
|
|
21
|
+
}) => Promise<ResolvedAuthCredentials>;
|
|
22
|
+
//# sourceMappingURL=auth-credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-credentials.d.ts","sourceRoot":"","sources":["../src/auth-credentials.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,eAAe,GAAG;IAC7B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACrC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAKD,eAAO,MAAM,0BAA0B,QAAO,MAA2B,CAAA;AAEzE,eAAO,MAAM,YAAY,GAAI,cAAc,MAAM,KAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAM9D,CAAA;AAEzB,eAAO,MAAM,aAAa,GAAI,cAAc,MAAM,EAAE,UAAU,eAAe,KAAG,OAAO,CAAC,IAAI,CAmB3F,CAAA;AAED,eAAO,MAAM,sBAAsB,GAAI,QAAQ;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;CACrB,KAAG,OAAO,CAAC,uBAAuB,CA6BlC,CAAA"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
const DEFAULT_AUTH_DIRECTORY = path.join(os.homedir(), '.config', 'opencode');
|
|
5
|
+
const DEFAULT_AUTH_FILE = path.join(DEFAULT_AUTH_DIRECTORY, 'timefly-auth.json');
|
|
6
|
+
export const resolveDefaultAuthFilePath = () => DEFAULT_AUTH_FILE;
|
|
7
|
+
export const readAuthFile = (authFilePath) => readFile(authFilePath, 'utf8')
|
|
8
|
+
.then((rawContent) => JSON.parse(rawContent))
|
|
9
|
+
.then((authFile) => typeof authFile.accessToken === 'string' && typeof authFile.refreshToken === 'string' ? authFile : undefined)
|
|
10
|
+
.catch(() => undefined);
|
|
11
|
+
export const writeAuthFile = (authFilePath, authFile) => {
|
|
12
|
+
const authDirectory = path.dirname(authFilePath);
|
|
13
|
+
return mkdir(authDirectory, { recursive: true })
|
|
14
|
+
.then(() => writeFile(authFilePath, JSON.stringify({
|
|
15
|
+
...authFile,
|
|
16
|
+
savedAt: new Date().toISOString()
|
|
17
|
+
}, null, 2), 'utf8'))
|
|
18
|
+
.then(() => undefined);
|
|
19
|
+
};
|
|
20
|
+
export const resolveAuthCredentials = (config) => {
|
|
21
|
+
const environmentAccessToken = process.env.TIMEFLY_ACCESS_TOKEN;
|
|
22
|
+
const environmentApiBaseUrl = process.env.TIMEFLY_API_BASE_URL;
|
|
23
|
+
const authFilePath = config.authFilePath ?? resolveDefaultAuthFilePath();
|
|
24
|
+
if (config.accessToken) {
|
|
25
|
+
return Promise.resolve({
|
|
26
|
+
accessToken: config.accessToken,
|
|
27
|
+
refreshToken: config.refreshToken,
|
|
28
|
+
apiBaseUrl: config.apiBaseUrl ?? environmentApiBaseUrl,
|
|
29
|
+
authFilePath
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (environmentAccessToken) {
|
|
33
|
+
return Promise.resolve({
|
|
34
|
+
accessToken: environmentAccessToken,
|
|
35
|
+
refreshToken: config.refreshToken,
|
|
36
|
+
apiBaseUrl: config.apiBaseUrl ?? environmentApiBaseUrl,
|
|
37
|
+
authFilePath
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return readAuthFile(authFilePath).then((authFile) => ({
|
|
41
|
+
accessToken: authFile?.accessToken,
|
|
42
|
+
refreshToken: authFile?.refreshToken ?? config.refreshToken,
|
|
43
|
+
apiBaseUrl: config.apiBaseUrl ?? authFile?.apiBaseUrl ?? environmentApiBaseUrl,
|
|
44
|
+
authFilePath
|
|
45
|
+
}));
|
|
46
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-ai-usage-event.d.ts","sourceRoot":"","sources":["../src/create-ai-usage-event.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA;AASvE,eAAO,MAAM,kBAAkB,GAC9B,QAAQ,YAAY,CAAC,QAAQ,CAAC,EAC9B,eAAe,MAAM,EACrB,OAAO,uBAAuB,KAC5B,YAoBF,CAAA"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { v7 as uuidV7 } from 'uuid';
|
|
2
|
+
const formatLocalDay = (date) => {
|
|
3
|
+
const year = date.getFullYear();
|
|
4
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
5
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
6
|
+
return `${year}-${month}-${day}`;
|
|
7
|
+
};
|
|
8
|
+
export const createAiUsageEvent = (source, sourceVersion, input) => {
|
|
9
|
+
const eventAt = input.eventAtUtc ?? new Date();
|
|
10
|
+
return {
|
|
11
|
+
eventId: uuidV7(),
|
|
12
|
+
source,
|
|
13
|
+
sourceVersion,
|
|
14
|
+
sessionId: input.sessionId,
|
|
15
|
+
eventType: input.eventType,
|
|
16
|
+
eventAtUtc: eventAt.toISOString(),
|
|
17
|
+
localDay: formatLocalDay(eventAt),
|
|
18
|
+
modelId: input.modelId,
|
|
19
|
+
planMode: input.planMode,
|
|
20
|
+
inputTokens: input.inputTokens,
|
|
21
|
+
outputTokens: input.outputTokens,
|
|
22
|
+
totalTokens: input.totalTokens,
|
|
23
|
+
toolName: input.toolName,
|
|
24
|
+
durationMs: input.durationMs,
|
|
25
|
+
metadata: input.metadata
|
|
26
|
+
};
|
|
27
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { resolveAuthCredentials, resolveDefaultAuthFilePath, readAuthFile, writeAuthFile } from './auth-credentials.js';
|
|
2
|
+
export type { ResolvedAuthCredentials, TimeFlyAuthFile } from './auth-credentials.js';
|
|
3
|
+
export { createAiUsageEvent } from './create-ai-usage-event.js';
|
|
4
|
+
export { appendToLocalQueue, readLocalQueue, resolveQueueFilePath } from './local-queue.js';
|
|
5
|
+
export { isSyncFailure, parseSyncFailure } from './sync-error.js';
|
|
6
|
+
export type { SyncFailure } from './sync-error.js';
|
|
7
|
+
export { refreshAccessToken } from './token-refresh.js';
|
|
8
|
+
export { createTimeFlyAiClient, TimeFlyAiClient } from './timefly-ai-client.js';
|
|
9
|
+
export type { RecordResult } from './timefly-ai-client.js';
|
|
10
|
+
export type { AiSdkConfig, AiSyncBatch, AiSyncResponse, AiToolSource, AiUsageEvent, AiUsageEventType, AiUsageMetadata, CreateAiUsageEventInput } from './types.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AACvH,YAAY,EAAE,uBAAuB,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAA;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAC/D,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAC3F,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AACjE,YAAY,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC/E,YAAY,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAC1D,YAAY,EACX,WAAW,EACX,WAAW,EACX,cAAc,EACd,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,uBAAuB,EACvB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { resolveAuthCredentials, resolveDefaultAuthFilePath, readAuthFile, writeAuthFile } from './auth-credentials.js';
|
|
2
|
+
export { createAiUsageEvent } from './create-ai-usage-event.js';
|
|
3
|
+
export { appendToLocalQueue, readLocalQueue, resolveQueueFilePath } from './local-queue.js';
|
|
4
|
+
export { isSyncFailure, parseSyncFailure } from './sync-error.js';
|
|
5
|
+
export { refreshAccessToken } from './token-refresh.js';
|
|
6
|
+
export { createTimeFlyAiClient, TimeFlyAiClient } from './timefly-ai-client.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AiUsageEvent } from './types.js';
|
|
2
|
+
export type LocalQueue = {
|
|
3
|
+
events: AiUsageEvent[];
|
|
4
|
+
};
|
|
5
|
+
export declare const readLocalQueue: (queueFilePath: string) => Promise<LocalQueue>;
|
|
6
|
+
export declare const appendToLocalQueue: (queueFilePath: string, events: AiUsageEvent[]) => Promise<void>;
|
|
7
|
+
export declare const clearAcceptedFromQueue: (queueFilePath: string, acceptedEventIds: Set<string>) => Promise<void>;
|
|
8
|
+
export declare const resolveQueueFilePath: (customPath?: string) => string;
|
|
9
|
+
//# sourceMappingURL=local-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-queue.d.ts","sourceRoot":"","sources":["../src/local-queue.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAI9C,MAAM,MAAM,UAAU,GAAG;IACxB,MAAM,EAAE,YAAY,EAAE,CAAA;CACtB,CAAA;AAID,eAAO,MAAM,cAAc,GAAI,eAAe,MAAM,KAAG,OAAO,CAAC,UAAU,CAG7C,CAAA;AAE5B,eAAO,MAAM,kBAAkB,GAC9B,eAAe,MAAM,EACrB,QAAQ,YAAY,EAAE,KACpB,OAAO,CAAC,IAAI,CAUd,CAAA;AAED,eAAO,MAAM,sBAAsB,GAClC,eAAe,MAAM,EACrB,kBAAkB,GAAG,CAAC,MAAM,CAAC,KAC3B,OAAO,CAAC,IAAI,CAMS,CAAA;AAExB,eAAO,MAAM,oBAAoB,GAAI,aAAa,MAAM,KAAG,MACA,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const DEFAULT_QUEUE_FILE = '.timefly-ai-queue.json';
|
|
4
|
+
const emptyQueue = () => ({ events: [] });
|
|
5
|
+
export const readLocalQueue = (queueFilePath) => readFile(queueFilePath, 'utf8')
|
|
6
|
+
.then((rawContent) => JSON.parse(rawContent))
|
|
7
|
+
.catch(() => emptyQueue());
|
|
8
|
+
export const appendToLocalQueue = (queueFilePath, events) => {
|
|
9
|
+
const directory = path.dirname(queueFilePath);
|
|
10
|
+
return mkdir(directory, { recursive: true })
|
|
11
|
+
.then(() => readLocalQueue(queueFilePath))
|
|
12
|
+
.then((queue) => ({
|
|
13
|
+
events: [...queue.events, ...events]
|
|
14
|
+
}))
|
|
15
|
+
.then((updatedQueue) => writeFile(queueFilePath, JSON.stringify(updatedQueue, null, 2), 'utf8'))
|
|
16
|
+
.then(() => undefined);
|
|
17
|
+
};
|
|
18
|
+
export const clearAcceptedFromQueue = (queueFilePath, acceptedEventIds) => readLocalQueue(queueFilePath)
|
|
19
|
+
.then((queue) => ({
|
|
20
|
+
events: queue.events.filter((event) => !acceptedEventIds.has(event.eventId))
|
|
21
|
+
}))
|
|
22
|
+
.then((updatedQueue) => writeFile(queueFilePath, JSON.stringify(updatedQueue, null, 2), 'utf8'))
|
|
23
|
+
.then(() => undefined);
|
|
24
|
+
export const resolveQueueFilePath = (customPath) => customPath ?? path.join(process.cwd(), DEFAULT_QUEUE_FILE);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type SyncFailure = {
|
|
2
|
+
statusCode: number;
|
|
3
|
+
message: string;
|
|
4
|
+
isSupporterRequired: boolean;
|
|
5
|
+
isUnauthorized: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare const parseSyncFailure: (statusCode: number, responseBody: string) => SyncFailure;
|
|
8
|
+
export declare const isSyncFailure: (error: unknown) => error is SyncFailure;
|
|
9
|
+
//# sourceMappingURL=sync-error.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-error.d.ts","sourceRoot":"","sources":["../src/sync-error.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,mBAAmB,EAAE,OAAO,CAAA;IAC5B,cAAc,EAAE,OAAO,CAAA;CACvB,CAAA;AAED,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,EAAE,cAAc,MAAM,KAAG,WAK1E,CAAA;AAEF,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,WAIF,CAAA"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const parseSyncFailure = (statusCode, responseBody) => ({
|
|
2
|
+
statusCode,
|
|
3
|
+
message: responseBody || `AI sync failed: ${statusCode}`,
|
|
4
|
+
isSupporterRequired: statusCode === 403 && responseBody.includes('Supporter'),
|
|
5
|
+
isUnauthorized: statusCode === 401
|
|
6
|
+
});
|
|
7
|
+
export const isSyncFailure = (error) => typeof error === 'object' &&
|
|
8
|
+
error !== null &&
|
|
9
|
+
'statusCode' in error &&
|
|
10
|
+
typeof error.statusCode === 'number';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AiSdkConfig, AiSyncResponse, AiUsageEvent } from './types.js';
|
|
2
|
+
export type RecordResult = {
|
|
3
|
+
synced: boolean;
|
|
4
|
+
queued: boolean;
|
|
5
|
+
accepted: number;
|
|
6
|
+
};
|
|
7
|
+
export declare class TimeFlyAiClient {
|
|
8
|
+
private readonly source;
|
|
9
|
+
private readonly sourceVersion;
|
|
10
|
+
private readonly queueFilePath;
|
|
11
|
+
private apiBaseUrl;
|
|
12
|
+
private accessToken?;
|
|
13
|
+
private refreshToken?;
|
|
14
|
+
private authFilePath?;
|
|
15
|
+
private credentialsLoaded;
|
|
16
|
+
constructor(config: AiSdkConfig);
|
|
17
|
+
getSource(): AiSdkConfig['source'];
|
|
18
|
+
getSourceVersion(): string;
|
|
19
|
+
recordEvents(events: AiUsageEvent[]): Promise<RecordResult>;
|
|
20
|
+
flushPendingQueue(): Promise<AiSyncResponse | undefined>;
|
|
21
|
+
private ensureCredentialsLoaded;
|
|
22
|
+
private persistRefreshedTokens;
|
|
23
|
+
private refreshTokensIfPossible;
|
|
24
|
+
private syncEventsWithRefresh;
|
|
25
|
+
private parseSyncResponse;
|
|
26
|
+
private syncEvents;
|
|
27
|
+
}
|
|
28
|
+
export declare const createTimeFlyAiClient: (config: Partial<AiSdkConfig> & Pick<AiSdkConfig, "source" | "sourceVersion">) => TimeFlyAiClient;
|
|
29
|
+
//# sourceMappingURL=timefly-ai-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timefly-ai-client.d.ts","sourceRoot":"","sources":["../src/timefly-ai-client.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAe,cAAc,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAWxF,MAAM,MAAM,YAAY,GAAG;IAC1B,MAAM,EAAE,OAAO,CAAA;IACf,MAAM,EAAE,OAAO,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;CAChB,CAAA;AAID,qBAAa,eAAe;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA8B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAQ;IACtC,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,WAAW,CAAC,CAAQ;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAQ;IAC7B,OAAO,CAAC,YAAY,CAAC,CAAQ;IAC7B,OAAO,CAAC,iBAAiB,CAAQ;gBAErB,MAAM,EAAE,WAAW;IAU/B,SAAS,IAAI,WAAW,CAAC,QAAQ,CAAC;IAIlC,gBAAgB,IAAI,MAAM;IAI1B,YAAY,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC;IAiC3D,iBAAiB,IAAI,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAiBxD,OAAO,CAAC,uBAAuB;IAqB/B,OAAO,CAAC,sBAAsB;IAe9B,OAAO,CAAC,uBAAuB;IAa/B,OAAO,CAAC,qBAAqB;IAU7B,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,UAAU;CA+BlB;AAED,eAAO,MAAM,qBAAqB,GAAI,QAAQ,OAAO,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,QAAQ,GAAG,eAAe,CAAC,KAAG,eASlH,CAAA"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { gzipSync } from 'node:zlib';
|
|
2
|
+
import { resolveAuthCredentials, writeAuthFile } from './auth-credentials.js';
|
|
3
|
+
import { appendToLocalQueue, clearAcceptedFromQueue, readLocalQueue, resolveQueueFilePath } from './local-queue.js';
|
|
4
|
+
import { isSyncFailure, parseSyncFailure } from './sync-error.js';
|
|
5
|
+
import { refreshAccessToken } from './token-refresh.js';
|
|
6
|
+
const buildAiSyncUserAgent = (source, sourceVersion) => `TimeFlyAi|${source}|${sourceVersion}`;
|
|
7
|
+
export class TimeFlyAiClient {
|
|
8
|
+
source;
|
|
9
|
+
sourceVersion;
|
|
10
|
+
queueFilePath;
|
|
11
|
+
apiBaseUrl;
|
|
12
|
+
accessToken;
|
|
13
|
+
refreshToken;
|
|
14
|
+
authFilePath;
|
|
15
|
+
credentialsLoaded = false;
|
|
16
|
+
constructor(config) {
|
|
17
|
+
this.apiBaseUrl = config.apiBaseUrl.replace(/\/$/, '');
|
|
18
|
+
this.accessToken = config.accessToken;
|
|
19
|
+
this.refreshToken = config.refreshToken;
|
|
20
|
+
this.authFilePath = config.authFilePath;
|
|
21
|
+
this.source = config.source;
|
|
22
|
+
this.sourceVersion = config.sourceVersion;
|
|
23
|
+
this.queueFilePath = resolveQueueFilePath(config.queueFilePath);
|
|
24
|
+
}
|
|
25
|
+
getSource() {
|
|
26
|
+
return this.source;
|
|
27
|
+
}
|
|
28
|
+
getSourceVersion() {
|
|
29
|
+
return this.sourceVersion;
|
|
30
|
+
}
|
|
31
|
+
recordEvents(events) {
|
|
32
|
+
if (!events.length) {
|
|
33
|
+
return Promise.resolve({ synced: false, queued: false, accepted: 0 });
|
|
34
|
+
}
|
|
35
|
+
return this.ensureCredentialsLoaded()
|
|
36
|
+
.then(() => this.flushPendingQueue())
|
|
37
|
+
.then(() => this.syncEventsWithRefresh(events))
|
|
38
|
+
.then((syncResponse) => clearAcceptedFromQueue(this.queueFilePath, new Set(events.map((event) => event.eventId))).then(() => ({
|
|
39
|
+
synced: true,
|
|
40
|
+
queued: false,
|
|
41
|
+
accepted: syncResponse.accepted
|
|
42
|
+
})))
|
|
43
|
+
.catch((error) => appendToLocalQueue(this.queueFilePath, events).then(() => {
|
|
44
|
+
if (isSyncFailure(error)) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
synced: false,
|
|
49
|
+
queued: true,
|
|
50
|
+
accepted: 0
|
|
51
|
+
};
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
flushPendingQueue() {
|
|
55
|
+
return this.ensureCredentialsLoaded()
|
|
56
|
+
.then(() => readLocalQueue(this.queueFilePath))
|
|
57
|
+
.then((queue) => {
|
|
58
|
+
if (!queue.events.length) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return this.syncEventsWithRefresh(queue.events).then((syncResponse) => clearAcceptedFromQueue(this.queueFilePath, new Set(queue.events.map((event) => event.eventId))).then(() => syncResponse));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
ensureCredentialsLoaded() {
|
|
65
|
+
if (this.credentialsLoaded) {
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
}
|
|
68
|
+
return resolveAuthCredentials({
|
|
69
|
+
accessToken: this.accessToken,
|
|
70
|
+
refreshToken: this.refreshToken,
|
|
71
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
72
|
+
authFilePath: this.authFilePath
|
|
73
|
+
}).then((credentials) => {
|
|
74
|
+
this.accessToken = credentials.accessToken;
|
|
75
|
+
this.refreshToken = credentials.refreshToken;
|
|
76
|
+
this.authFilePath = credentials.authFilePath;
|
|
77
|
+
if (credentials.apiBaseUrl) {
|
|
78
|
+
this.apiBaseUrl = credentials.apiBaseUrl.replace(/\/$/, '');
|
|
79
|
+
}
|
|
80
|
+
this.credentialsLoaded = true;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
persistRefreshedTokens(accessToken, refreshToken) {
|
|
84
|
+
this.accessToken = accessToken;
|
|
85
|
+
this.refreshToken = refreshToken;
|
|
86
|
+
if (!this.authFilePath) {
|
|
87
|
+
return Promise.resolve();
|
|
88
|
+
}
|
|
89
|
+
return writeAuthFile(this.authFilePath, {
|
|
90
|
+
accessToken,
|
|
91
|
+
refreshToken,
|
|
92
|
+
apiBaseUrl: this.apiBaseUrl
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
refreshTokensIfPossible() {
|
|
96
|
+
if (!this.refreshToken) {
|
|
97
|
+
return Promise.reject(parseSyncFailure(401, 'TIMEFLY_ACCESS_TOKEN is required for sync'));
|
|
98
|
+
}
|
|
99
|
+
return refreshAccessToken({
|
|
100
|
+
apiBaseUrl: this.apiBaseUrl,
|
|
101
|
+
refreshToken: this.refreshToken
|
|
102
|
+
})
|
|
103
|
+
.then((refreshedTokens) => this.persistRefreshedTokens(refreshedTokens.accessToken, refreshedTokens.refreshToken))
|
|
104
|
+
.then(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
syncEventsWithRefresh(events, hasRetried = false) {
|
|
107
|
+
return this.syncEvents(events).catch((error) => {
|
|
108
|
+
if (!hasRetried && isSyncFailure(error) && error.isUnauthorized) {
|
|
109
|
+
return this.refreshTokensIfPossible().then(() => this.syncEventsWithRefresh(events, true));
|
|
110
|
+
}
|
|
111
|
+
throw error;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
parseSyncResponse(response, responseBody) {
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw parseSyncFailure(response.status, responseBody);
|
|
117
|
+
}
|
|
118
|
+
const envelope = JSON.parse(responseBody);
|
|
119
|
+
const accepted = envelope.data?.accepted ?? envelope.data?.queued ?? 0;
|
|
120
|
+
return { accepted };
|
|
121
|
+
}
|
|
122
|
+
syncEvents(events) {
|
|
123
|
+
if (!this.accessToken) {
|
|
124
|
+
return Promise.reject(parseSyncFailure(401, 'TIMEFLY_ACCESS_TOKEN is required for sync'));
|
|
125
|
+
}
|
|
126
|
+
const batch = { events };
|
|
127
|
+
const payload = JSON.stringify(batch);
|
|
128
|
+
const compressedBody = gzipSync(Buffer.from(payload, 'utf8'));
|
|
129
|
+
const url = `${this.apiBaseUrl}/ai/sync`;
|
|
130
|
+
return fetch(url, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: {
|
|
133
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
134
|
+
'Content-Type': 'application/json',
|
|
135
|
+
'Content-Encoding': 'gzip',
|
|
136
|
+
'Accept-Encoding': 'gzip',
|
|
137
|
+
'User-Agent': buildAiSyncUserAgent(this.source, this.sourceVersion),
|
|
138
|
+
'X-TimeFly-Source': this.source,
|
|
139
|
+
'X-TimeFly-Source-Version': this.sourceVersion
|
|
140
|
+
},
|
|
141
|
+
body: compressedBody
|
|
142
|
+
})
|
|
143
|
+
.then((response) => response.text().then((responseBody) => ({
|
|
144
|
+
response,
|
|
145
|
+
responseBody
|
|
146
|
+
})))
|
|
147
|
+
.then(({ response, responseBody }) => this.parseSyncResponse(response, responseBody));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export const createTimeFlyAiClient = (config) => new TimeFlyAiClient({
|
|
151
|
+
apiBaseUrl: config.apiBaseUrl ?? process.env.TIMEFLY_API_BASE_URL ?? 'https://api.timefly.dev',
|
|
152
|
+
accessToken: config.accessToken ?? process.env.TIMEFLY_ACCESS_TOKEN,
|
|
153
|
+
refreshToken: config.refreshToken,
|
|
154
|
+
authFilePath: config.authFilePath,
|
|
155
|
+
source: config.source,
|
|
156
|
+
sourceVersion: config.sourceVersion,
|
|
157
|
+
queueFilePath: config.queueFilePath
|
|
158
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type RefreshTokenResponse = {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
refreshToken: string;
|
|
4
|
+
};
|
|
5
|
+
export type RefreshTokenFailure = {
|
|
6
|
+
statusCode: number;
|
|
7
|
+
message: string;
|
|
8
|
+
isSupporterRequired: boolean;
|
|
9
|
+
isUnauthorized: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare const refreshAccessToken: (input: {
|
|
12
|
+
apiBaseUrl: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
deviceFingerprint?: string;
|
|
15
|
+
}) => Promise<RefreshTokenResponse>;
|
|
16
|
+
//# sourceMappingURL=token-refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-refresh.d.ts","sourceRoot":"","sources":["../src/token-refresh.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,oBAAoB,GAAG;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,mBAAmB,EAAE,OAAO,CAAA;IAC5B,cAAc,EAAE,OAAO,CAAA;CACvB,CAAA;AA+BD,eAAO,MAAM,kBAAkB,GAAI,OAAO;IACzC,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC1B,KAAG,OAAO,CAAC,oBAAoB,CAkB/B,CAAA"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const parseRefreshResponse = (response, responseBody) => {
|
|
2
|
+
if (!response.ok) {
|
|
3
|
+
const isSupporterRequired = response.status === 403 && responseBody.includes('Supporter');
|
|
4
|
+
const failure = {
|
|
5
|
+
statusCode: response.status,
|
|
6
|
+
message: responseBody || `Token refresh failed: ${response.status}`,
|
|
7
|
+
isSupporterRequired,
|
|
8
|
+
isUnauthorized: response.status === 401
|
|
9
|
+
};
|
|
10
|
+
throw failure;
|
|
11
|
+
}
|
|
12
|
+
const envelope = JSON.parse(responseBody);
|
|
13
|
+
const nestedTokens = envelope.data?.tokens ?? envelope.tokens;
|
|
14
|
+
if (!nestedTokens?.accessToken || !nestedTokens.refreshToken) {
|
|
15
|
+
throw new Error('Token refresh response missing tokens');
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
accessToken: nestedTokens.accessToken,
|
|
19
|
+
refreshToken: nestedTokens.refreshToken
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export const refreshAccessToken = (input) => {
|
|
23
|
+
const url = `${input.apiBaseUrl.replace(/\/$/, '')}/auth/extension/refresh`;
|
|
24
|
+
return fetch(url, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
...(input.deviceFingerprint ? { 'x-device-fingerprint': input.deviceFingerprint } : {})
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({ refreshToken: input.refreshToken })
|
|
31
|
+
})
|
|
32
|
+
.then((response) => response.text().then((responseBody) => ({
|
|
33
|
+
response,
|
|
34
|
+
responseBody
|
|
35
|
+
})))
|
|
36
|
+
.then(({ response, responseBody }) => parseRefreshResponse(response, responseBody));
|
|
37
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type AiToolSource = 'opencode' | 'claude-code' | 'cursor' | 'copilot' | 'windsurf' | 'unknown';
|
|
2
|
+
export type AiUsageEventType = 'session_start' | 'session_end' | 'llm_request' | 'llm_response' | 'tool_call' | 'tool_result' | 'turn_complete' | 'compaction' | 'error';
|
|
3
|
+
export type AiUsageMetadata = Record<string, string | number | boolean | undefined>;
|
|
4
|
+
export type AiUsageEvent = {
|
|
5
|
+
eventId: string;
|
|
6
|
+
source: AiToolSource;
|
|
7
|
+
sourceVersion: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
eventType: AiUsageEventType;
|
|
10
|
+
eventAtUtc: string;
|
|
11
|
+
localDay: string;
|
|
12
|
+
modelId?: string;
|
|
13
|
+
planMode?: string;
|
|
14
|
+
inputTokens?: number;
|
|
15
|
+
outputTokens?: number;
|
|
16
|
+
totalTokens?: number;
|
|
17
|
+
toolName?: string;
|
|
18
|
+
durationMs?: number;
|
|
19
|
+
metadata?: AiUsageMetadata;
|
|
20
|
+
};
|
|
21
|
+
export type AiSyncBatch = {
|
|
22
|
+
events: AiUsageEvent[];
|
|
23
|
+
};
|
|
24
|
+
export type AiSyncResponse = {
|
|
25
|
+
accepted: number;
|
|
26
|
+
};
|
|
27
|
+
export type AiSdkConfig = {
|
|
28
|
+
apiBaseUrl: string;
|
|
29
|
+
accessToken?: string;
|
|
30
|
+
refreshToken?: string;
|
|
31
|
+
authFilePath?: string;
|
|
32
|
+
source: AiToolSource;
|
|
33
|
+
sourceVersion: string;
|
|
34
|
+
queueFilePath?: string;
|
|
35
|
+
};
|
|
36
|
+
export type CreateAiUsageEventInput = {
|
|
37
|
+
sessionId: string;
|
|
38
|
+
eventType: AiUsageEventType;
|
|
39
|
+
modelId?: string;
|
|
40
|
+
planMode?: string;
|
|
41
|
+
inputTokens?: number;
|
|
42
|
+
outputTokens?: number;
|
|
43
|
+
totalTokens?: number;
|
|
44
|
+
toolName?: string;
|
|
45
|
+
durationMs?: number;
|
|
46
|
+
metadata?: AiUsageMetadata;
|
|
47
|
+
eventAtUtc?: Date;
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,aAAa,GAAG,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAA;AAErG,MAAM,MAAM,gBAAgB,GACzB,eAAe,GACf,aAAa,GACb,aAAa,GACb,cAAc,GACd,WAAW,GACX,aAAa,GACb,eAAe,GACf,YAAY,GACZ,OAAO,CAAA;AAEV,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC,CAAA;AAEnF,MAAM,MAAM,YAAY,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,YAAY,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,gBAAgB,CAAA;IAC3B,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,eAAe,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACzB,MAAM,EAAE,YAAY,EAAE,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAC5B,QAAQ,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACzB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,YAAY,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACrC,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,gBAAgB,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,EAAE,eAAe,CAAA;IAC1B,UAAU,CAAC,EAAE,IAAI,CAAA;CACjB,CAAA"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@timefly/ai-sdk",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "TimeFly SDK for AI usage telemetry",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p test/tsconfig.json --noEmit",
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"prepublishOnly": "bun run build && bun run typecheck && bun test"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/TimeFly-Dev/ai-integrations.git",
|
|
27
|
+
"directory": "packages/sdk"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"timefly",
|
|
34
|
+
"telemetry",
|
|
35
|
+
"ai",
|
|
36
|
+
"usage",
|
|
37
|
+
"analytics"
|
|
38
|
+
],
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"uuid": "^14.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/bun": "^1.3.14",
|
|
45
|
+
"typescript": "^5.9.3"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
}
|
|
50
|
+
}
|