@remnic/connector-omi 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 +86 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +409 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -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,86 @@
|
|
|
1
|
+
# @remnic/connector-omi
|
|
2
|
+
|
|
3
|
+
Omi AI wearable connector for [Remnic](https://github.com/joshuaswarren/remnic).
|
|
4
|
+
Pulls your Omi necklace conversations into Remnic's wearable-transcript
|
|
5
|
+
pipeline: cleaned, speaker-labeled, redacted, searchable day
|
|
6
|
+
transcripts — and, under per-source trust gates, memories. Optionally
|
|
7
|
+
imports Omi's own "memories" (extracted facts) into the review queue.
|
|
8
|
+
|
|
9
|
+
This is an **à-la-carte optional companion** of `@remnic/core`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @remnic/connector-omi
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Remnic discovers it at runtime. No further registration is needed.
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
The connector uses Omi's Integrations API, which is scoped to an app
|
|
20
|
+
you create:
|
|
21
|
+
|
|
22
|
+
1. In the Omi app: **Apps → Create App → External Integration**, and
|
|
23
|
+
grant the **read conversations** capability (plus **read memories**
|
|
24
|
+
if you want native-memory import). Install/enable the app for your
|
|
25
|
+
account.
|
|
26
|
+
2. On the app's management page, create an **API key** (`sk_...`).
|
|
27
|
+
3. Note your **app id** and your **uid** (Omi passes `?uid=` to your
|
|
28
|
+
app's links; it identifies the account to read).
|
|
29
|
+
4. Configure:
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
{
|
|
33
|
+
"wearables": {
|
|
34
|
+
"enabled": true,
|
|
35
|
+
"sources": {
|
|
36
|
+
"omi": {
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"appId": "your-omi-app-id",
|
|
39
|
+
"userId": "your-omi-uid",
|
|
40
|
+
"memoryMode": "smart", // smart (default) | off | review | auto
|
|
41
|
+
"importNativeMemories": "smart" // Omi memories through the same trust pipeline
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Provide the key via `OMI_API_KEY` (or `REMNIC_OMI_API_KEY`, or `apiKey`
|
|
49
|
+
in config).
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
remnic wearables check omi
|
|
55
|
+
remnic wearables sync --source omi --days 7
|
|
56
|
+
remnic wearables transcript --date 2026-06-10 --source omi
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Speaker labels
|
|
60
|
+
|
|
61
|
+
Omi marks the wearer (`is_user`) automatically. Other voices arrive as
|
|
62
|
+
`SPEAKER_NN` diarization labels — or stable person ids once you tag
|
|
63
|
+
people in Omi. Map either form to a display name once:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
remnic wearables speakers set omi SPEAKER_01 "Jane Doe"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Notes
|
|
70
|
+
|
|
71
|
+
- Transcripts are fetched **unabridged**: the connector passes
|
|
72
|
+
`max_transcript_segments=-1` because the API's default silently
|
|
73
|
+
truncates conversations to their first 100 segments.
|
|
74
|
+
- Day windows are timezone-correct: the connector computes local-day
|
|
75
|
+
ISO bounds (DST-aware) for the API's `start_date`/`end_date` filters,
|
|
76
|
+
and only `completed`, non-discarded conversations sync.
|
|
77
|
+
- Default `memoryMode: "smart"`: the LLM judge + per-source trust prior
|
|
78
|
+
+ cross-device corroboration write high-trust facts active, queue
|
|
79
|
+
borderline ones, and drop the rest. Omi-native memories run through
|
|
80
|
+
the same pipeline with a reduced prior.
|
|
81
|
+
|
|
82
|
+
Full documentation: [docs/wearables.md](https://github.com/joshuaswarren/remnic/blob/main/docs/wearables.md).
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { WearableConversation, WearableNativeMemory, WearableConnectorFactoryOptions, WearableSourceConnector, WearableConnectorRegistration } from '@remnic/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal Omi Integrations API client (raw fetch, no SDK).
|
|
5
|
+
*
|
|
6
|
+
* Contract verified against docs.omi.me and the open-source backend
|
|
7
|
+
* (`backend/routers/integration.py`, `models/integrations.py`) in
|
|
8
|
+
* 2026-06:
|
|
9
|
+
*
|
|
10
|
+
* - base `https://api.omi.me`, auth `Authorization: Bearer sk_...`
|
|
11
|
+
* (an app API key created in the Omi app; the app needs the
|
|
12
|
+
* External Integration `read_conversations` / `read_memories`
|
|
13
|
+
* capabilities and must be enabled for the target user)
|
|
14
|
+
* - `uid` rides as a query parameter on every endpoint
|
|
15
|
+
* - `GET /v2/integrations/{app_id}/conversations` with
|
|
16
|
+
* `limit`/`offset` pagination, `start_date`/`end_date` (ISO 8601),
|
|
17
|
+
* repeated `statuses` params, and `max_transcript_segments=-1`
|
|
18
|
+
* (the API default silently truncates to the first 100 segments)
|
|
19
|
+
* - `GET /v2/integrations/{app_id}/memories` with `limit`/`offset`
|
|
20
|
+
* - responses serialize with exclude_none — every optional field may
|
|
21
|
+
* be entirely absent
|
|
22
|
+
* - errors are FastAPI-shaped `{"detail": "..."}`
|
|
23
|
+
*
|
|
24
|
+
* The API key is never logged and never appears in thrown error
|
|
25
|
+
* messages.
|
|
26
|
+
*/
|
|
27
|
+
declare const OMI_DEFAULT_BASE_URL = "https://api.omi.me";
|
|
28
|
+
interface OmiTranscriptSegment {
|
|
29
|
+
text?: string;
|
|
30
|
+
speaker?: string;
|
|
31
|
+
is_user?: boolean;
|
|
32
|
+
person_id?: string | null;
|
|
33
|
+
start?: number;
|
|
34
|
+
end?: number;
|
|
35
|
+
}
|
|
36
|
+
interface OmiConversation {
|
|
37
|
+
id: string;
|
|
38
|
+
created_at?: string;
|
|
39
|
+
started_at?: string;
|
|
40
|
+
finished_at?: string;
|
|
41
|
+
structured?: {
|
|
42
|
+
title?: string;
|
|
43
|
+
overview?: string;
|
|
44
|
+
category?: string;
|
|
45
|
+
action_items?: Array<{
|
|
46
|
+
description?: string;
|
|
47
|
+
completed?: boolean;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
transcript_segments?: OmiTranscriptSegment[];
|
|
51
|
+
geolocation?: {
|
|
52
|
+
address?: string | null;
|
|
53
|
+
} | null;
|
|
54
|
+
status?: string;
|
|
55
|
+
discarded?: boolean;
|
|
56
|
+
}
|
|
57
|
+
interface OmiMemory {
|
|
58
|
+
id: string;
|
|
59
|
+
content?: string;
|
|
60
|
+
category?: string;
|
|
61
|
+
tags?: string[];
|
|
62
|
+
created_at?: string;
|
|
63
|
+
}
|
|
64
|
+
interface OmiConversationsPage {
|
|
65
|
+
conversations: OmiConversation[];
|
|
66
|
+
nextOffset: number | null;
|
|
67
|
+
}
|
|
68
|
+
interface OmiMemoriesPage {
|
|
69
|
+
memories: OmiMemory[];
|
|
70
|
+
nextOffset: number | null;
|
|
71
|
+
}
|
|
72
|
+
interface OmiClientOptions {
|
|
73
|
+
apiKey: string;
|
|
74
|
+
appId: string;
|
|
75
|
+
userId: string;
|
|
76
|
+
baseUrl?: string;
|
|
77
|
+
fetchImpl?: typeof fetch;
|
|
78
|
+
timeoutMs?: number;
|
|
79
|
+
sleep?: (ms: number) => Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
declare class OmiApiError extends Error {
|
|
82
|
+
readonly status?: number | undefined;
|
|
83
|
+
/** FastAPI `detail` string when the body carried one. */
|
|
84
|
+
readonly detail?: string | undefined;
|
|
85
|
+
constructor(message: string, status?: number | undefined,
|
|
86
|
+
/** FastAPI `detail` string when the body carried one. */
|
|
87
|
+
detail?: string | undefined);
|
|
88
|
+
}
|
|
89
|
+
declare class OmiClient {
|
|
90
|
+
private readonly apiKey;
|
|
91
|
+
private readonly appId;
|
|
92
|
+
private readonly userId;
|
|
93
|
+
private readonly baseUrl;
|
|
94
|
+
private readonly fetchImpl;
|
|
95
|
+
private readonly timeoutMs;
|
|
96
|
+
private readonly sleep;
|
|
97
|
+
constructor(options: OmiClientOptions);
|
|
98
|
+
/** One page of completed conversations inside [startIso, endIso). */
|
|
99
|
+
listConversations(params: {
|
|
100
|
+
startIso: string;
|
|
101
|
+
endIso: string;
|
|
102
|
+
offset?: number;
|
|
103
|
+
signal?: AbortSignal;
|
|
104
|
+
}): Promise<OmiConversationsPage>;
|
|
105
|
+
/** One page of Omi memories (provider-extracted facts). */
|
|
106
|
+
listMemories(params?: {
|
|
107
|
+
offset?: number;
|
|
108
|
+
signal?: AbortSignal;
|
|
109
|
+
}): Promise<OmiMemoriesPage>;
|
|
110
|
+
verifyAuth(signal?: AbortSignal): Promise<{
|
|
111
|
+
ok: boolean;
|
|
112
|
+
detail?: string;
|
|
113
|
+
}>;
|
|
114
|
+
private requestJson;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Normalize Omi conversations into Remnic's provider-agnostic
|
|
119
|
+
* `WearableConversation` shape, plus the timezone-aware day-window
|
|
120
|
+
* helpers the Omi API needs (its date filters are ISO datetimes).
|
|
121
|
+
*
|
|
122
|
+
* Omi segments carry `is_user` for the wearer, opaque `SPEAKER_NN`
|
|
123
|
+
* diarization labels, optional `person_id`s (user-defined people), and
|
|
124
|
+
* start/end offsets in seconds relative to the conversation start.
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
declare const OMI_SOURCE_ID = "omi";
|
|
128
|
+
/** "GMT+05:30" → "+05:30"; plain "GMT" → "+00:00". */
|
|
129
|
+
declare function timezoneOffsetIso(instant: Date, timezone: string): string;
|
|
130
|
+
/** ISO instant for local midnight of `date` in `timezone`. */
|
|
131
|
+
declare function zonedDayStartIso(date: string, timezone: string): string;
|
|
132
|
+
declare function nextIsoDate(date: string): string;
|
|
133
|
+
/** Half-open [start, end) ISO bounds of a local day. */
|
|
134
|
+
declare function zonedDayBounds(date: string, timezone: string): {
|
|
135
|
+
startIso: string;
|
|
136
|
+
endIso: string;
|
|
137
|
+
};
|
|
138
|
+
declare function conversationToWearable(conversation: OmiConversation): WearableConversation;
|
|
139
|
+
declare function memoryToNativeMemory(memory: OmiMemory): WearableNativeMemory | null;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @remnic/connector-omi — Omi AI wearable connector.
|
|
143
|
+
*
|
|
144
|
+
* À-la-carte optional companion of @remnic/core (computed-specifier
|
|
145
|
+
* discovery; importing this module self-registers idempotently).
|
|
146
|
+
*
|
|
147
|
+
* Requires an Omi integration app with the External Integration
|
|
148
|
+
* `read_conversations` (and, for native-memory import,
|
|
149
|
+
* `read_memories`) capabilities:
|
|
150
|
+
* - `wearables.sources.omi.appId` — the app id
|
|
151
|
+
* - `wearables.sources.omi.userId` — the target uid
|
|
152
|
+
* - key via `REMNIC_OMI_API_KEY` / `OMI_API_KEY` env (or `apiKey`)
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
declare function resolveOmiApiKey(configuredKey: string | undefined, env?: NodeJS.ProcessEnv): string | undefined;
|
|
156
|
+
declare function createOmiConnector(options: WearableConnectorFactoryOptions): WearableSourceConnector;
|
|
157
|
+
declare const wearableConnectorRegistration: WearableConnectorRegistration;
|
|
158
|
+
/**
|
|
159
|
+
* Idempotently register the connector with the core registry. Importing
|
|
160
|
+
* this module registers it as a side effect; calling this again is safe
|
|
161
|
+
* (returns false when already registered).
|
|
162
|
+
*/
|
|
163
|
+
declare function ensureOmiConnectorRegistered(): boolean;
|
|
164
|
+
|
|
165
|
+
export { OMI_DEFAULT_BASE_URL, OMI_SOURCE_ID, OmiApiError, OmiClient, type OmiClientOptions, type OmiConversation, type OmiConversationsPage, type OmiMemoriesPage, type OmiMemory, type OmiTranscriptSegment, conversationToWearable, createOmiConnector, ensureOmiConnectorRegistered, memoryToNativeMemory, nextIsoDate, resolveOmiApiKey, timezoneOffsetIso, wearableConnectorRegistration, zonedDayBounds, zonedDayStartIso };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
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 OMI_DEFAULT_BASE_URL = "https://api.omi.me";
|
|
11
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
12
|
+
var MAX_RETRIES = 3;
|
|
13
|
+
var MAX_RETRY_DELAY_MS = 3e4;
|
|
14
|
+
var PAGE_SIZE = 100;
|
|
15
|
+
var OmiApiError = class extends Error {
|
|
16
|
+
constructor(message, status, detail) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.detail = detail;
|
|
20
|
+
this.name = "OmiApiError";
|
|
21
|
+
}
|
|
22
|
+
status;
|
|
23
|
+
detail;
|
|
24
|
+
};
|
|
25
|
+
var OmiClient = class {
|
|
26
|
+
apiKey;
|
|
27
|
+
appId;
|
|
28
|
+
userId;
|
|
29
|
+
baseUrl;
|
|
30
|
+
fetchImpl;
|
|
31
|
+
timeoutMs;
|
|
32
|
+
sleep;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
if (typeof options.apiKey !== "string" || options.apiKey.trim().length === 0) {
|
|
35
|
+
throw new OmiApiError(
|
|
36
|
+
"Omi API key is missing. Set wearables.sources.omi.apiKey or the OMI_API_KEY environment variable (create a key under your app's API Keys in the Omi app)."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (typeof options.appId !== "string" || options.appId.trim().length === 0) {
|
|
40
|
+
throw new OmiApiError(
|
|
41
|
+
"Omi app id is missing. Set wearables.sources.omi.appId to your integration app's id."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (typeof options.userId !== "string" || options.userId.trim().length === 0) {
|
|
45
|
+
throw new OmiApiError(
|
|
46
|
+
"Omi user id is missing. Set wearables.sources.omi.userId to the target uid (shown when the user installs/opens your Omi app)."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
this.apiKey = options.apiKey.trim();
|
|
50
|
+
this.appId = options.appId.trim();
|
|
51
|
+
this.userId = options.userId.trim();
|
|
52
|
+
this.baseUrl = stripTrailingSlashes(options.baseUrl ?? OMI_DEFAULT_BASE_URL);
|
|
53
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
54
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
55
|
+
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
56
|
+
}
|
|
57
|
+
/** One page of completed conversations inside [startIso, endIso). */
|
|
58
|
+
async listConversations(params) {
|
|
59
|
+
const offset = params.offset ?? 0;
|
|
60
|
+
const search = new URLSearchParams({
|
|
61
|
+
uid: this.userId,
|
|
62
|
+
limit: String(PAGE_SIZE),
|
|
63
|
+
offset: String(offset),
|
|
64
|
+
start_date: params.startIso,
|
|
65
|
+
end_date: params.endIso,
|
|
66
|
+
include_discarded: "false",
|
|
67
|
+
// -1 = unlimited; the API default silently truncates transcripts
|
|
68
|
+
// to their first 100 segments.
|
|
69
|
+
max_transcript_segments: "-1"
|
|
70
|
+
});
|
|
71
|
+
search.append("statuses", "completed");
|
|
72
|
+
const payload = await this.requestJson(
|
|
73
|
+
`/v2/integrations/${encodeURIComponent(this.appId)}/conversations?${search.toString()}`,
|
|
74
|
+
params.signal
|
|
75
|
+
);
|
|
76
|
+
const conversations = payload.conversations;
|
|
77
|
+
if (!Array.isArray(conversations)) {
|
|
78
|
+
throw new OmiApiError(
|
|
79
|
+
"Omi API returned an unexpected conversations shape (missing conversations array)"
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const valid = conversations.filter(
|
|
83
|
+
(entry) => entry !== null && typeof entry === "object" && typeof entry.id === "string"
|
|
84
|
+
);
|
|
85
|
+
return {
|
|
86
|
+
conversations: valid,
|
|
87
|
+
nextOffset: conversations.length === PAGE_SIZE ? offset + PAGE_SIZE : null
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** One page of Omi memories (provider-extracted facts). */
|
|
91
|
+
async listMemories(params = {}) {
|
|
92
|
+
const offset = params.offset ?? 0;
|
|
93
|
+
const search = new URLSearchParams({
|
|
94
|
+
uid: this.userId,
|
|
95
|
+
limit: String(PAGE_SIZE),
|
|
96
|
+
offset: String(offset)
|
|
97
|
+
});
|
|
98
|
+
const payload = await this.requestJson(
|
|
99
|
+
`/v2/integrations/${encodeURIComponent(this.appId)}/memories?${search.toString()}`,
|
|
100
|
+
params.signal
|
|
101
|
+
);
|
|
102
|
+
const memories = payload.memories;
|
|
103
|
+
if (!Array.isArray(memories)) {
|
|
104
|
+
throw new OmiApiError(
|
|
105
|
+
"Omi API returned an unexpected memories shape (missing memories array)"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const valid = memories.filter(
|
|
109
|
+
(entry) => entry !== null && typeof entry === "object" && typeof entry.id === "string"
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
memories: valid,
|
|
113
|
+
nextOffset: memories.length === PAGE_SIZE ? offset + PAGE_SIZE : null
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async verifyAuth(signal) {
|
|
117
|
+
try {
|
|
118
|
+
const search = new URLSearchParams({
|
|
119
|
+
uid: this.userId,
|
|
120
|
+
limit: "1",
|
|
121
|
+
offset: "0",
|
|
122
|
+
max_transcript_segments: "1"
|
|
123
|
+
});
|
|
124
|
+
await this.requestJson(
|
|
125
|
+
`/v2/integrations/${encodeURIComponent(this.appId)}/conversations?${search.toString()}`,
|
|
126
|
+
signal
|
|
127
|
+
);
|
|
128
|
+
return { ok: true };
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (err instanceof OmiApiError && err.status !== void 0) {
|
|
131
|
+
const hint = err.status === 401 ? "missing/malformed Authorization header" : err.status === 403 ? "key rejected, app not enabled for this uid, or missing read_conversations capability" : err.status === 404 ? "app not found \u2014 check wearables.sources.omi.appId" : void 0;
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
detail: [err.detail, hint].filter(Boolean).join(" \u2014 ") || err.message
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
detail: err instanceof OmiApiError ? err.message : describeNetworkError(err)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async requestJson(pathAndQuery, signal) {
|
|
144
|
+
let lastError;
|
|
145
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
146
|
+
signal?.throwIfAborted();
|
|
147
|
+
const timeoutSignal = AbortSignal.timeout(this.timeoutMs);
|
|
148
|
+
const combined = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
149
|
+
let response;
|
|
150
|
+
try {
|
|
151
|
+
response = await this.fetchImpl(`${this.baseUrl}${pathAndQuery}`, {
|
|
152
|
+
method: "GET",
|
|
153
|
+
headers: {
|
|
154
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
155
|
+
Accept: "application/json"
|
|
156
|
+
},
|
|
157
|
+
signal: combined
|
|
158
|
+
});
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (signal?.aborted) throw err;
|
|
161
|
+
lastError = err;
|
|
162
|
+
if (attempt < MAX_RETRIES) {
|
|
163
|
+
await this.sleep(backoffMs(attempt));
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
throw new OmiApiError(
|
|
167
|
+
`Omi API request failed after ${MAX_RETRIES + 1} attempts: ${describeNetworkError(err)}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (response.status === 429 || response.status >= 500) {
|
|
171
|
+
lastError = new OmiApiError(
|
|
172
|
+
`Omi API responded ${response.status}`,
|
|
173
|
+
response.status,
|
|
174
|
+
await readDetail(response)
|
|
175
|
+
);
|
|
176
|
+
if (attempt < MAX_RETRIES) {
|
|
177
|
+
await this.sleep(retryDelayMs(response, attempt));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
throw lastError;
|
|
181
|
+
}
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
throw new OmiApiError(
|
|
184
|
+
`Omi API responded ${response.status} for ${pathAndQuery.split("?")[0]}`,
|
|
185
|
+
response.status,
|
|
186
|
+
await readDetail(response)
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
return await response.json();
|
|
191
|
+
} catch {
|
|
192
|
+
throw new OmiApiError("Omi API returned a non-JSON body");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
throw lastError instanceof Error ? lastError : new OmiApiError("Omi API request failed");
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
function describeNetworkError(err) {
|
|
199
|
+
if (!(err instanceof Error)) return "unexpected non-Error failure";
|
|
200
|
+
const code = err.code;
|
|
201
|
+
return typeof code === "string" && code.length > 0 ? `${err.name} (${code})` : err.name;
|
|
202
|
+
}
|
|
203
|
+
function stripTrailingSlashes(value) {
|
|
204
|
+
let end = value.length;
|
|
205
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
|
|
206
|
+
return value.slice(0, end);
|
|
207
|
+
}
|
|
208
|
+
async function readDetail(response) {
|
|
209
|
+
try {
|
|
210
|
+
const body = await response.clone().json();
|
|
211
|
+
return typeof body?.detail === "string" ? body.detail : void 0;
|
|
212
|
+
} catch {
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function backoffMs(attempt) {
|
|
217
|
+
return Math.min(MAX_RETRY_DELAY_MS, 1e3 * 2 ** attempt);
|
|
218
|
+
}
|
|
219
|
+
function retryDelayMs(response, attempt) {
|
|
220
|
+
const headerValue = response.headers.get("retry-after");
|
|
221
|
+
if (headerValue !== null) {
|
|
222
|
+
const parsed = Number(headerValue);
|
|
223
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
224
|
+
return Math.min(MAX_RETRY_DELAY_MS, Math.ceil(parsed * 1e3));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return backoffMs(attempt);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/normalize.ts
|
|
231
|
+
var OMI_SOURCE_ID = "omi";
|
|
232
|
+
function timezoneOffsetIso(instant, timezone) {
|
|
233
|
+
try {
|
|
234
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
235
|
+
timeZone: timezone,
|
|
236
|
+
timeZoneName: "longOffset"
|
|
237
|
+
}).formatToParts(instant);
|
|
238
|
+
const name = parts.find((part) => part.type === "timeZoneName")?.value ?? "GMT";
|
|
239
|
+
const match = name.match(/GMT([+-]\d{2}:\d{2})?/);
|
|
240
|
+
return match?.[1] ?? "+00:00";
|
|
241
|
+
} catch {
|
|
242
|
+
return "+00:00";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function zonedDayStartIso(date, timezone) {
|
|
246
|
+
let offset = timezoneOffsetIso(/* @__PURE__ */ new Date(`${date}T12:00:00Z`), timezone);
|
|
247
|
+
const candidate = /* @__PURE__ */ new Date(`${date}T00:00:00${offset}`);
|
|
248
|
+
const refined = timezoneOffsetIso(candidate, timezone);
|
|
249
|
+
if (refined !== offset) {
|
|
250
|
+
offset = refined;
|
|
251
|
+
}
|
|
252
|
+
return `${date}T00:00:00${offset}`;
|
|
253
|
+
}
|
|
254
|
+
function nextIsoDate(date) {
|
|
255
|
+
const parsed = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
256
|
+
parsed.setUTCDate(parsed.getUTCDate() + 1);
|
|
257
|
+
return parsed.toISOString().slice(0, 10);
|
|
258
|
+
}
|
|
259
|
+
function zonedDayBounds(date, timezone) {
|
|
260
|
+
return {
|
|
261
|
+
startIso: zonedDayStartIso(date, timezone),
|
|
262
|
+
endIso: zonedDayStartIso(nextIsoDate(date), timezone)
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function conversationToWearable(conversation) {
|
|
266
|
+
const startedAtMs = conversation.started_at ? Date.parse(conversation.started_at) : Number.NaN;
|
|
267
|
+
const segments = [];
|
|
268
|
+
for (const segment of conversation.transcript_segments ?? []) {
|
|
269
|
+
const text = typeof segment.text === "string" ? segment.text.trim() : "";
|
|
270
|
+
if (text.length === 0) continue;
|
|
271
|
+
const isWearer = segment.is_user === true;
|
|
272
|
+
const personId = typeof segment.person_id === "string" && segment.person_id.length > 0 ? segment.person_id : void 0;
|
|
273
|
+
const label = typeof segment.speaker === "string" && segment.speaker.trim().length > 0 ? segment.speaker.trim() : void 0;
|
|
274
|
+
const startIso = !Number.isNaN(startedAtMs) && typeof segment.start === "number" ? new Date(startedAtMs + segment.start * 1e3).toISOString() : void 0;
|
|
275
|
+
const endIso = !Number.isNaN(startedAtMs) && typeof segment.end === "number" ? new Date(startedAtMs + segment.end * 1e3).toISOString() : void 0;
|
|
276
|
+
segments.push({
|
|
277
|
+
text,
|
|
278
|
+
// person_id is the most stable key when the user has tagged the
|
|
279
|
+
// speaker as a known person in Omi; the diarization label
|
|
280
|
+
// otherwise.
|
|
281
|
+
speakerKey: isWearer ? "user" : personId ?? label ?? "unknown",
|
|
282
|
+
...label !== void 0 ? { speakerName: label } : {},
|
|
283
|
+
...isWearer ? { isWearer: true } : {},
|
|
284
|
+
...startIso !== void 0 ? { startIso } : {},
|
|
285
|
+
...endIso !== void 0 ? { endIso } : {}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const title = conversation.structured?.title?.trim();
|
|
289
|
+
const overview = conversation.structured?.overview?.trim();
|
|
290
|
+
const address = conversation.geolocation?.address;
|
|
291
|
+
return {
|
|
292
|
+
id: conversation.id,
|
|
293
|
+
source: OMI_SOURCE_ID,
|
|
294
|
+
...title && title.length > 0 ? { title } : {},
|
|
295
|
+
...overview && overview.length > 0 ? { summary: overview } : {},
|
|
296
|
+
startIso: conversation.started_at ?? conversation.created_at ?? "",
|
|
297
|
+
...conversation.finished_at !== void 0 ? { endIso: conversation.finished_at } : {},
|
|
298
|
+
...typeof address === "string" && address.length > 0 ? { location: address } : {},
|
|
299
|
+
segments
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function memoryToNativeMemory(memory) {
|
|
303
|
+
const content = typeof memory.content === "string" ? memory.content.trim() : "";
|
|
304
|
+
if (content.length === 0) return null;
|
|
305
|
+
return {
|
|
306
|
+
id: memory.id,
|
|
307
|
+
content,
|
|
308
|
+
...memory.created_at !== void 0 ? { createdIso: memory.created_at } : {},
|
|
309
|
+
...Array.isArray(memory.tags) && memory.tags.length > 0 ? { tags: memory.tags } : {}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/index.ts
|
|
314
|
+
function resolveOmiApiKey(configuredKey, env = process.env) {
|
|
315
|
+
if (typeof configuredKey === "string" && configuredKey.trim().length > 0) {
|
|
316
|
+
return configuredKey.trim();
|
|
317
|
+
}
|
|
318
|
+
for (const name of ["REMNIC_OMI_API_KEY", "OMI_API_KEY"]) {
|
|
319
|
+
const value = env[name];
|
|
320
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
321
|
+
return value.trim();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return void 0;
|
|
325
|
+
}
|
|
326
|
+
function parseOffsetCursor(cursor) {
|
|
327
|
+
if (typeof cursor !== "string" || cursor.length === 0) return 0;
|
|
328
|
+
const parsed = Number(cursor);
|
|
329
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
|
330
|
+
}
|
|
331
|
+
function createOmiConnector(options) {
|
|
332
|
+
let client = null;
|
|
333
|
+
const getClient = () => {
|
|
334
|
+
if (!client) {
|
|
335
|
+
client = new OmiClient({
|
|
336
|
+
apiKey: resolveOmiApiKey(options.settings.apiKey) ?? "",
|
|
337
|
+
appId: options.settings.appId ?? "",
|
|
338
|
+
userId: options.settings.userId ?? "",
|
|
339
|
+
baseUrl: options.settings.baseUrl
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
return client;
|
|
343
|
+
};
|
|
344
|
+
return {
|
|
345
|
+
id: OMI_SOURCE_ID,
|
|
346
|
+
displayName: "Omi",
|
|
347
|
+
async verifyAuth(signal) {
|
|
348
|
+
return getClient().verifyAuth(signal);
|
|
349
|
+
},
|
|
350
|
+
async fetchConversations(opts) {
|
|
351
|
+
const bounds = zonedDayBounds(opts.date, opts.timezone);
|
|
352
|
+
const page = await getClient().listConversations({
|
|
353
|
+
startIso: bounds.startIso,
|
|
354
|
+
endIso: bounds.endIso,
|
|
355
|
+
offset: parseOffsetCursor(opts.cursor),
|
|
356
|
+
signal: opts.signal
|
|
357
|
+
});
|
|
358
|
+
return {
|
|
359
|
+
conversations: page.conversations.filter((conversation) => conversation.discarded !== true).map(conversationToWearable),
|
|
360
|
+
nextCursor: page.nextOffset !== null ? String(page.nextOffset) : null
|
|
361
|
+
};
|
|
362
|
+
},
|
|
363
|
+
async fetchNativeMemories(opts) {
|
|
364
|
+
const page = await getClient().listMemories({
|
|
365
|
+
offset: parseOffsetCursor(opts.cursor),
|
|
366
|
+
signal: opts.signal
|
|
367
|
+
});
|
|
368
|
+
const memories = [];
|
|
369
|
+
for (const memory of page.memories) {
|
|
370
|
+
const mapped = memoryToNativeMemory(memory);
|
|
371
|
+
if (mapped !== null) memories.push(mapped);
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
memories,
|
|
375
|
+
nextCursor: page.nextOffset !== null ? String(page.nextOffset) : null
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
var wearableConnectorRegistration = {
|
|
381
|
+
id: OMI_SOURCE_ID,
|
|
382
|
+
displayName: "Omi",
|
|
383
|
+
factory: createOmiConnector
|
|
384
|
+
};
|
|
385
|
+
function ensureOmiConnectorRegistered() {
|
|
386
|
+
if (getWearableConnector(OMI_SOURCE_ID) !== void 0) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
registerWearableConnector(wearableConnectorRegistration);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
ensureOmiConnectorRegistered();
|
|
393
|
+
export {
|
|
394
|
+
OMI_DEFAULT_BASE_URL,
|
|
395
|
+
OMI_SOURCE_ID,
|
|
396
|
+
OmiApiError,
|
|
397
|
+
OmiClient,
|
|
398
|
+
conversationToWearable,
|
|
399
|
+
createOmiConnector,
|
|
400
|
+
ensureOmiConnectorRegistered,
|
|
401
|
+
memoryToNativeMemory,
|
|
402
|
+
nextIsoDate,
|
|
403
|
+
resolveOmiApiKey,
|
|
404
|
+
timezoneOffsetIso,
|
|
405
|
+
wearableConnectorRegistration,
|
|
406
|
+
zonedDayBounds,
|
|
407
|
+
zonedDayStartIso
|
|
408
|
+
};
|
|
409
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/normalize.ts"],"sourcesContent":["/**\n * @remnic/connector-omi — Omi AI wearable connector.\n *\n * À-la-carte optional companion of @remnic/core (computed-specifier\n * discovery; importing this module self-registers idempotently).\n *\n * Requires an Omi integration app with the External Integration\n * `read_conversations` (and, for native-memory import,\n * `read_memories`) capabilities:\n * - `wearables.sources.omi.appId` — the app id\n * - `wearables.sources.omi.userId` — the target uid\n * - key via `REMNIC_OMI_API_KEY` / `OMI_API_KEY` env (or `apiKey`)\n */\n\nimport {\n registerWearableConnector,\n getWearableConnector,\n type WearableConnectorFactoryOptions,\n type WearableConnectorRegistration,\n type WearableFetchOptions,\n type WearableFetchPage,\n type WearableNativeMemoryPage,\n type WearableSourceConnector,\n} from \"@remnic/core\";\n\nimport { OmiClient } from \"./client.js\";\nimport {\n conversationToWearable,\n memoryToNativeMemory,\n OMI_SOURCE_ID,\n zonedDayBounds,\n} from \"./normalize.js\";\n\nexport { OmiApiError, OmiClient, OMI_DEFAULT_BASE_URL } from \"./client.js\";\nexport type {\n OmiClientOptions,\n OmiConversation,\n OmiConversationsPage,\n OmiMemoriesPage,\n OmiMemory,\n OmiTranscriptSegment,\n} from \"./client.js\";\nexport {\n conversationToWearable,\n memoryToNativeMemory,\n nextIsoDate,\n OMI_SOURCE_ID,\n timezoneOffsetIso,\n zonedDayBounds,\n zonedDayStartIso,\n} from \"./normalize.js\";\n\nexport function resolveOmiApiKey(\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 for (const name of [\"REMNIC_OMI_API_KEY\", \"OMI_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\nfunction parseOffsetCursor(cursor: string | null | undefined): number {\n if (typeof cursor !== \"string\" || cursor.length === 0) return 0;\n const parsed = Number(cursor);\n return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;\n}\n\nexport function createOmiConnector(\n options: WearableConnectorFactoryOptions,\n): WearableSourceConnector {\n // Construction is lazy so `wearables status` works without\n // credentials; the client constructor throws the actionable\n // missing-credential messages at call time.\n let client: OmiClient | null = null;\n const getClient = (): OmiClient => {\n if (!client) {\n client = new OmiClient({\n apiKey: resolveOmiApiKey(options.settings.apiKey) ?? \"\",\n appId: options.settings.appId ?? \"\",\n userId: options.settings.userId ?? \"\",\n baseUrl: options.settings.baseUrl,\n });\n }\n return client;\n };\n\n return {\n id: OMI_SOURCE_ID,\n displayName: \"Omi\",\n async verifyAuth(signal?: AbortSignal) {\n return getClient().verifyAuth(signal);\n },\n async fetchConversations(opts: WearableFetchOptions): Promise<WearableFetchPage> {\n const bounds = zonedDayBounds(opts.date, opts.timezone);\n const page = await getClient().listConversations({\n startIso: bounds.startIso,\n endIso: bounds.endIso,\n offset: parseOffsetCursor(opts.cursor),\n signal: opts.signal,\n });\n return {\n conversations: page.conversations\n .filter((conversation) => conversation.discarded !== true)\n .map(conversationToWearable),\n nextCursor: page.nextOffset !== null ? String(page.nextOffset) : null,\n };\n },\n async fetchNativeMemories(opts: {\n cursor?: string | null;\n signal?: AbortSignal;\n }): Promise<WearableNativeMemoryPage> {\n const page = await getClient().listMemories({\n offset: parseOffsetCursor(opts.cursor),\n signal: opts.signal,\n });\n const memories = [];\n for (const memory of page.memories) {\n const mapped = memoryToNativeMemory(memory);\n if (mapped !== null) memories.push(mapped);\n }\n return {\n memories,\n nextCursor: page.nextOffset !== null ? String(page.nextOffset) : null,\n };\n },\n };\n}\n\nexport const wearableConnectorRegistration: WearableConnectorRegistration = {\n id: OMI_SOURCE_ID,\n displayName: \"Omi\",\n factory: createOmiConnector,\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 ensureOmiConnectorRegistered(): boolean {\n if (getWearableConnector(OMI_SOURCE_ID) !== undefined) {\n return false;\n }\n registerWearableConnector(wearableConnectorRegistration);\n return true;\n}\n\nensureOmiConnectorRegistered();\n","/**\n * Minimal Omi Integrations API client (raw fetch, no SDK).\n *\n * Contract verified against docs.omi.me and the open-source backend\n * (`backend/routers/integration.py`, `models/integrations.py`) in\n * 2026-06:\n *\n * - base `https://api.omi.me`, auth `Authorization: Bearer sk_...`\n * (an app API key created in the Omi app; the app needs the\n * External Integration `read_conversations` / `read_memories`\n * capabilities and must be enabled for the target user)\n * - `uid` rides as a query parameter on every endpoint\n * - `GET /v2/integrations/{app_id}/conversations` with\n * `limit`/`offset` pagination, `start_date`/`end_date` (ISO 8601),\n * repeated `statuses` params, and `max_transcript_segments=-1`\n * (the API default silently truncates to the first 100 segments)\n * - `GET /v2/integrations/{app_id}/memories` with `limit`/`offset`\n * - responses serialize with exclude_none — every optional field may\n * be entirely absent\n * - errors are FastAPI-shaped `{\"detail\": \"...\"}`\n *\n * The API key is never logged and never appears in thrown error\n * messages.\n */\n\nexport const OMI_DEFAULT_BASE_URL = \"https://api.omi.me\";\n\nconst DEFAULT_TIMEOUT_MS = 30_000;\nconst MAX_RETRIES = 3;\nconst MAX_RETRY_DELAY_MS = 30_000;\n/** Page size for both conversations and memories (API max is 1000). */\nconst PAGE_SIZE = 100;\n\nexport interface OmiTranscriptSegment {\n text?: string;\n speaker?: string;\n is_user?: boolean;\n person_id?: string | null;\n start?: number;\n end?: number;\n}\n\nexport interface OmiConversation {\n id: string;\n created_at?: string;\n started_at?: string;\n finished_at?: string;\n structured?: {\n title?: string;\n overview?: string;\n category?: string;\n action_items?: Array<{ description?: string; completed?: boolean }>;\n };\n transcript_segments?: OmiTranscriptSegment[];\n geolocation?: { address?: string | null } | null;\n status?: string;\n discarded?: boolean;\n}\n\nexport interface OmiMemory {\n id: string;\n content?: string;\n category?: string;\n tags?: string[];\n created_at?: string;\n}\n\nexport interface OmiConversationsPage {\n conversations: OmiConversation[];\n nextOffset: number | null;\n}\n\nexport interface OmiMemoriesPage {\n memories: OmiMemory[];\n nextOffset: number | null;\n}\n\nexport interface OmiClientOptions {\n apiKey: string;\n appId: string;\n userId: string;\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n timeoutMs?: number;\n sleep?: (ms: number) => Promise<void>;\n}\n\nexport class OmiApiError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n /** FastAPI `detail` string when the body carried one. */\n readonly detail?: string,\n ) {\n super(message);\n this.name = \"OmiApiError\";\n }\n}\n\nexport class OmiClient {\n private readonly apiKey: string;\n private readonly appId: string;\n private readonly userId: 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: OmiClientOptions) {\n if (typeof options.apiKey !== \"string\" || options.apiKey.trim().length === 0) {\n throw new OmiApiError(\n \"Omi API key is missing. Set wearables.sources.omi.apiKey or the \" +\n \"OMI_API_KEY environment variable (create a key under your app's \" +\n \"API Keys in the Omi app).\",\n );\n }\n if (typeof options.appId !== \"string\" || options.appId.trim().length === 0) {\n throw new OmiApiError(\n \"Omi app id is missing. Set wearables.sources.omi.appId to your \" +\n \"integration app's id.\",\n );\n }\n if (typeof options.userId !== \"string\" || options.userId.trim().length === 0) {\n throw new OmiApiError(\n \"Omi user id is missing. Set wearables.sources.omi.userId to the \" +\n \"target uid (shown when the user installs/opens your Omi app).\",\n );\n }\n this.apiKey = options.apiKey.trim();\n this.appId = options.appId.trim();\n this.userId = options.userId.trim();\n this.baseUrl = stripTrailingSlashes(options.baseUrl ?? OMI_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 completed conversations inside [startIso, endIso). */\n async listConversations(params: {\n startIso: string;\n endIso: string;\n offset?: number;\n signal?: AbortSignal;\n }): Promise<OmiConversationsPage> {\n const offset = params.offset ?? 0;\n const search = new URLSearchParams({\n uid: this.userId,\n limit: String(PAGE_SIZE),\n offset: String(offset),\n start_date: params.startIso,\n end_date: params.endIso,\n include_discarded: \"false\",\n // -1 = unlimited; the API default silently truncates transcripts\n // to their first 100 segments.\n max_transcript_segments: \"-1\",\n });\n // Repeated param (FastAPI List[str]) — comma-joining does NOT work.\n search.append(\"statuses\", \"completed\");\n const payload = await this.requestJson(\n `/v2/integrations/${encodeURIComponent(this.appId)}/conversations?${search.toString()}`,\n params.signal,\n );\n const conversations = (payload as { conversations?: unknown }).conversations;\n if (!Array.isArray(conversations)) {\n throw new OmiApiError(\n \"Omi API returned an unexpected conversations shape (missing conversations array)\",\n );\n }\n const valid = conversations.filter(\n (entry): entry is OmiConversation =>\n entry !== null &&\n typeof entry === \"object\" &&\n typeof (entry as { id?: unknown }).id === \"string\",\n );\n return {\n conversations: valid,\n nextOffset: conversations.length === PAGE_SIZE ? offset + PAGE_SIZE : null,\n };\n }\n\n /** One page of Omi memories (provider-extracted facts). */\n async listMemories(params: {\n offset?: number;\n signal?: AbortSignal;\n } = {}): Promise<OmiMemoriesPage> {\n const offset = params.offset ?? 0;\n const search = new URLSearchParams({\n uid: this.userId,\n limit: String(PAGE_SIZE),\n offset: String(offset),\n });\n const payload = await this.requestJson(\n `/v2/integrations/${encodeURIComponent(this.appId)}/memories?${search.toString()}`,\n params.signal,\n );\n const memories = (payload as { memories?: unknown }).memories;\n if (!Array.isArray(memories)) {\n throw new OmiApiError(\n \"Omi API returned an unexpected memories shape (missing memories array)\",\n );\n }\n const valid = memories.filter(\n (entry): entry is OmiMemory =>\n entry !== null &&\n typeof entry === \"object\" &&\n typeof (entry as { id?: unknown }).id === \"string\",\n );\n return {\n memories: valid,\n nextOffset: memories.length === PAGE_SIZE ? offset + PAGE_SIZE : null,\n };\n }\n\n async verifyAuth(signal?: AbortSignal): Promise<{ ok: boolean; detail?: string }> {\n try {\n const search = new URLSearchParams({\n uid: this.userId,\n limit: \"1\",\n offset: \"0\",\n max_transcript_segments: \"1\",\n });\n await this.requestJson(\n `/v2/integrations/${encodeURIComponent(this.appId)}/conversations?${search.toString()}`,\n signal,\n );\n return { ok: true };\n } catch (err) {\n if (err instanceof OmiApiError && err.status !== undefined) {\n // The backend's authorization chain yields distinct, actionable\n // detail strings: bad key (403), app not enabled for the user\n // (403), missing capability (403), app not found (404).\n const hint =\n err.status === 401\n ? \"missing/malformed Authorization header\"\n : err.status === 403\n ? \"key rejected, app not enabled for this uid, or missing read_conversations capability\"\n : err.status === 404\n ? \"app not found — check wearables.sources.omi.appId\"\n : undefined;\n return {\n ok: false,\n detail: [err.detail, hint].filter(Boolean).join(\" — \") || err.message,\n };\n }\n // OmiApiError messages are our own constructed strings (already\n // scrubbed — network text is reduced to name + code inside\n // requestJson); foreign errors reduce to name + errno code so raw\n // Node text (paths, loader stacks) never reaches operator\n // surfaces.\n return {\n ok: false,\n detail: err instanceof OmiApiError ? err.message : describeNetworkError(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 Authorization: `Bearer ${this.apiKey}`,\n Accept: \"application/json\",\n },\n signal: combined,\n });\n } catch (err) {\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 OmiApiError(\n `Omi API request failed after ${MAX_RETRIES + 1} attempts: ${describeNetworkError(err)}`,\n );\n }\n\n if (response.status === 429 || response.status >= 500) {\n lastError = new OmiApiError(\n `Omi API responded ${response.status}`,\n response.status,\n await readDetail(response),\n );\n if (attempt < MAX_RETRIES) {\n await this.sleep(retryDelayMs(response, attempt));\n continue;\n }\n throw lastError;\n }\n if (!response.ok) {\n throw new OmiApiError(\n `Omi API responded ${response.status} for ${pathAndQuery.split(\"?\")[0]}`,\n response.status,\n await readDetail(response),\n );\n }\n try {\n return await response.json();\n } catch {\n throw new OmiApiError(\"Omi API returned a non-JSON body\");\n }\n }\n throw lastError instanceof Error ? lastError : new OmiApiError(\"Omi 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.\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\nasync function readDetail(response: Response): Promise<string | undefined> {\n try {\n const body = (await response.clone().json()) as { detail?: unknown };\n return typeof body?.detail === \"string\" ? body.detail : undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction backoffMs(attempt: number): number {\n return Math.min(MAX_RETRY_DELAY_MS, 1_000 * 2 ** attempt);\n}\n\nfunction retryDelayMs(response: Response, attempt: number): number {\n const headerValue = response.headers.get(\"retry-after\");\n if (headerValue !== null) {\n const parsed = Number(headerValue);\n if (Number.isFinite(parsed) && parsed > 0) {\n return Math.min(MAX_RETRY_DELAY_MS, Math.ceil(parsed * 1_000));\n }\n }\n return backoffMs(attempt);\n}\n","/**\n * Normalize Omi conversations into Remnic's provider-agnostic\n * `WearableConversation` shape, plus the timezone-aware day-window\n * helpers the Omi API needs (its date filters are ISO datetimes).\n *\n * Omi segments carry `is_user` for the wearer, opaque `SPEAKER_NN`\n * diarization labels, optional `person_id`s (user-defined people), and\n * start/end offsets in seconds relative to the conversation start.\n */\n\nimport type {\n WearableConversation,\n WearableNativeMemory,\n WearableTranscriptSegment,\n} from \"@remnic/core\";\n\nimport type { OmiConversation, OmiMemory } from \"./client.js\";\n\nexport const OMI_SOURCE_ID = \"omi\";\n\n/** \"GMT+05:30\" → \"+05:30\"; plain \"GMT\" → \"+00:00\". */\nexport function timezoneOffsetIso(instant: Date, timezone: string): string {\n try {\n const parts = new Intl.DateTimeFormat(\"en-US\", {\n timeZone: timezone,\n timeZoneName: \"longOffset\",\n }).formatToParts(instant);\n const name = parts.find((part) => part.type === \"timeZoneName\")?.value ?? \"GMT\";\n const match = name.match(/GMT([+-]\\d{2}:\\d{2})?/);\n return match?.[1] ?? \"+00:00\";\n } catch {\n return \"+00:00\";\n }\n}\n\n/** ISO instant for local midnight of `date` in `timezone`. */\nexport function zonedDayStartIso(date: string, timezone: string): string {\n // Two-pass offset resolution: guess from midday (stable away from DST\n // transitions), then re-derive at the candidate midnight itself.\n let offset = timezoneOffsetIso(new Date(`${date}T12:00:00Z`), timezone);\n const candidate = new Date(`${date}T00:00:00${offset}`);\n const refined = timezoneOffsetIso(candidate, timezone);\n if (refined !== offset) {\n offset = refined;\n }\n return `${date}T00:00:00${offset}`;\n}\n\nexport function nextIsoDate(date: string): string {\n const parsed = new Date(`${date}T00:00:00Z`);\n parsed.setUTCDate(parsed.getUTCDate() + 1);\n return parsed.toISOString().slice(0, 10);\n}\n\n/** Half-open [start, end) ISO bounds of a local day. */\nexport function zonedDayBounds(\n date: string,\n timezone: string,\n): { startIso: string; endIso: string } {\n return {\n startIso: zonedDayStartIso(date, timezone),\n endIso: zonedDayStartIso(nextIsoDate(date), timezone),\n };\n}\n\nexport function conversationToWearable(\n conversation: OmiConversation,\n): WearableConversation {\n const startedAtMs = conversation.started_at\n ? Date.parse(conversation.started_at)\n : Number.NaN;\n const segments: WearableTranscriptSegment[] = [];\n for (const segment of conversation.transcript_segments ?? []) {\n const text = typeof segment.text === \"string\" ? segment.text.trim() : \"\";\n if (text.length === 0) continue;\n const isWearer = segment.is_user === true;\n const personId =\n typeof segment.person_id === \"string\" && segment.person_id.length > 0\n ? segment.person_id\n : undefined;\n const label =\n typeof segment.speaker === \"string\" && segment.speaker.trim().length > 0\n ? segment.speaker.trim()\n : undefined;\n const startIso =\n !Number.isNaN(startedAtMs) && typeof segment.start === \"number\"\n ? new Date(startedAtMs + segment.start * 1_000).toISOString()\n : undefined;\n const endIso =\n !Number.isNaN(startedAtMs) && typeof segment.end === \"number\"\n ? new Date(startedAtMs + segment.end * 1_000).toISOString()\n : undefined;\n segments.push({\n text,\n // person_id is the most stable key when the user has tagged the\n // speaker as a known person in Omi; the diarization label\n // otherwise.\n speakerKey: isWearer ? \"user\" : (personId ?? label ?? \"unknown\"),\n ...(label !== undefined ? { speakerName: label } : {}),\n ...(isWearer ? { isWearer: true } : {}),\n ...(startIso !== undefined ? { startIso } : {}),\n ...(endIso !== undefined ? { endIso } : {}),\n });\n }\n\n const title = conversation.structured?.title?.trim();\n const overview = conversation.structured?.overview?.trim();\n const address = conversation.geolocation?.address;\n\n return {\n id: conversation.id,\n source: OMI_SOURCE_ID,\n ...(title && title.length > 0 ? { title } : {}),\n ...(overview && overview.length > 0 ? { summary: overview } : {}),\n startIso: conversation.started_at ?? conversation.created_at ?? \"\",\n ...(conversation.finished_at !== undefined\n ? { endIso: conversation.finished_at }\n : {}),\n ...(typeof address === \"string\" && address.length > 0\n ? { location: address }\n : {}),\n segments,\n };\n}\n\nexport function memoryToNativeMemory(memory: OmiMemory): WearableNativeMemory | null {\n const content = typeof memory.content === \"string\" ? memory.content.trim() : \"\";\n if (content.length === 0) return null;\n return {\n id: memory.id,\n content,\n ...(memory.created_at !== undefined ? { createdIso: memory.created_at } : {}),\n ...(Array.isArray(memory.tags) && memory.tags.length > 0\n ? { tags: memory.tags }\n : {}),\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@remnic/connector-omi",
|
|
3
|
+
"version": "9.3.631",
|
|
4
|
+
"description": "Omi AI wearable connector for Remnic — pull, clean, and remember necklace transcripts via the Omi Integrations API",
|
|
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-omi"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"remnic",
|
|
37
|
+
"memory",
|
|
38
|
+
"omi",
|
|
39
|
+
"omi.me",
|
|
40
|
+
"wearable",
|
|
41
|
+
"transcript"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
45
|
+
"precheck-types": "node ../../scripts/ensure-bench-build-deps.mjs",
|
|
46
|
+
"check-types": "tsc --noEmit",
|
|
47
|
+
"test": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=remnic-source\" tsx --test src/client.test.ts src/normalize.test.ts src/index.test.ts"
|
|
48
|
+
}
|
|
49
|
+
}
|