@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 `ai_usage_events`.
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;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"}
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"}
@@ -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
- return mkdir(authDirectory, { recursive: true })
14
- .then(() => writeFile(authFilePath, JSON.stringify({
14
+ const authFileContent = JSON.stringify({
15
15
  ...authFile,
16
16
  savedAt: new Date().toISOString()
17
- }, null, 2), 'utf8'))
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;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"}
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"}
@@ -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
- const directory = path.dirname(queueFilePath);
10
- return mkdir(directory, { recursive: true })
11
- .then(() => readLocalQueue(queueFilePath))
20
+ if (!events.length) {
21
+ return Promise.resolve();
22
+ }
23
+ return readLocalQueue(queueFilePath)
12
24
  .then((queue) => ({
13
- events: [...queue.events, ...events]
25
+ events: mergeQueuedEvents(queue.events, events)
14
26
  }))
15
- .then((updatedQueue) => writeFile(queueFilePath, JSON.stringify(updatedQueue, null, 2), 'utf8'))
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) => writeFile(queueFilePath, JSON.stringify(updatedQueue, null, 2), 'utf8'))
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;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"}
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) => clearAcceptedFromQueue(this.queueFilePath, new Set(events.map((event) => event.eventId))).then(() => ({
41
+ .then((syncResponse) => appendToLocalQueue(this.queueFilePath, unacceptedEventsFromResponse(events, syncResponse)).then(() => ({
39
42
  synced: true,
40
- queued: false,
43
+ queued: syncResponse.accepted < events.length,
41
44
  accepted: syncResponse.accepted
42
45
  })))
43
- .catch((error) => appendToLocalQueue(this.queueFilePath, events).then(() => {
44
- if (isSyncFailure(error)) {
46
+ .catch((error) => {
47
+ if (isPermanentSyncFailure(error)) {
45
48
  throw error;
46
49
  }
47
- return {
48
- synced: false,
49
- queued: true,
50
- accepted: 0
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, new Set(queue.events.map((event) => event.eventId))).then(() => syncResponse));
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.0",
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
- "node": ">=18"
48
+ "bun": ">=1.3.0"
49
49
  }
50
50
  }