@timefly/ai-sdk 0.2.0 → 0.2.1
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
CHANGED
|
@@ -56,7 +56,7 @@ client.recordEvents([event]).then((result) => {
|
|
|
56
56
|
|
|
57
57
|
`POST {apiBaseUrl}/ai/sync` — gzip JSON body `{ events: AiUsageEvent[] }`
|
|
58
58
|
|
|
59
|
-
Stored in ClickHouse table `
|
|
59
|
+
Stored in ClickHouse table `activity_events` as `ai.*` activities.
|
|
60
60
|
|
|
61
61
|
## Environment
|
|
62
62
|
|
|
@@ -1 +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;
|
|
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;AAMD,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,CAwB3F,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"}
|
package/dist/auth-credentials.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
const DEFAULT_AUTH_DIRECTORY = path.join(os.homedir(), '.config', 'opencode');
|
|
5
5
|
const DEFAULT_AUTH_FILE = path.join(DEFAULT_AUTH_DIRECTORY, 'timefly-auth.json');
|
|
6
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
6
7
|
export const resolveDefaultAuthFilePath = () => DEFAULT_AUTH_FILE;
|
|
7
8
|
export const readAuthFile = (authFilePath) => readFile(authFilePath, 'utf8')
|
|
8
9
|
.then((rawContent) => JSON.parse(rawContent))
|
|
@@ -10,11 +11,16 @@ export const readAuthFile = (authFilePath) => readFile(authFilePath, 'utf8')
|
|
|
10
11
|
.catch(() => undefined);
|
|
11
12
|
export const writeAuthFile = (authFilePath, authFile) => {
|
|
12
13
|
const authDirectory = path.dirname(authFilePath);
|
|
13
|
-
|
|
14
|
-
.then(() => writeFile(authFilePath, JSON.stringify({
|
|
14
|
+
const authFileContent = JSON.stringify({
|
|
15
15
|
...authFile,
|
|
16
16
|
savedAt: new Date().toISOString()
|
|
17
|
-
}, null, 2)
|
|
17
|
+
}, null, 2);
|
|
18
|
+
return mkdir(authDirectory, { recursive: true })
|
|
19
|
+
.then(() => writeFile(authFilePath, authFileContent, {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
mode: PRIVATE_FILE_MODE
|
|
22
|
+
}))
|
|
23
|
+
.then(() => chmod(authFilePath, PRIVATE_FILE_MODE).catch(() => undefined))
|
|
18
24
|
.then(() => undefined);
|
|
19
25
|
};
|
|
20
26
|
export const resolveAuthCredentials = (config) => {
|
|
@@ -1 +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;
|
|
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;AAK9C,MAAM,MAAM,UAAU,GAAG;IACxB,MAAM,EAAE,YAAY,EAAE,CAAA;CACtB,CAAA;AAqBD,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,CAKwD,CAAA;AAEvE,eAAO,MAAM,oBAAoB,GAAI,aAAa,MAAM,KAAG,MACA,CAAA"}
|
package/dist/local-queue.js
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const DEFAULT_QUEUE_FILE = '.timefly-ai-queue.json';
|
|
4
|
+
const MAX_LOCAL_QUEUE_EVENTS = 5000;
|
|
4
5
|
const emptyQueue = () => ({ events: [] });
|
|
6
|
+
const mergeQueuedEvents = (currentEvents, nextEvents) => Array.from(new Map([...currentEvents, ...nextEvents].map((event) => [event.eventId, event])).values()).slice(-MAX_LOCAL_QUEUE_EVENTS);
|
|
7
|
+
const writeLocalQueue = (queueFilePath, queue) => {
|
|
8
|
+
if (!queue.events.length) {
|
|
9
|
+
return rm(queueFilePath, { force: true }).then(() => undefined);
|
|
10
|
+
}
|
|
11
|
+
const directory = path.dirname(queueFilePath);
|
|
12
|
+
return mkdir(directory, { recursive: true })
|
|
13
|
+
.then(() => writeFile(queueFilePath, JSON.stringify(queue, null, 2), 'utf8'))
|
|
14
|
+
.then(() => undefined);
|
|
15
|
+
};
|
|
5
16
|
export const readLocalQueue = (queueFilePath) => readFile(queueFilePath, 'utf8')
|
|
6
17
|
.then((rawContent) => JSON.parse(rawContent))
|
|
7
18
|
.catch(() => emptyQueue());
|
|
8
19
|
export const appendToLocalQueue = (queueFilePath, events) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
if (!events.length) {
|
|
21
|
+
return Promise.resolve();
|
|
22
|
+
}
|
|
23
|
+
return readLocalQueue(queueFilePath)
|
|
12
24
|
.then((queue) => ({
|
|
13
|
-
events:
|
|
25
|
+
events: mergeQueuedEvents(queue.events, events)
|
|
14
26
|
}))
|
|
15
|
-
.then((updatedQueue) =>
|
|
16
|
-
.then(() => undefined);
|
|
27
|
+
.then((updatedQueue) => writeLocalQueue(queueFilePath, updatedQueue));
|
|
17
28
|
};
|
|
18
29
|
export const clearAcceptedFromQueue = (queueFilePath, acceptedEventIds) => readLocalQueue(queueFilePath)
|
|
19
30
|
.then((queue) => ({
|
|
20
31
|
events: queue.events.filter((event) => !acceptedEventIds.has(event.eventId))
|
|
21
32
|
}))
|
|
22
|
-
.then((updatedQueue) =>
|
|
23
|
-
.then(() => undefined);
|
|
33
|
+
.then((updatedQueue) => writeLocalQueue(queueFilePath, updatedQueue));
|
|
24
34
|
export const resolveQueueFilePath = (customPath) => customPath ?? path.join(process.cwd(), DEFAULT_QUEUE_FILE);
|
|
@@ -1 +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;
|
|
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;AAaD,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;IA8B3D,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"}
|
|
@@ -4,6 +4,9 @@ import { appendToLocalQueue, clearAcceptedFromQueue, readLocalQueue, resolveQueu
|
|
|
4
4
|
import { isSyncFailure, parseSyncFailure } from './sync-error.js';
|
|
5
5
|
import { refreshAccessToken } from './token-refresh.js';
|
|
6
6
|
const buildAiSyncUserAgent = (source, sourceVersion) => `TimeFlyAi|${source}|${sourceVersion}`;
|
|
7
|
+
const acceptedEventIdsFromResponse = (events, syncResponse) => new Set(events.slice(0, syncResponse.accepted).map((event) => event.eventId));
|
|
8
|
+
const unacceptedEventsFromResponse = (events, syncResponse) => events.slice(syncResponse.accepted);
|
|
9
|
+
const isPermanentSyncFailure = (error) => isSyncFailure(error) && (error.isUnauthorized || error.isSupporterRequired);
|
|
7
10
|
export class TimeFlyAiClient {
|
|
8
11
|
source;
|
|
9
12
|
sourceVersion;
|
|
@@ -35,21 +38,23 @@ export class TimeFlyAiClient {
|
|
|
35
38
|
return this.ensureCredentialsLoaded()
|
|
36
39
|
.then(() => this.flushPendingQueue())
|
|
37
40
|
.then(() => this.syncEventsWithRefresh(events))
|
|
38
|
-
.then((syncResponse) =>
|
|
41
|
+
.then((syncResponse) => appendToLocalQueue(this.queueFilePath, unacceptedEventsFromResponse(events, syncResponse)).then(() => ({
|
|
39
42
|
synced: true,
|
|
40
|
-
queued:
|
|
43
|
+
queued: syncResponse.accepted < events.length,
|
|
41
44
|
accepted: syncResponse.accepted
|
|
42
45
|
})))
|
|
43
|
-
.catch((error) =>
|
|
44
|
-
if (
|
|
46
|
+
.catch((error) => {
|
|
47
|
+
if (isPermanentSyncFailure(error)) {
|
|
45
48
|
throw error;
|
|
46
49
|
}
|
|
47
|
-
return {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
return appendToLocalQueue(this.queueFilePath, events).then(() => {
|
|
51
|
+
return {
|
|
52
|
+
synced: false,
|
|
53
|
+
queued: true,
|
|
54
|
+
accepted: 0
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
});
|
|
53
58
|
}
|
|
54
59
|
flushPendingQueue() {
|
|
55
60
|
return this.ensureCredentialsLoaded()
|
|
@@ -58,7 +63,7 @@ export class TimeFlyAiClient {
|
|
|
58
63
|
if (!queue.events.length) {
|
|
59
64
|
return undefined;
|
|
60
65
|
}
|
|
61
|
-
return this.syncEventsWithRefresh(queue.events).then((syncResponse) => clearAcceptedFromQueue(this.queueFilePath,
|
|
66
|
+
return this.syncEventsWithRefresh(queue.events).then((syncResponse) => clearAcceptedFromQueue(this.queueFilePath, acceptedEventIdsFromResponse(queue.events, syncResponse)).then(() => syncResponse));
|
|
62
67
|
});
|
|
63
68
|
}
|
|
64
69
|
ensureCredentialsLoaded() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timefly/ai-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "TimeFly SDK for AI usage telemetry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,6 +45,6 @@
|
|
|
45
45
|
"typescript": "^5.9.3"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
|
-
"
|
|
48
|
+
"bun": ">=1.3.0"
|
|
49
49
|
}
|
|
50
50
|
}
|