@remnic/connector-limitless 9.3.631
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/LICENSE +21 -0
- package/README.md +88 -0
- package/dist/index.d.ts +116 -0
- package/dist/index.js +299 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Joshua Warren
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @remnic/connector-limitless
|
|
2
|
+
|
|
3
|
+
Limitless.ai Pendant connector for [Remnic](https://github.com/joshuaswarren/remnic).
|
|
4
|
+
Pulls your Pendant lifelogs into Remnic's wearable-transcript pipeline:
|
|
5
|
+
cleaned, speaker-labeled, redacted, searchable day transcripts — and,
|
|
6
|
+
under per-source trust gates, memories.
|
|
7
|
+
|
|
8
|
+
This is an **à-la-carte optional companion** of `@remnic/core`. The base
|
|
9
|
+
install never pulls it in; install it only if you wear a Pendant:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @remnic/connector-limitless
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Remnic discovers it at runtime. No further registration is needed.
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
1. Create an API key in the Limitless app: **Developer settings →
|
|
20
|
+
Create API Key**.
|
|
21
|
+
2. Provide the key via environment variable (preferred) or config:
|
|
22
|
+
- `LIMITLESS_API_KEY` (or `REMNIC_LIMITLESS_API_KEY`)
|
|
23
|
+
- or `wearables.sources.limitless.apiKey` in the plugin config
|
|
24
|
+
3. Enable the source:
|
|
25
|
+
|
|
26
|
+
```jsonc
|
|
27
|
+
{
|
|
28
|
+
"wearables": {
|
|
29
|
+
"enabled": true,
|
|
30
|
+
"sources": {
|
|
31
|
+
"limitless": {
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"memoryMode": "smart" // smart (default) | off | review | auto
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
4. Sync:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
remnic wearables check limitless # verify the key
|
|
44
|
+
remnic wearables sync --source limitless --days 7
|
|
45
|
+
remnic wearables transcript --date 2026-06-10
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## What it pulls
|
|
49
|
+
|
|
50
|
+
- Pendant **lifelogs** via `GET /v1/lifelogs` (day-windowed, cursor
|
|
51
|
+
pagination, `includeContents` for the diarized segments).
|
|
52
|
+
- Dialogue comes from `blockquote` content nodes. The wearer is
|
|
53
|
+
pre-identified by the API (`speakerIdentifier: "user"`); other
|
|
54
|
+
speakers carry Limitless display names ("Speaker 2" or saved names)
|
|
55
|
+
and can be renamed once via
|
|
56
|
+
`remnic wearables speakers set limitless "Speaker 2" "Jane Doe"` —
|
|
57
|
+
the label applies to every stored transcript going forward.
|
|
58
|
+
|
|
59
|
+
The Limitless API is in beta and currently exposes Pendant data only
|
|
60
|
+
(no web/desktop meetings). Rate limits (180 req/min) are respected with
|
|
61
|
+
automatic retry honoring the API's `retryAfter`.
|
|
62
|
+
|
|
63
|
+
## Trust gating (worth reading)
|
|
64
|
+
|
|
65
|
+
Limitless transcription quality varies, so memory creation is
|
|
66
|
+
trust-gated. The default `memoryMode: "smart"` is fully automated:
|
|
67
|
+
Remnic's LLM extraction judge plus a per-source trust prior
|
|
68
|
+
(`sourceTrust`) and corroboration (agreement with another wearable's
|
|
69
|
+
same-day transcript, or with an existing memory) combine into a trust
|
|
70
|
+
score — high-trust facts are written active, borderline facts go to the
|
|
71
|
+
review queue, low-trust facts are dropped. Tune with `sourceTrust`,
|
|
72
|
+
`autoApproveTrust`, `reviewTrust`, and `maxMemoriesPerDay`, or switch
|
|
73
|
+
to `"review"` (everything queued), `"auto"`, or `"off"`.
|
|
74
|
+
|
|
75
|
+
Full documentation: [docs/wearables.md](https://github.com/joshuaswarren/remnic/blob/main/docs/wearables.md).
|
|
76
|
+
|
|
77
|
+
## Programmatic use
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { createLimitlessConnector, LimitlessClient } from "@remnic/connector-limitless";
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The package also exports the raw `LimitlessClient` (typed lifelog
|
|
84
|
+
shapes, pagination, retry) for standalone use.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { WearableConversation, WearableConnectorFactoryOptions, WearableSourceConnector, WearableConnectorRegistration } from '@remnic/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal Limitless Developer API client (raw fetch, no SDK).
|
|
5
|
+
*
|
|
6
|
+
* Contract verified against https://www.limitless.ai/developers/docs/api
|
|
7
|
+
* (2026-06): base https://api.limitless.ai, header `X-API-Key`,
|
|
8
|
+
* `GET /v1/lifelogs` with cursor pagination at `meta.lifelogs.nextCursor`
|
|
9
|
+
* and a hard per-page max of 10. Rate limit is 180 req/min; 429 bodies
|
|
10
|
+
* carry `retryAfter` (a string, in seconds).
|
|
11
|
+
*
|
|
12
|
+
* The API key is never logged and never included in thrown error
|
|
13
|
+
* messages.
|
|
14
|
+
*/
|
|
15
|
+
declare const LIMITLESS_DEFAULT_BASE_URL = "https://api.limitless.ai";
|
|
16
|
+
/** Hard API maximum for `limit` on /v1/lifelogs. */
|
|
17
|
+
declare const LIFELOGS_MAX_PAGE_SIZE = 10;
|
|
18
|
+
interface LimitlessContentNode {
|
|
19
|
+
type: string;
|
|
20
|
+
content?: string;
|
|
21
|
+
startTime?: string;
|
|
22
|
+
endTime?: string;
|
|
23
|
+
startOffsetMs?: number;
|
|
24
|
+
endOffsetMs?: number;
|
|
25
|
+
children?: LimitlessContentNode[];
|
|
26
|
+
speakerName?: string | null;
|
|
27
|
+
speakerIdentifier?: "user" | null;
|
|
28
|
+
}
|
|
29
|
+
interface LimitlessLifelog {
|
|
30
|
+
id: string;
|
|
31
|
+
title?: string;
|
|
32
|
+
markdown?: string | null;
|
|
33
|
+
startTime?: string;
|
|
34
|
+
endTime?: string;
|
|
35
|
+
isStarred?: boolean;
|
|
36
|
+
updatedAt?: string;
|
|
37
|
+
contents?: LimitlessContentNode[];
|
|
38
|
+
}
|
|
39
|
+
interface LifelogsPage {
|
|
40
|
+
lifelogs: LimitlessLifelog[];
|
|
41
|
+
nextCursor: string | null;
|
|
42
|
+
}
|
|
43
|
+
interface LimitlessClientOptions {
|
|
44
|
+
apiKey: string;
|
|
45
|
+
baseUrl?: string;
|
|
46
|
+
/** Injectable for tests. Defaults to global fetch. */
|
|
47
|
+
fetchImpl?: typeof fetch;
|
|
48
|
+
/** Per-request timeout (ms). */
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
/** Injectable sleep for tests. */
|
|
51
|
+
sleep?: (ms: number) => Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
declare class LimitlessApiError extends Error {
|
|
54
|
+
readonly status?: number | undefined;
|
|
55
|
+
constructor(message: string, status?: number | undefined);
|
|
56
|
+
}
|
|
57
|
+
declare class LimitlessClient {
|
|
58
|
+
private readonly apiKey;
|
|
59
|
+
private readonly baseUrl;
|
|
60
|
+
private readonly fetchImpl;
|
|
61
|
+
private readonly timeoutMs;
|
|
62
|
+
private readonly sleep;
|
|
63
|
+
constructor(options: LimitlessClientOptions);
|
|
64
|
+
/** One page of lifelogs for a single day. */
|
|
65
|
+
listLifelogs(params: {
|
|
66
|
+
date: string;
|
|
67
|
+
timezone: string;
|
|
68
|
+
cursor?: string | null;
|
|
69
|
+
signal?: AbortSignal;
|
|
70
|
+
}): Promise<LifelogsPage>;
|
|
71
|
+
/** Cheap auth probe. */
|
|
72
|
+
verifyAuth(signal?: AbortSignal): Promise<{
|
|
73
|
+
ok: boolean;
|
|
74
|
+
detail?: string;
|
|
75
|
+
}>;
|
|
76
|
+
private requestJson;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Normalize Limitless lifelogs into Remnic's provider-agnostic
|
|
81
|
+
* `WearableConversation` shape.
|
|
82
|
+
*
|
|
83
|
+
* Spoken dialogue lives in `blockquote` content nodes (possibly nested
|
|
84
|
+
* under headings via `children`); heading nodes structure the lifelog
|
|
85
|
+
* and are not utterances. The wearer is marked by
|
|
86
|
+
* `speakerIdentifier === "user"`; everyone else carries a
|
|
87
|
+
* `speakerName` ("Speaker 2" or a saved name).
|
|
88
|
+
*/
|
|
89
|
+
|
|
90
|
+
declare const LIMITLESS_SOURCE_ID = "limitless";
|
|
91
|
+
declare function lifelogToConversation(lifelog: LimitlessLifelog): WearableConversation;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @remnic/connector-limitless — Limitless.ai Pendant connector.
|
|
95
|
+
*
|
|
96
|
+
* À-la-carte optional companion of @remnic/core: installing core alone
|
|
97
|
+
* never pulls this in; core discovers it at runtime via a
|
|
98
|
+
* computed-specifier dynamic import (see wearables/registry.ts) or via
|
|
99
|
+
* a direct import of this module, which self-registers idempotently.
|
|
100
|
+
*
|
|
101
|
+
* API key: `wearables.sources.limitless.apiKey`, else the
|
|
102
|
+
* `REMNIC_LIMITLESS_API_KEY` / `LIMITLESS_API_KEY` environment
|
|
103
|
+
* variables (checked in that order).
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
declare function resolveLimitlessApiKey(configuredKey: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
|
|
107
|
+
declare function createLimitlessConnector(options: WearableConnectorFactoryOptions): WearableSourceConnector;
|
|
108
|
+
declare const wearableConnectorRegistration: WearableConnectorRegistration;
|
|
109
|
+
/**
|
|
110
|
+
* Idempotently register the connector with the core registry. Importing
|
|
111
|
+
* this module registers it as a side effect; calling this again is safe
|
|
112
|
+
* (returns false when already registered).
|
|
113
|
+
*/
|
|
114
|
+
declare function ensureLimitlessConnectorRegistered(): boolean;
|
|
115
|
+
|
|
116
|
+
export { LIFELOGS_MAX_PAGE_SIZE, LIMITLESS_DEFAULT_BASE_URL, LIMITLESS_SOURCE_ID, type LifelogsPage, LimitlessApiError, LimitlessClient, type LimitlessClientOptions, type LimitlessContentNode, type LimitlessLifelog, createLimitlessConnector, ensureLimitlessConnectorRegistered, lifelogToConversation, resolveLimitlessApiKey, wearableConnectorRegistration };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// openclaw-engram: Local-first memory plugin
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import {
|
|
5
|
+
registerWearableConnector,
|
|
6
|
+
getWearableConnector
|
|
7
|
+
} from "@remnic/core";
|
|
8
|
+
|
|
9
|
+
// src/client.ts
|
|
10
|
+
var LIMITLESS_DEFAULT_BASE_URL = "https://api.limitless.ai";
|
|
11
|
+
var LIFELOGS_MAX_PAGE_SIZE = 10;
|
|
12
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
13
|
+
var MAX_RETRIES = 3;
|
|
14
|
+
var MAX_RETRY_DELAY_MS = 3e4;
|
|
15
|
+
var LimitlessApiError = class extends Error {
|
|
16
|
+
constructor(message, status) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.name = "LimitlessApiError";
|
|
20
|
+
}
|
|
21
|
+
status;
|
|
22
|
+
};
|
|
23
|
+
var LimitlessClient = class {
|
|
24
|
+
apiKey;
|
|
25
|
+
baseUrl;
|
|
26
|
+
fetchImpl;
|
|
27
|
+
timeoutMs;
|
|
28
|
+
sleep;
|
|
29
|
+
constructor(options) {
|
|
30
|
+
if (typeof options.apiKey !== "string" || options.apiKey.trim().length === 0) {
|
|
31
|
+
throw new LimitlessApiError(
|
|
32
|
+
"Limitless API key is missing. Set wearables.sources.limitless.apiKey or the LIMITLESS_API_KEY environment variable (create a key under Developer settings in the Limitless app)."
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
this.apiKey = options.apiKey.trim();
|
|
36
|
+
this.baseUrl = stripTrailingSlashes(options.baseUrl ?? LIMITLESS_DEFAULT_BASE_URL);
|
|
37
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
38
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
39
|
+
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
40
|
+
}
|
|
41
|
+
/** One page of lifelogs for a single day. */
|
|
42
|
+
async listLifelogs(params) {
|
|
43
|
+
const search = new URLSearchParams({
|
|
44
|
+
date: params.date,
|
|
45
|
+
timezone: params.timezone,
|
|
46
|
+
limit: String(LIFELOGS_MAX_PAGE_SIZE),
|
|
47
|
+
direction: "asc",
|
|
48
|
+
// The markdown rendering duplicates what `contents` carries and
|
|
49
|
+
// inflates payloads; segments come from `contents`.
|
|
50
|
+
includeMarkdown: "false",
|
|
51
|
+
includeHeadings: "false",
|
|
52
|
+
includeContents: "true"
|
|
53
|
+
});
|
|
54
|
+
if (typeof params.cursor === "string" && params.cursor.length > 0) {
|
|
55
|
+
search.set("cursor", params.cursor);
|
|
56
|
+
}
|
|
57
|
+
const payload = await this.requestJson(
|
|
58
|
+
`/v1/lifelogs?${search.toString()}`,
|
|
59
|
+
params.signal
|
|
60
|
+
);
|
|
61
|
+
const data = payload.data;
|
|
62
|
+
const lifelogsRaw = data?.lifelogs;
|
|
63
|
+
if (!Array.isArray(lifelogsRaw)) {
|
|
64
|
+
throw new LimitlessApiError(
|
|
65
|
+
"Limitless API returned an unexpected /v1/lifelogs shape (missing data.lifelogs array)"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const meta = payload.meta;
|
|
69
|
+
const nextCursorRaw = meta?.lifelogs?.nextCursor;
|
|
70
|
+
return {
|
|
71
|
+
lifelogs: lifelogsRaw.filter(
|
|
72
|
+
(entry) => entry !== null && typeof entry === "object" && typeof entry.id === "string"
|
|
73
|
+
),
|
|
74
|
+
nextCursor: typeof nextCursorRaw === "string" && nextCursorRaw.length > 0 ? nextCursorRaw : null
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** Cheap auth probe. */
|
|
78
|
+
async verifyAuth(signal) {
|
|
79
|
+
try {
|
|
80
|
+
const search = new URLSearchParams({
|
|
81
|
+
limit: "1",
|
|
82
|
+
includeMarkdown: "false",
|
|
83
|
+
includeHeadings: "false"
|
|
84
|
+
});
|
|
85
|
+
await this.requestJson(`/v1/lifelogs?${search.toString()}`, signal);
|
|
86
|
+
return { ok: true };
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err instanceof LimitlessApiError && (err.status === 401 || err.status === 403)) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
detail: "Limitless rejected the API key (401/403) \u2014 create a new key under Developer settings"
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async requestJson(pathAndQuery, signal) {
|
|
101
|
+
let lastError;
|
|
102
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
103
|
+
signal?.throwIfAborted();
|
|
104
|
+
const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
|
|
105
|
+
const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
106
|
+
let response;
|
|
107
|
+
try {
|
|
108
|
+
response = await this.fetchImpl(`${this.baseUrl}${pathAndQuery}`, {
|
|
109
|
+
method: "GET",
|
|
110
|
+
headers: {
|
|
111
|
+
"X-API-Key": this.apiKey,
|
|
112
|
+
Accept: "application/json"
|
|
113
|
+
},
|
|
114
|
+
signal: combined
|
|
115
|
+
});
|
|
116
|
+
} catch (err) {
|
|
117
|
+
if (signal?.aborted) throw err;
|
|
118
|
+
lastError = err;
|
|
119
|
+
if (attempt < MAX_RETRIES) {
|
|
120
|
+
await this.sleep(backoffMs(attempt));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
throw new LimitlessApiError(
|
|
124
|
+
`Limitless API request failed after ${MAX_RETRIES + 1} attempts: ${describeNetworkError(err)}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (response.status === 429 || response.status >= 500) {
|
|
128
|
+
lastError = new LimitlessApiError(
|
|
129
|
+
`Limitless API responded ${response.status}`,
|
|
130
|
+
response.status
|
|
131
|
+
);
|
|
132
|
+
if (attempt < MAX_RETRIES) {
|
|
133
|
+
await this.sleep(await retryDelayMs(response, attempt));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
throw lastError;
|
|
137
|
+
}
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new LimitlessApiError(
|
|
140
|
+
`Limitless API responded ${response.status} for ${pathAndQuery.split("?")[0]}`,
|
|
141
|
+
response.status
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
return await response.json();
|
|
146
|
+
} catch {
|
|
147
|
+
throw new LimitlessApiError("Limitless API returned a non-JSON body");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw lastError instanceof Error ? lastError : new LimitlessApiError("Limitless API request failed");
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
function describeNetworkError(err) {
|
|
154
|
+
if (!(err instanceof Error)) return "unexpected non-Error failure";
|
|
155
|
+
const code = err.code;
|
|
156
|
+
return typeof code === "string" && code.length > 0 ? `${err.name} (${code})` : err.name;
|
|
157
|
+
}
|
|
158
|
+
function stripTrailingSlashes(value) {
|
|
159
|
+
let end = value.length;
|
|
160
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
|
|
161
|
+
return value.slice(0, end);
|
|
162
|
+
}
|
|
163
|
+
function backoffMs(attempt) {
|
|
164
|
+
return Math.min(MAX_RETRY_DELAY_MS, 1e3 * 2 ** attempt);
|
|
165
|
+
}
|
|
166
|
+
async function retryDelayMs(response, attempt) {
|
|
167
|
+
const headerValue = response.headers.get("retry-after");
|
|
168
|
+
let seconds;
|
|
169
|
+
if (headerValue !== null) {
|
|
170
|
+
const parsed = Number(headerValue);
|
|
171
|
+
if (Number.isFinite(parsed) && parsed > 0) seconds = parsed;
|
|
172
|
+
}
|
|
173
|
+
if (seconds === void 0) {
|
|
174
|
+
try {
|
|
175
|
+
const body = await response.clone().json();
|
|
176
|
+
const parsed = Number(body?.retryAfter);
|
|
177
|
+
if (Number.isFinite(parsed) && parsed > 0) seconds = parsed;
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (seconds !== void 0) {
|
|
182
|
+
return Math.min(MAX_RETRY_DELAY_MS, Math.ceil(seconds * 1e3));
|
|
183
|
+
}
|
|
184
|
+
return backoffMs(attempt);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/normalize.ts
|
|
188
|
+
var LIMITLESS_SOURCE_ID = "limitless";
|
|
189
|
+
var MAX_NODE_DEPTH = 16;
|
|
190
|
+
function lifelogToConversation(lifelog) {
|
|
191
|
+
const segments = [];
|
|
192
|
+
collectSegments(lifelog.contents ?? [], segments, 0);
|
|
193
|
+
return {
|
|
194
|
+
id: lifelog.id,
|
|
195
|
+
source: LIMITLESS_SOURCE_ID,
|
|
196
|
+
title: typeof lifelog.title === "string" ? lifelog.title : void 0,
|
|
197
|
+
startIso: lifelog.startTime ?? "",
|
|
198
|
+
endIso: lifelog.endTime,
|
|
199
|
+
segments
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function collectSegments(nodes, out, depth) {
|
|
203
|
+
if (depth > MAX_NODE_DEPTH) return;
|
|
204
|
+
for (const node of nodes) {
|
|
205
|
+
if (node === null || typeof node !== "object") continue;
|
|
206
|
+
if (node.type === "blockquote" && typeof node.content === "string") {
|
|
207
|
+
const text = node.content.trim();
|
|
208
|
+
if (text.length > 0) {
|
|
209
|
+
const isWearer = node.speakerIdentifier === "user";
|
|
210
|
+
const speakerName = typeof node.speakerName === "string" && node.speakerName.trim().length > 0 ? node.speakerName.trim() : void 0;
|
|
211
|
+
out.push({
|
|
212
|
+
text,
|
|
213
|
+
// The wearer's stable key is "user"; other speakers key by the
|
|
214
|
+
// provider name so saved names survive across lifelogs and the
|
|
215
|
+
// registry can re-label "Speaker 2" everywhere at once.
|
|
216
|
+
speakerKey: isWearer ? "user" : speakerName ?? "unknown",
|
|
217
|
+
...speakerName !== void 0 ? { speakerName } : {},
|
|
218
|
+
...isWearer ? { isWearer: true } : {},
|
|
219
|
+
...typeof node.startTime === "string" ? { startIso: node.startTime } : {},
|
|
220
|
+
...typeof node.endTime === "string" ? { endIso: node.endTime } : {}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
225
|
+
collectSegments(node.children, out, depth + 1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/index.ts
|
|
231
|
+
function resolveLimitlessApiKey(configuredKey, env = process.env) {
|
|
232
|
+
if (typeof configuredKey === "string" && configuredKey.trim().length > 0) {
|
|
233
|
+
return configuredKey.trim();
|
|
234
|
+
}
|
|
235
|
+
for (const name of ["REMNIC_LIMITLESS_API_KEY", "LIMITLESS_API_KEY"]) {
|
|
236
|
+
const value = env[name];
|
|
237
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
238
|
+
return value.trim();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
function createLimitlessConnector(options) {
|
|
244
|
+
let client = null;
|
|
245
|
+
const getClient = () => {
|
|
246
|
+
if (!client) {
|
|
247
|
+
client = new LimitlessClient({
|
|
248
|
+
apiKey: resolveLimitlessApiKey(options.settings.apiKey) ?? "",
|
|
249
|
+
baseUrl: options.settings.baseUrl
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return client;
|
|
253
|
+
};
|
|
254
|
+
return {
|
|
255
|
+
id: LIMITLESS_SOURCE_ID,
|
|
256
|
+
displayName: "Limitless Pendant",
|
|
257
|
+
async verifyAuth(signal) {
|
|
258
|
+
return getClient().verifyAuth(signal);
|
|
259
|
+
},
|
|
260
|
+
async fetchConversations(opts) {
|
|
261
|
+
const page = await getClient().listLifelogs({
|
|
262
|
+
date: opts.date,
|
|
263
|
+
timezone: opts.timezone,
|
|
264
|
+
cursor: opts.cursor,
|
|
265
|
+
signal: opts.signal
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
conversations: page.lifelogs.map(lifelogToConversation),
|
|
269
|
+
nextCursor: page.nextCursor
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
var wearableConnectorRegistration = {
|
|
275
|
+
id: LIMITLESS_SOURCE_ID,
|
|
276
|
+
displayName: "Limitless Pendant",
|
|
277
|
+
factory: createLimitlessConnector
|
|
278
|
+
};
|
|
279
|
+
function ensureLimitlessConnectorRegistered() {
|
|
280
|
+
if (getWearableConnector(LIMITLESS_SOURCE_ID) !== void 0) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
registerWearableConnector(wearableConnectorRegistration);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
ensureLimitlessConnectorRegistered();
|
|
287
|
+
export {
|
|
288
|
+
LIFELOGS_MAX_PAGE_SIZE,
|
|
289
|
+
LIMITLESS_DEFAULT_BASE_URL,
|
|
290
|
+
LIMITLESS_SOURCE_ID,
|
|
291
|
+
LimitlessApiError,
|
|
292
|
+
LimitlessClient,
|
|
293
|
+
createLimitlessConnector,
|
|
294
|
+
ensureLimitlessConnectorRegistered,
|
|
295
|
+
lifelogToConversation,
|
|
296
|
+
resolveLimitlessApiKey,
|
|
297
|
+
wearableConnectorRegistration
|
|
298
|
+
};
|
|
299
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/normalize.ts"],"sourcesContent":["/**\n * @remnic/connector-limitless — Limitless.ai Pendant connector.\n *\n * À-la-carte optional companion of @remnic/core: installing core alone\n * never pulls this in; core discovers it at runtime via a\n * computed-specifier dynamic import (see wearables/registry.ts) or via\n * a direct import of this module, which self-registers idempotently.\n *\n * API key: `wearables.sources.limitless.apiKey`, else the\n * `REMNIC_LIMITLESS_API_KEY` / `LIMITLESS_API_KEY` environment\n * variables (checked in that order).\n */\n\nimport {\n registerWearableConnector,\n getWearableConnector,\n type WearableConnectorFactoryOptions,\n type WearableConnectorRegistration,\n type WearableFetchOptions,\n type WearableFetchPage,\n type WearableSourceConnector,\n} from \"@remnic/core\";\n\nimport { LimitlessClient } from \"./client.js\";\nimport { lifelogToConversation, LIMITLESS_SOURCE_ID } from \"./normalize.js\";\n\nexport { LimitlessClient, LimitlessApiError, LIFELOGS_MAX_PAGE_SIZE, LIMITLESS_DEFAULT_BASE_URL } from \"./client.js\";\nexport type {\n LifelogsPage,\n LimitlessClientOptions,\n LimitlessContentNode,\n LimitlessLifelog,\n} from \"./client.js\";\nexport { lifelogToConversation, LIMITLESS_SOURCE_ID } from \"./normalize.js\";\n\nexport function resolveLimitlessApiKey(\n configuredKey: string | undefined,\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n if (typeof configuredKey === \"string\" && configuredKey.trim().length > 0) {\n return configuredKey.trim();\n }\n // REMNIC_* first, then the provider-conventional name.\n for (const name of [\"REMNIC_LIMITLESS_API_KEY\", \"LIMITLESS_API_KEY\"]) {\n const value = env[name];\n if (typeof value === \"string\" && value.trim().length > 0) {\n return value.trim();\n }\n }\n return undefined;\n}\n\nexport function createLimitlessConnector(\n options: WearableConnectorFactoryOptions,\n): WearableSourceConnector {\n // Key resolution happens lazily at call time (not factory time) so\n // `wearables status` works without credentials and env changes take\n // effect without a restart; the client constructor throws the\n // actionable missing-key message.\n let client: LimitlessClient | null = null;\n const getClient = (): LimitlessClient => {\n if (!client) {\n client = new LimitlessClient({\n apiKey: resolveLimitlessApiKey(options.settings.apiKey) ?? \"\",\n baseUrl: options.settings.baseUrl,\n });\n }\n return client;\n };\n\n return {\n id: LIMITLESS_SOURCE_ID,\n displayName: \"Limitless Pendant\",\n async verifyAuth(signal?: AbortSignal) {\n return getClient().verifyAuth(signal);\n },\n async fetchConversations(\n opts: WearableFetchOptions,\n ): Promise<WearableFetchPage> {\n const page = await getClient().listLifelogs({\n date: opts.date,\n timezone: opts.timezone,\n cursor: opts.cursor,\n signal: opts.signal,\n });\n return {\n conversations: page.lifelogs.map(lifelogToConversation),\n nextCursor: page.nextCursor,\n };\n },\n };\n}\n\nexport const wearableConnectorRegistration: WearableConnectorRegistration = {\n id: LIMITLESS_SOURCE_ID,\n displayName: \"Limitless Pendant\",\n factory: createLimitlessConnector,\n};\n\n/**\n * Idempotently register the connector with the core registry. Importing\n * this module registers it as a side effect; calling this again is safe\n * (returns false when already registered).\n */\nexport function ensureLimitlessConnectorRegistered(): boolean {\n if (getWearableConnector(LIMITLESS_SOURCE_ID) !== undefined) {\n return false;\n }\n registerWearableConnector(wearableConnectorRegistration);\n return true;\n}\n\nensureLimitlessConnectorRegistered();\n","/**\n * Minimal Limitless Developer API client (raw fetch, no SDK).\n *\n * Contract verified against https://www.limitless.ai/developers/docs/api\n * (2026-06): base https://api.limitless.ai, header `X-API-Key`,\n * `GET /v1/lifelogs` with cursor pagination at `meta.lifelogs.nextCursor`\n * and a hard per-page max of 10. Rate limit is 180 req/min; 429 bodies\n * carry `retryAfter` (a string, in seconds).\n *\n * The API key is never logged and never included in thrown error\n * messages.\n */\n\nexport const LIMITLESS_DEFAULT_BASE_URL = \"https://api.limitless.ai\";\n\n/** Hard API maximum for `limit` on /v1/lifelogs. */\nexport const LIFELOGS_MAX_PAGE_SIZE = 10;\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_RETRIES = 3;\n/** Backoff cap so a hostile retryAfter can't stall a sync for minutes. */\nconst MAX_RETRY_DELAY_MS = 30_000;\n\nexport interface LimitlessContentNode {\n type: string;\n content?: string;\n startTime?: string;\n endTime?: string;\n startOffsetMs?: number;\n endOffsetMs?: number;\n children?: LimitlessContentNode[];\n speakerName?: string | null;\n speakerIdentifier?: \"user\" | null;\n}\n\nexport interface LimitlessLifelog {\n id: string;\n title?: string;\n markdown?: string | null;\n startTime?: string;\n endTime?: string;\n isStarred?: boolean;\n updatedAt?: string;\n contents?: LimitlessContentNode[];\n}\n\nexport interface LifelogsPage {\n lifelogs: LimitlessLifelog[];\n nextCursor: string | null;\n}\n\nexport interface LimitlessClientOptions {\n apiKey: string;\n baseUrl?: string;\n /** Injectable for tests. Defaults to global fetch. */\n fetchImpl?: typeof fetch;\n /** Per-request timeout (ms). */\n timeoutMs?: number;\n /** Injectable sleep for tests. */\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport class LimitlessApiError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n ) {\n super(message);\n this.name = \"LimitlessApiError\";\n }\n}\n\nexport class LimitlessClient {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly fetchImpl: typeof fetch;\n private readonly timeoutMs: number;\n private readonly sleep: (ms: number) => Promise<void>;\n\n constructor(options: LimitlessClientOptions) {\n if (typeof options.apiKey !== \"string\" || options.apiKey.trim().length === 0) {\n throw new LimitlessApiError(\n \"Limitless API key is missing. Set wearables.sources.limitless.apiKey \" +\n \"or the LIMITLESS_API_KEY environment variable (create a key under \" +\n \"Developer settings in the Limitless app).\",\n );\n }\n this.apiKey = options.apiKey.trim();\n this.baseUrl = stripTrailingSlashes(options.baseUrl ?? LIMITLESS_DEFAULT_BASE_URL);\n this.fetchImpl = options.fetchImpl ?? fetch;\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.sleep =\n options.sleep ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));\n }\n\n /** One page of lifelogs for a single day. */\n async listLifelogs(params: {\n date: string;\n timezone: string;\n cursor?: string | null;\n signal?: AbortSignal;\n }): Promise<LifelogsPage> {\n const search = new URLSearchParams({\n date: params.date,\n timezone: params.timezone,\n limit: String(LIFELOGS_MAX_PAGE_SIZE),\n direction: \"asc\",\n // The markdown rendering duplicates what `contents` carries and\n // inflates payloads; segments come from `contents`.\n includeMarkdown: \"false\",\n includeHeadings: \"false\",\n includeContents: \"true\",\n });\n if (typeof params.cursor === \"string\" && params.cursor.length > 0) {\n search.set(\"cursor\", params.cursor);\n }\n const payload = await this.requestJson(\n `/v1/lifelogs?${search.toString()}`,\n params.signal,\n );\n const data = (payload as { data?: { lifelogs?: unknown } }).data;\n const lifelogsRaw = data?.lifelogs;\n if (!Array.isArray(lifelogsRaw)) {\n throw new LimitlessApiError(\n \"Limitless API returned an unexpected /v1/lifelogs shape (missing data.lifelogs array)\",\n );\n }\n const meta = (payload as { meta?: { lifelogs?: { nextCursor?: unknown } } }).meta;\n const nextCursorRaw = meta?.lifelogs?.nextCursor;\n return {\n lifelogs: lifelogsRaw.filter(\n (entry): entry is LimitlessLifelog =>\n entry !== null && typeof entry === \"object\" && typeof (entry as { id?: unknown }).id === \"string\",\n ),\n nextCursor:\n typeof nextCursorRaw === \"string\" && nextCursorRaw.length > 0\n ? nextCursorRaw\n : null,\n };\n }\n\n /** Cheap auth probe. */\n async verifyAuth(signal?: AbortSignal): Promise<{ ok: boolean; detail?: string }> {\n try {\n const search = new URLSearchParams({\n limit: \"1\",\n includeMarkdown: \"false\",\n includeHeadings: \"false\",\n });\n await this.requestJson(`/v1/lifelogs?${search.toString()}`, signal);\n return { ok: true };\n } catch (err) {\n if (err instanceof LimitlessApiError && (err.status === 401 || err.status === 403)) {\n return {\n ok: false,\n detail: \"Limitless rejected the API key (401/403) — create a new key under Developer settings\",\n };\n }\n return {\n ok: false,\n detail: err instanceof Error ? err.message : String(err),\n };\n }\n }\n\n private async requestJson(pathAndQuery: string, signal?: AbortSignal): Promise<unknown> {\n let lastError: unknown;\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n signal?.throwIfAborted();\n const timeoutSignal = AbortSignal.timeout(this.timeoutMs);\n const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;\n let response: Response;\n try {\n response = await this.fetchImpl(`${this.baseUrl}${pathAndQuery}`, {\n method: \"GET\",\n headers: {\n \"X-API-Key\": this.apiKey,\n Accept: \"application/json\",\n },\n signal: combined,\n });\n } catch (err) {\n // Network failure / timeout: retry unless the caller aborted.\n if (signal?.aborted) throw err;\n lastError = err;\n if (attempt < MAX_RETRIES) {\n await this.sleep(backoffMs(attempt));\n continue;\n }\n throw new LimitlessApiError(\n `Limitless API request failed after ${MAX_RETRIES + 1} attempts: ${describeNetworkError(err)}`,\n );\n }\n\n if (response.status === 429 || response.status >= 500) {\n lastError = new LimitlessApiError(\n `Limitless API responded ${response.status}`,\n response.status,\n );\n if (attempt < MAX_RETRIES) {\n await this.sleep(await retryDelayMs(response, attempt));\n continue;\n }\n throw lastError;\n }\n if (!response.ok) {\n throw new LimitlessApiError(\n `Limitless API responded ${response.status} for ${pathAndQuery.split(\"?\")[0]}`,\n response.status,\n );\n }\n try {\n return await response.json();\n } catch {\n throw new LimitlessApiError(\"Limitless API returned a non-JSON body\");\n }\n }\n // Unreachable: every loop path returns or throws. Keep the throw for\n // exhaustiveness.\n throw lastError instanceof Error\n ? lastError\n : new LimitlessApiError(\"Limitless API request failed\");\n }\n}\n\n/**\n * Network/timeout failures wrap Node error text that can carry loader\n * paths or stack fragments; sync errors reach MCP clients verbatim, so\n * only the error name + code survive (Cursor review on PR #1458).\n */\nfunction describeNetworkError(err: unknown): string {\n if (!(err instanceof Error)) return \"unexpected non-Error failure\";\n const code = (err as NodeJS.ErrnoException).code;\n return typeof code === \"string\" && code.length > 0 ? `${err.name} (${code})` : err.name;\n}\n\n/** Loop instead of `/\\/+$/` — CodeQL js/polynomial-redos on user-set URLs. */\nfunction stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 0x2f) end--;\n return value.slice(0, end);\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(MAX_RETRY_DELAY_MS, 1_000 * 2 ** attempt);\n}\n\nasync function retryDelayMs(response: Response, attempt: number): Promise<number> {\n // 429 bodies carry retryAfter as a STRING of seconds (per docs); a\n // Retry-After header may also appear. Either is honored, capped.\n const headerValue = response.headers.get(\"retry-after\");\n let seconds: number | undefined;\n if (headerValue !== null) {\n const parsed = Number(headerValue);\n if (Number.isFinite(parsed) && parsed > 0) seconds = parsed;\n }\n if (seconds === undefined) {\n try {\n const body = (await response.clone().json()) as { retryAfter?: unknown };\n const parsed = Number(body?.retryAfter);\n if (Number.isFinite(parsed) && parsed > 0) seconds = parsed;\n } catch {\n // Body unavailable or non-JSON — fall through to exponential backoff.\n }\n }\n if (seconds !== undefined) {\n return Math.min(MAX_RETRY_DELAY_MS, Math.ceil(seconds * 1_000));\n }\n return backoffMs(attempt);\n}\n","/**\n * Normalize Limitless lifelogs into Remnic's provider-agnostic\n * `WearableConversation` shape.\n *\n * Spoken dialogue lives in `blockquote` content nodes (possibly nested\n * under headings via `children`); heading nodes structure the lifelog\n * and are not utterances. The wearer is marked by\n * `speakerIdentifier === \"user\"`; everyone else carries a\n * `speakerName` (\"Speaker 2\" or a saved name).\n */\n\nimport type {\n WearableConversation,\n WearableTranscriptSegment,\n} from \"@remnic/core\";\n\nimport type { LimitlessContentNode, LimitlessLifelog } from \"./client.js\";\n\nexport const LIMITLESS_SOURCE_ID = \"limitless\";\n\n/** Guard against pathological nesting in provider payloads. */\nconst MAX_NODE_DEPTH = 16;\n\nexport function lifelogToConversation(\n lifelog: LimitlessLifelog,\n): WearableConversation {\n const segments: WearableTranscriptSegment[] = [];\n collectSegments(lifelog.contents ?? [], segments, 0);\n return {\n id: lifelog.id,\n source: LIMITLESS_SOURCE_ID,\n title: typeof lifelog.title === \"string\" ? lifelog.title : undefined,\n startIso: lifelog.startTime ?? \"\",\n endIso: lifelog.endTime,\n segments,\n };\n}\n\nfunction collectSegments(\n nodes: LimitlessContentNode[],\n out: WearableTranscriptSegment[],\n depth: number,\n): void {\n if (depth > MAX_NODE_DEPTH) return;\n for (const node of nodes) {\n if (node === null || typeof node !== \"object\") continue;\n if (node.type === \"blockquote\" && typeof node.content === \"string\") {\n const text = node.content.trim();\n if (text.length > 0) {\n const isWearer = node.speakerIdentifier === \"user\";\n const speakerName =\n typeof node.speakerName === \"string\" && node.speakerName.trim().length > 0\n ? node.speakerName.trim()\n : undefined;\n out.push({\n text,\n // The wearer's stable key is \"user\"; other speakers key by the\n // provider name so saved names survive across lifelogs and the\n // registry can re-label \"Speaker 2\" everywhere at once.\n speakerKey: isWearer ? \"user\" : (speakerName ?? \"unknown\"),\n ...(speakerName !== undefined ? { speakerName } : {}),\n ...(isWearer ? { isWearer: true } : {}),\n ...(typeof node.startTime === \"string\" ? { startIso: node.startTime } : {}),\n ...(typeof node.endTime === \"string\" ? { endIso: node.endTime } : {}),\n });\n }\n }\n if (Array.isArray(node.children) && node.children.length > 0) {\n collectSegments(node.children, out, depth + 1);\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@remnic/connector-limitless",
|
|
3
|
+
"version": "9.3.631",
|
|
4
|
+
"description": "Limitless.ai Pendant connector for Remnic — pull, clean, and remember wearable transcripts",
|
|
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
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@remnic/core": "^9.3.631"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsup": "^8.0.0",
|
|
25
|
+
"typescript": "^5.7.0",
|
|
26
|
+
"tsx": "^4.0.0",
|
|
27
|
+
"@remnic/core": "9.3.631"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/joshuaswarren/remnic.git",
|
|
33
|
+
"directory": "packages/connector-limitless"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"remnic",
|
|
37
|
+
"memory",
|
|
38
|
+
"limitless",
|
|
39
|
+
"pendant",
|
|
40
|
+
"wearable",
|
|
41
|
+
"transcript",
|
|
42
|
+
"lifelog"
|
|
43
|
+
],
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
46
|
+
"precheck-types": "node ../../scripts/ensure-bench-build-deps.mjs",
|
|
47
|
+
"check-types": "tsc --noEmit",
|
|
48
|
+
"test": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=remnic-source\" tsx --test src/client.test.ts src/normalize.test.ts src/index.test.ts"
|
|
49
|
+
}
|
|
50
|
+
}
|