@arkimedehq/recordare-client 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -7
- package/README_it.md +36 -9
- package/dist/client.d.ts +94 -4
- package/dist/client.js +116 -5
- package/dist/contract.d.ts +185 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/links.d.ts +10 -0
- package/dist/links.js +16 -0
- package/dist/mcp.js +1 -1
- package/dist/people.d.ts +5 -3
- package/dist/people.js +6 -2
- package/dist/tools.d.ts +4 -4
- package/dist/tools.js +4 -4
- package/package.json +1 -1
- package/src/client.ts +142 -8
- package/src/contract.ts +128 -0
- package/src/index.ts +1 -0
- package/src/links.ts +20 -0
- package/src/mcp.ts +1 -1
- package/src/people.ts +11 -5
- package/src/tools.ts +4 -4
package/dist/contract.d.ts
CHANGED
|
@@ -29,6 +29,19 @@ export interface Me {
|
|
|
29
29
|
via: string;
|
|
30
30
|
scopes: string[];
|
|
31
31
|
}
|
|
32
|
+
/** `DELETE api/v1/me`: what erasing the whole memory took, by kind. */
|
|
33
|
+
export interface Erased {
|
|
34
|
+
erased: {
|
|
35
|
+
conversations: number;
|
|
36
|
+
messages: number;
|
|
37
|
+
episodes: number;
|
|
38
|
+
facts: number;
|
|
39
|
+
notes: number;
|
|
40
|
+
digests: number;
|
|
41
|
+
sources: number;
|
|
42
|
+
contacts: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
32
45
|
/** `PATCH api/v1/me`: the person's settings from their platform. */
|
|
33
46
|
export interface MeSettings {
|
|
34
47
|
/** Follows the platform's profile: send it again whenever the user renames themselves. */
|
|
@@ -66,8 +79,33 @@ export interface IngestConversation {
|
|
|
66
79
|
source?: ConversationSource;
|
|
67
80
|
channel?: string;
|
|
68
81
|
title?: string;
|
|
82
|
+
/** A one-to-one chat (true) or a group (false), when the platform knows: a link code is never accepted in a group (D51). */
|
|
83
|
+
direct?: boolean;
|
|
69
84
|
participants?: IngestParticipant[];
|
|
70
85
|
}
|
|
86
|
+
/** A one-time code the holder sends from a channel to link that identity to themselves (D51; ten minutes, one use). */
|
|
87
|
+
export interface LinkCode {
|
|
88
|
+
code: string;
|
|
89
|
+
expiresAt: string;
|
|
90
|
+
}
|
|
91
|
+
/** The identities that speak as the memory's "I", and the claims waiting for the holder's confirmation (D51). */
|
|
92
|
+
export interface HolderIdentities {
|
|
93
|
+
linked: Array<{
|
|
94
|
+
id: string;
|
|
95
|
+
channel: string | null;
|
|
96
|
+
client: string | null;
|
|
97
|
+
externalId: string;
|
|
98
|
+
linkedAt: string;
|
|
99
|
+
}>;
|
|
100
|
+
pending: Array<{
|
|
101
|
+
id: string;
|
|
102
|
+
channel: string | null;
|
|
103
|
+
client: string | null;
|
|
104
|
+
externalId: string;
|
|
105
|
+
displayName: string | null;
|
|
106
|
+
claimedAt: string;
|
|
107
|
+
}>;
|
|
108
|
+
}
|
|
71
109
|
export interface IngestMessage {
|
|
72
110
|
/** The platform's own stable id: Recordare deduplicates on it, so a resend never duplicates. */
|
|
73
111
|
externalId: string;
|
|
@@ -99,6 +137,52 @@ export interface IngestResult {
|
|
|
99
137
|
duplicates: number;
|
|
100
138
|
conflicts: string[];
|
|
101
139
|
}
|
|
140
|
+
/** One voice of a room transcript (continuous listening, `ingestRoom`). */
|
|
141
|
+
export interface RoomSpeaker {
|
|
142
|
+
/** The transcript-local reference messages use in `speakerRef`. */
|
|
143
|
+
ref: string;
|
|
144
|
+
displayName?: string;
|
|
145
|
+
/** The platform's user id of a speaker with an account: in their own memory their words are theirs. */
|
|
146
|
+
user?: string;
|
|
147
|
+
/** Otherwise (or as well) a channel identity, e.g. a voice profile: a contact in every recipient's memory. */
|
|
148
|
+
identity?: ParticipantIdentity;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* What a home device heard in a room (WORK_PLAN 6.5 (4)), fanned out by Recordare to the memories of `recipients` (the
|
|
152
|
+
* platform's user ids of the people present whose memory is on — the platform's switch, D50): each gets it as an
|
|
153
|
+
* `ambient` conversation with the same external id, so edits and deletions go through the per-user calls.
|
|
154
|
+
*/
|
|
155
|
+
export interface RoomRequest {
|
|
156
|
+
conversation: {
|
|
157
|
+
externalId: string;
|
|
158
|
+
channel?: string;
|
|
159
|
+
title?: string;
|
|
160
|
+
};
|
|
161
|
+
speakers: RoomSpeaker[];
|
|
162
|
+
recipients: string[];
|
|
163
|
+
messages: Array<{
|
|
164
|
+
externalId: string;
|
|
165
|
+
/** Missing when the device could not tell who spoke: "someone" in every memory. */
|
|
166
|
+
speakerRef?: string;
|
|
167
|
+
content: string;
|
|
168
|
+
/** ISO 8601 with offset. */
|
|
169
|
+
sentAt: string;
|
|
170
|
+
upsert?: boolean;
|
|
171
|
+
}>;
|
|
172
|
+
hints?: {
|
|
173
|
+
conversationEnded?: boolean;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export interface RoomResult {
|
|
177
|
+
/** One per recipient, in the request's order; `conversationId` is that memory's conversation. */
|
|
178
|
+
recipients: Array<{
|
|
179
|
+
user: string;
|
|
180
|
+
conversationId: string | null;
|
|
181
|
+
accepted: number;
|
|
182
|
+
duplicates: number;
|
|
183
|
+
conflicts: string[];
|
|
184
|
+
}>;
|
|
185
|
+
}
|
|
102
186
|
export type PlanStatus = 'open' | 'confirmed' | 'cancelled' | 'rescheduled' | 'unresolved';
|
|
103
187
|
export type Precision = 'minute' | 'day' | 'month' | 'year' | 'approximate' | 'unknown';
|
|
104
188
|
export interface Episode {
|
|
@@ -193,10 +277,81 @@ export interface Note {
|
|
|
193
277
|
pinned: boolean;
|
|
194
278
|
pending: boolean;
|
|
195
279
|
inferred: boolean;
|
|
280
|
+
/** A recurring pattern the memory drew from its episodes (inferred, never pending; WORK_PLAN 5.4). */
|
|
281
|
+
pattern: boolean;
|
|
196
282
|
authorRole: string;
|
|
197
283
|
supportCount: number;
|
|
198
284
|
recordedAt: string;
|
|
199
285
|
}
|
|
286
|
+
/** The messages behind a fact or a note; text only from the client's own conversations (`otherClient` otherwise). */
|
|
287
|
+
export type ItemEvidence = Array<{
|
|
288
|
+
messageId?: string;
|
|
289
|
+
conversation?: string;
|
|
290
|
+
role: string;
|
|
291
|
+
author?: string | null;
|
|
292
|
+
sentAt: string;
|
|
293
|
+
text?: string;
|
|
294
|
+
otherClient?: true;
|
|
295
|
+
}>;
|
|
296
|
+
export interface FactDetail extends Fact {
|
|
297
|
+
authorRole: string;
|
|
298
|
+
/** The fact this one corrected (a correction by the person). */
|
|
299
|
+
corrects: string | null;
|
|
300
|
+
recordedAt: string;
|
|
301
|
+
evidence: ItemEvidence;
|
|
302
|
+
/** Episodes the fact was drawn from. */
|
|
303
|
+
episodeIds: string[];
|
|
304
|
+
}
|
|
305
|
+
export interface NoteDetail extends Note {
|
|
306
|
+
status: 'current' | 'superseded' | 'corrected';
|
|
307
|
+
supersedes: string | null;
|
|
308
|
+
corrects: string | null;
|
|
309
|
+
evidence: ItemEvidence;
|
|
310
|
+
episodeIds: string[];
|
|
311
|
+
}
|
|
312
|
+
/** One entry of the notes change feed; `note` is the note as it is now, null once forgotten. */
|
|
313
|
+
export interface NoteChange {
|
|
314
|
+
seq: number;
|
|
315
|
+
noteId: string;
|
|
316
|
+
change: 'created' | 'updated' | 'corrected' | 'confirmed' | 'forgotten' | 'retired';
|
|
317
|
+
at: string;
|
|
318
|
+
/** For `updated` / `corrected`: the note this one took the place of (a copy of it should follow), else null. */
|
|
319
|
+
replaces: string | null;
|
|
320
|
+
note: (Note & {
|
|
321
|
+
status: string;
|
|
322
|
+
}) | null;
|
|
323
|
+
}
|
|
324
|
+
export interface MemorySettings {
|
|
325
|
+
locale: string;
|
|
326
|
+
timezone: string;
|
|
327
|
+
/** null: the installation default (`effectiveQualityProfile`). */
|
|
328
|
+
qualityProfile: 'economy' | 'balanced' | 'full' | null;
|
|
329
|
+
effectiveQualityProfile: 'economy' | 'balanced' | 'full';
|
|
330
|
+
}
|
|
331
|
+
export interface UsageTotals {
|
|
332
|
+
calls: number;
|
|
333
|
+
inputTokens: number;
|
|
334
|
+
cachedInputTokens: number;
|
|
335
|
+
outputTokens: number;
|
|
336
|
+
failed: number;
|
|
337
|
+
}
|
|
338
|
+
export interface Usage extends UsageTotals {
|
|
339
|
+
from: string;
|
|
340
|
+
to: string;
|
|
341
|
+
byPrompt: Array<UsageTotals & {
|
|
342
|
+
prompt: string;
|
|
343
|
+
}>;
|
|
344
|
+
byDay: Array<UsageTotals & {
|
|
345
|
+
day: string;
|
|
346
|
+
}>;
|
|
347
|
+
}
|
|
348
|
+
export interface ExportJob {
|
|
349
|
+
id: string;
|
|
350
|
+
status: 'running' | 'done' | 'failed';
|
|
351
|
+
createdAt: string;
|
|
352
|
+
finishedAt: string | null;
|
|
353
|
+
bytes: number | null;
|
|
354
|
+
}
|
|
200
355
|
/** Size of the parts `learnSource` sends a big text in (UTF-8 bytes; the service's default request limit is 16 MB). */
|
|
201
356
|
export declare const SOURCE_PART_BYTES: number;
|
|
202
357
|
export type SourceKind = 'document' | 'page' | 'note' | 'book' | 'own_text';
|
|
@@ -250,6 +405,36 @@ export interface Source {
|
|
|
250
405
|
episodeIds: string[];
|
|
251
406
|
}
|
|
252
407
|
/** `POST api/v1/context`: the memories relevant to the message a host is about to answer (WORK_PLAN 5.7). */
|
|
408
|
+
/** `POST api/v1/context` options. */
|
|
409
|
+
export interface ContextOptions {
|
|
410
|
+
/** Open the block with the self card — who I am: current facts and notes about me. Off by default. */
|
|
411
|
+
card?: boolean;
|
|
412
|
+
}
|
|
413
|
+
/** `GET api/v1/cards`: a person as the memory knows them now (WORK_PLAN 8.13). */
|
|
414
|
+
export interface Card {
|
|
415
|
+
personId: string;
|
|
416
|
+
name: string;
|
|
417
|
+
aliases: string[];
|
|
418
|
+
facts: Array<{
|
|
419
|
+
id: string;
|
|
420
|
+
key: string;
|
|
421
|
+
value: string | null;
|
|
422
|
+
validFrom: string | null;
|
|
423
|
+
}>;
|
|
424
|
+
notes: Array<{
|
|
425
|
+
id: string;
|
|
426
|
+
category: string;
|
|
427
|
+
content: string;
|
|
428
|
+
pinned: boolean;
|
|
429
|
+
pattern: boolean;
|
|
430
|
+
}>;
|
|
431
|
+
/** The day of the latest episode about this contact (null for the self). */
|
|
432
|
+
lastMentioned: string | null;
|
|
433
|
+
}
|
|
434
|
+
export interface Cards {
|
|
435
|
+
self: Card;
|
|
436
|
+
contacts: Card[];
|
|
437
|
+
}
|
|
253
438
|
export interface MemoryContext {
|
|
254
439
|
/** A fenced `<memory-context>` block to append to the prompt, or null (nothing relevant, or off). */
|
|
255
440
|
block: string | null;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,5 @@ export { RecordareClient } from './client.js';
|
|
|
6
6
|
export { RecordareMcp, type McpOptions, type Tool, type ToolResult } from './mcp.js';
|
|
7
7
|
export { PersonDirectory, type Person, type PersonDirectoryOptions } from './people.js';
|
|
8
8
|
export { TOOLS } from './tools.js';
|
|
9
|
+
export { isLinkCode, LINK_CODE, LINK_CODE_INSTRUCTION } from './links.js';
|
|
9
10
|
export { afterFailure, backoffMs, DEFAULT_DELIVERY, type DeliveryPolicy, type Next } from './delivery.js';
|
package/dist/index.js
CHANGED
|
@@ -8,4 +8,5 @@ export { RecordareClient } from './client.js';
|
|
|
8
8
|
export { RecordareMcp } from './mcp.js';
|
|
9
9
|
export { PersonDirectory } from './people.js';
|
|
10
10
|
export { TOOLS } from './tools.js';
|
|
11
|
+
export { isLinkCode, LINK_CODE, LINK_CODE_INSTRUCTION } from './links.js';
|
|
11
12
|
export { afterFailure, backoffMs, DEFAULT_DELIVERY } from './delivery.js';
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D51 — linking the holder's channel identity by one-time code. The holder makes a code (`RecordareClient.linkCode`),
|
|
3
|
+
* sends it to the agent from the channel, then confirms the claim (`holderIdentities` / `confirmHolderClaim`). Recordare
|
|
4
|
+
* never stores the code's message; a host that sees the message before its model can tell the model what it is.
|
|
5
|
+
*/
|
|
6
|
+
/** A message that is only a link code (dashes and a final dot optional, any case) — the service's own pattern. */
|
|
7
|
+
export declare const LINK_CODE: RegExp;
|
|
8
|
+
export declare function isLinkCode(text: string): boolean;
|
|
9
|
+
/** What a host adds for its model when the user's message is a link code (the model would otherwise try to answer it). */
|
|
10
|
+
export declare const LINK_CODE_INSTRUCTION: string;
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright © 2026 Andrea Genovese
|
|
3
|
+
/**
|
|
4
|
+
* D51 — linking the holder's channel identity by one-time code. The holder makes a code (`RecordareClient.linkCode`),
|
|
5
|
+
* sends it to the agent from the channel, then confirms the claim (`holderIdentities` / `confirmHolderClaim`). Recordare
|
|
6
|
+
* never stores the code's message; a host that sees the message before its model can tell the model what it is.
|
|
7
|
+
*/
|
|
8
|
+
/** A message that is only a link code (dashes and a final dot optional, any case) — the service's own pattern. */
|
|
9
|
+
export const LINK_CODE = /^\s*RCD-?[0-9A-Z]{4}-?[0-9A-Z]{4}\.?\s*$/i;
|
|
10
|
+
export function isLinkCode(text) {
|
|
11
|
+
return LINK_CODE.test(text);
|
|
12
|
+
}
|
|
13
|
+
/** What a host adds for its model when the user's message is a link code (the model would otherwise try to answer it). */
|
|
14
|
+
export const LINK_CODE_INSTRUCTION = 'The user\'s last message is a one-time code that links their account on this channel to their '
|
|
15
|
+
+ 'memory (Recordare); it is handled automatically. Reply only, briefly and in the user\'s language, that the code was '
|
|
16
|
+
+ 'received and that they now confirm the link where they created the code. Do not repeat the code.';
|
package/dist/mcp.js
CHANGED
|
@@ -80,7 +80,7 @@ export class RecordareMcp {
|
|
|
80
80
|
},
|
|
81
81
|
requestInit: { headers: { ...this.http.headers(user), [CONVERSATION_HEADER]: conversation } },
|
|
82
82
|
});
|
|
83
|
-
const client = new Client(this.options.clientInfo ?? { name: 'recordare-client', version: '0.
|
|
83
|
+
const client = new Client(this.options.clientInfo ?? { name: 'recordare-client', version: '0.4.0' });
|
|
84
84
|
await client.connect(transport);
|
|
85
85
|
return client;
|
|
86
86
|
}
|
package/dist/people.d.ts
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Who each of the platform's users is in Recordare, cached: the memory (memoryId), its mode and the Atlas
|
|
3
3
|
* address. Recordare has no consent flag (D50): the platform's own switch (`enabled`) decides whether it is contacted.
|
|
4
4
|
* The synchronous readers never wait on Recordare: they answer from the cache (even if stale) and refresh in the
|
|
5
|
-
* background. Each lookup also keeps the person's name in sync with the platform's profile.
|
|
5
|
+
* background. Each lookup also keeps the person's name and gender in sync with the platform's profile.
|
|
6
6
|
*/
|
|
7
|
-
import { type MemoryMode } from './contract.js';
|
|
7
|
+
import { type MemoryGender, type MemoryMode } from './contract.js';
|
|
8
8
|
import { type RecordareClient } from './client.js';
|
|
9
9
|
export interface Person {
|
|
10
10
|
/** null = not known yet (not opted in on the platform, or Recordare unreachable before the first lookup). */
|
|
@@ -18,6 +18,8 @@ export interface PlatformUser {
|
|
|
18
18
|
enabled: boolean;
|
|
19
19
|
/** The current profile name: the person's name in Recordare follows it. */
|
|
20
20
|
name?: string | null;
|
|
21
|
+
/** The profile's gender: the first person's gender in gendered languages follows it (not set = left as it is). */
|
|
22
|
+
gender?: MemoryGender | null;
|
|
21
23
|
}
|
|
22
24
|
export interface PersonDirectoryOptions {
|
|
23
25
|
/** How long a lookup is trusted; default 5 minutes. */
|
|
@@ -26,7 +28,7 @@ export interface PersonDirectoryOptions {
|
|
|
26
28
|
user: (user: string) => Promise<PlatformUser | null>;
|
|
27
29
|
/** A person resolved (e.g. to store the memoryId with the platform's user). */
|
|
28
30
|
onResolved?: (user: string, person: Person) => void | Promise<void>;
|
|
29
|
-
/** Called when a lookup or a
|
|
31
|
+
/** Called when a lookup or a settings update fails (the cache keeps the last known person). */
|
|
30
32
|
onError?: (user: string, err: unknown) => void;
|
|
31
33
|
}
|
|
32
34
|
export declare class PersonDirectory {
|
package/dist/people.js
CHANGED
|
@@ -51,8 +51,12 @@ export class PersonDirectory {
|
|
|
51
51
|
return remember({ memoryId: last?.memoryId ?? null, mode: last?.mode ?? null, atlasUrl: last?.atlasUrl ?? null });
|
|
52
52
|
const me = await this.client.me(user);
|
|
53
53
|
const name = platform.name?.trim().slice(0, 100);
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
const changed = {
|
|
55
|
+
...(name && me.displayName !== name ? { displayName: name } : {}),
|
|
56
|
+
...(platform.gender && me.gender !== platform.gender ? { gender: platform.gender } : {}),
|
|
57
|
+
};
|
|
58
|
+
if (Object.keys(changed).length) {
|
|
59
|
+
await this.client.updateMe(user, changed).catch((err) => this.options.onError?.(user, err));
|
|
56
60
|
}
|
|
57
61
|
const person = { memoryId: me.memoryId, mode: me.mode, atlasUrl: me.atlasUrl ?? null };
|
|
58
62
|
await this.options.onResolved?.(user, person);
|
package/dist/tools.d.ts
CHANGED
|
@@ -76,7 +76,7 @@ export declare const TOOLS: readonly [{
|
|
|
76
76
|
}, {
|
|
77
77
|
readonly name: "log_episode";
|
|
78
78
|
readonly title: "Note something that happened or is planned";
|
|
79
|
-
readonly description: "Explicit capture (\"note that today I serviced the car\"). Resolve the date yourself (ISO).";
|
|
79
|
+
readonly description: "Explicit capture (\"note that today I serviced the car\"). Resolve the date yourself (ISO). Conversations are remembered automatically: use it only when explicitly asked, and never ask whether to remember or save something.";
|
|
80
80
|
readonly inputSchema: {
|
|
81
81
|
readonly type: "object";
|
|
82
82
|
readonly properties: {
|
|
@@ -115,7 +115,7 @@ export declare const TOOLS: readonly [{
|
|
|
115
115
|
}, {
|
|
116
116
|
readonly name: "remember";
|
|
117
117
|
readonly title: "Remember something";
|
|
118
|
-
readonly description: "Explicit \"remember that…\" about preferences, habits, values, knowledge.";
|
|
118
|
+
readonly description: "Explicit \"remember that…\" about preferences, habits, values, knowledge. Conversations are remembered automatically: use it only when explicitly asked, and never ask whether to remember or save something.";
|
|
119
119
|
readonly inputSchema: {
|
|
120
120
|
readonly type: "object";
|
|
121
121
|
readonly properties: {
|
|
@@ -147,7 +147,7 @@ export declare const TOOLS: readonly [{
|
|
|
147
147
|
}, {
|
|
148
148
|
readonly name: "search_episodes";
|
|
149
149
|
readonly title: "Search what happened";
|
|
150
|
-
readonly description: "Search your memory of what happened: what you lived, did, planned or learned, and what happened to the people you know (events, plans, changes), with dates and status. Each item has a subject: you (written in the first person), a person by name, someone, or undecided. Use `from`/`to` (ISO dates, inclusive; see resolve_period) for questions about a period. mode: \"search\" = most relevant, \"list\" = chronological in the period (overviews, counting), \"latest\" = most recent first (\"when did I last…\").";
|
|
150
|
+
readonly description: "Search your memory of what happened: what you lived, did, planned or learned, and what happened to the people you know (events, plans, changes), with dates and status. Each item has a subject: you (written in the first person), a person by name, someone, or undecided. Use `from`/`to` (ISO dates, inclusive; see resolve_period) for questions about a period; a vague span (\"lately\", \"these weeks\") is not a period: leave them out. mode: \"search\" = most relevant, \"list\" = chronological in the period (overviews, counting), \"latest\" = most recent first (\"when did I last…\"). An item or chat excerpt marked `heard` was overheard in the room by a listening device, not said to you: tell it as heard at home, and its speaker may have been misrecognised — what it says of you may be someone else's. A chat excerpt by `assistant` is your own earlier reply: what you told them then (advice, a recipe, a name), not something they lived.";
|
|
151
151
|
readonly inputSchema: {
|
|
152
152
|
readonly type: "object";
|
|
153
153
|
readonly properties: {
|
|
@@ -203,7 +203,7 @@ export declare const TOOLS: readonly [{
|
|
|
203
203
|
}, {
|
|
204
204
|
readonly name: "search_memory";
|
|
205
205
|
readonly title: "Search what you know about yourself and others";
|
|
206
|
-
readonly description: "Search your memory of preferences, habits, values, relationships, knowledge and current state (car, home, job…) — yours and of the people you know; each item has its subject. Use `as_of` (ISO date) for \"what was it on that date\" questions; each fact comes with its history.";
|
|
206
|
+
readonly description: "Search your memory of preferences, habits, values, relationships, knowledge and current state (car, home, job…) — yours and of the people you know; each item has its subject. Use `as_of` (ISO date) for \"what was it on that date\" questions; each fact comes with its history. A note with `pattern` is a habit you noticed yourself in repeated episodes, not something said: tell it as such.";
|
|
207
207
|
readonly inputSchema: {
|
|
208
208
|
readonly type: "object";
|
|
209
209
|
readonly properties: {
|
package/dist/tools.js
CHANGED
|
@@ -100,7 +100,7 @@ export const TOOLS = [
|
|
|
100
100
|
{
|
|
101
101
|
"name": "log_episode",
|
|
102
102
|
"title": "Note something that happened or is planned",
|
|
103
|
-
"description": "Explicit capture (\"note that today I serviced the car\"). Resolve the date yourself (ISO).",
|
|
103
|
+
"description": "Explicit capture (\"note that today I serviced the car\"). Resolve the date yourself (ISO). Conversations are remembered automatically: use it only when explicitly asked, and never ask whether to remember or save something.",
|
|
104
104
|
"inputSchema": {
|
|
105
105
|
"type": "object",
|
|
106
106
|
"properties": {
|
|
@@ -150,7 +150,7 @@ export const TOOLS = [
|
|
|
150
150
|
{
|
|
151
151
|
"name": "remember",
|
|
152
152
|
"title": "Remember something",
|
|
153
|
-
"description": "Explicit \"remember that…\" about preferences, habits, values, knowledge.",
|
|
153
|
+
"description": "Explicit \"remember that…\" about preferences, habits, values, knowledge. Conversations are remembered automatically: use it only when explicitly asked, and never ask whether to remember or save something.",
|
|
154
154
|
"inputSchema": {
|
|
155
155
|
"type": "object",
|
|
156
156
|
"properties": {
|
|
@@ -196,7 +196,7 @@ export const TOOLS = [
|
|
|
196
196
|
{
|
|
197
197
|
"name": "search_episodes",
|
|
198
198
|
"title": "Search what happened",
|
|
199
|
-
"description": "Search your memory of what happened: what you lived, did, planned or learned, and what happened to the people you know (events, plans, changes), with dates and status. Each item has a subject: you (written in the first person), a person by name, someone, or undecided. Use `from`/`to` (ISO dates, inclusive; see resolve_period) for questions about a period. mode: \"search\" = most relevant, \"list\" = chronological in the period (overviews, counting), \"latest\" = most recent first (\"when did I last…\").",
|
|
199
|
+
"description": "Search your memory of what happened: what you lived, did, planned or learned, and what happened to the people you know (events, plans, changes), with dates and status. Each item has a subject: you (written in the first person), a person by name, someone, or undecided. Use `from`/`to` (ISO dates, inclusive; see resolve_period) for questions about a period; a vague span (\"lately\", \"these weeks\") is not a period: leave them out. mode: \"search\" = most relevant, \"list\" = chronological in the period (overviews, counting), \"latest\" = most recent first (\"when did I last…\"). An item or chat excerpt marked `heard` was overheard in the room by a listening device, not said to you: tell it as heard at home, and its speaker may have been misrecognised — what it says of you may be someone else's. A chat excerpt by `assistant` is your own earlier reply: what you told them then (advice, a recipe, a name), not something they lived.",
|
|
200
200
|
"inputSchema": {
|
|
201
201
|
"type": "object",
|
|
202
202
|
"properties": {
|
|
@@ -260,7 +260,7 @@ export const TOOLS = [
|
|
|
260
260
|
{
|
|
261
261
|
"name": "search_memory",
|
|
262
262
|
"title": "Search what you know about yourself and others",
|
|
263
|
-
"description": "Search your memory of preferences, habits, values, relationships, knowledge and current state (car, home, job…) — yours and of the people you know; each item has its subject. Use `as_of` (ISO date) for \"what was it on that date\" questions; each fact comes with its history.",
|
|
263
|
+
"description": "Search your memory of preferences, habits, values, relationships, knowledge and current state (car, home, job…) — yours and of the people you know; each item has its subject. Use `as_of` (ISO date) for \"what was it on that date\" questions; each fact comes with its history. A note with `pattern` is a habit you noticed yourself in repeated episodes, not something said: tell it as such.",
|
|
264
264
|
"inputSchema": {
|
|
265
265
|
"type": "object",
|
|
266
266
|
"properties": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arkimedehq/recordare-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Client library for Recordare, the memory service for agent platforms: identity, ingest, deletions, MCP recall.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"author": "Andrea Genovese",
|
package/src/client.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Copyright © 2026 Andrea Genovese
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
CONVERSATION_HEADER, type Digest, type Episode, type EpisodeDetail, type EpisodeQuery, type
|
|
6
|
-
type
|
|
7
|
-
type Precision, type Source,
|
|
5
|
+
type Cards, type ContextOptions, CONVERSATION_HEADER, type Digest, type Episode, type Erased, type EpisodeDetail, type EpisodeQuery, type ExportJob, type Fact, type FactDetail,
|
|
6
|
+
type HolderIdentities, type IngestRequest, type IngestResult, type RoomRequest, type RoomResult, type LearnSource, type LinkCode, type LearnSourceResult, MAX_MESSAGES_PER_REQUEST, type Me, type MemorySettings,
|
|
7
|
+
type MeSettings, type MemoryContext, type Note, type NoteChange, type NoteDetail, type PlanStatus, type Precision, type Source,
|
|
8
|
+
SOURCE_PART_BYTES, type Usage,
|
|
8
9
|
} from './contract.js';
|
|
9
10
|
import { MemoryNotEmptyError, RecordareHttpError } from './errors.js';
|
|
10
11
|
import { type ClientOptions, Http } from './http.js';
|
|
@@ -49,6 +50,39 @@ export class RecordareClient {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Erases the whole memory behind `user`, at once and for good (needs the `erase` scope): everything it holds goes.
|
|
55
|
+
* `memoryId` (from `me`) confirms which memory; a user without a memory answers 404, none is created.
|
|
56
|
+
*/
|
|
57
|
+
eraseMemory(user: string, memoryId: string): Promise<Erased> {
|
|
58
|
+
return this.http.request<Erased>('DELETE', `api/v1/me${qs({ confirm: memoryId })}`, { user });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A one-time code for the holder to send from a channel (D51; personal memories only, else 409). */
|
|
62
|
+
linkCode(user: string): Promise<LinkCode> {
|
|
63
|
+
return this.http.request<LinkCode>('POST', 'api/v1/me/link-codes', { user });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The identities that speak as the memory's "I" and the claims waiting for confirmation (D51). */
|
|
67
|
+
holderIdentities(user: string): Promise<HolderIdentities> {
|
|
68
|
+
return this.http.request<HolderIdentities>('GET', 'api/v1/me/identities', { user });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Confirms a claim: that channel identity is the holder from now on. */
|
|
72
|
+
async confirmHolderClaim(user: string, id: string): Promise<void> {
|
|
73
|
+
await this.http.request('POST', `api/v1/me/identities/pending/${encodeURIComponent(id)}/confirm`, { user });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Rejects a claim (not the holder). */
|
|
77
|
+
async rejectHolderClaim(user: string, id: string): Promise<void> {
|
|
78
|
+
await this.http.request('DELETE', `api/v1/me/identities/pending/${encodeURIComponent(id)}`, { user });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Unlinks one of the holder's identities. */
|
|
82
|
+
async unlinkHolderIdentity(user: string, id: string): Promise<void> {
|
|
83
|
+
await this.http.request('DELETE', `api/v1/me/identities/${encodeURIComponent(id)}`, { user });
|
|
84
|
+
}
|
|
85
|
+
|
|
52
86
|
/**
|
|
53
87
|
* Sends messages of one conversation, split into requests of at most MAX_MESSAGES_PER_REQUEST. Idempotent: Recordare
|
|
54
88
|
* deduplicates on each message's externalId. Recordare always stores (D50): the on/off switch is the platform's.
|
|
@@ -74,21 +108,46 @@ export class RecordareClient {
|
|
|
74
108
|
return total;
|
|
75
109
|
}
|
|
76
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Sends a room transcript to the memories of the people present (continuous listening), split into requests of at
|
|
113
|
+
* most MAX_MESSAGES_PER_REQUEST; needs a client key (not a personal token). Idempotent like `ingest`.
|
|
114
|
+
*/
|
|
115
|
+
async ingestRoom(req: RoomRequest): Promise<RoomResult> {
|
|
116
|
+
const total = new Map<string, RoomResult['recipients'][number]>();
|
|
117
|
+
for (let i = 0; i < req.messages.length; i += MAX_MESSAGES_PER_REQUEST) {
|
|
118
|
+
const last = i + MAX_MESSAGES_PER_REQUEST >= req.messages.length;
|
|
119
|
+
const part = await this.http.request<RoomResult>('POST', 'api/v1/ingest/room', {
|
|
120
|
+
body: { ...req, messages: req.messages.slice(i, i + MAX_MESSAGES_PER_REQUEST), hints: req.hints && last ? req.hints : undefined },
|
|
121
|
+
});
|
|
122
|
+
for (const r of part.recipients) {
|
|
123
|
+
const t = total.get(r.user);
|
|
124
|
+
if (!t) total.set(r.user, { ...r, conflicts: [...r.conflicts] });
|
|
125
|
+
else {
|
|
126
|
+
t.conversationId = r.conversationId ?? t.conversationId;
|
|
127
|
+
t.accepted += r.accepted;
|
|
128
|
+
t.duplicates += r.duplicates;
|
|
129
|
+
t.conflicts.push(...r.conflicts);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { recipients: [...total.values()] };
|
|
134
|
+
}
|
|
135
|
+
|
|
77
136
|
/**
|
|
78
137
|
* The memories relevant to `query` (the message about to be answered) as a fenced block to append to the prompt — or
|
|
79
138
|
* null. Append it at the end of the system prompt (it changes every turn: keep the stable part first for prompt
|
|
80
|
-
* caching); never store it as a chat message.
|
|
139
|
+
* caching); never store it as a chat message. `card`: open it with the self card (who I am), off by default.
|
|
81
140
|
*/
|
|
82
|
-
context(user: string, conversation: string, query: string): Promise<MemoryContext> {
|
|
83
|
-
return this.http.request<MemoryContext>('POST', 'api/v1/context', { user, body: { query }, headers: { [CONVERSATION_HEADER]: conversation } });
|
|
141
|
+
context(user: string, conversation: string, query: string, opts: ContextOptions = {}): Promise<MemoryContext> {
|
|
142
|
+
return this.http.request<MemoryContext>('POST', 'api/v1/context', { user, body: { query, ...opts }, headers: { [CONVERSATION_HEADER]: conversation } });
|
|
84
143
|
}
|
|
85
144
|
|
|
86
145
|
/**
|
|
87
146
|
* Stores the turn and returns the memory context for it in one round trip (needs the `ingest` and `read` scopes):
|
|
88
147
|
* the context is computed for the ingested conversation, from `query` or else the turn's last user message.
|
|
89
148
|
*/
|
|
90
|
-
contextWithTurn(user: string, turn: IngestRequest, query?: string): Promise<MemoryContext> {
|
|
91
|
-
return this.http.request<MemoryContext>('POST', 'api/v1/context', { user, body: { ingest: turn, ...(query ? { query } : {}) } });
|
|
149
|
+
contextWithTurn(user: string, turn: IngestRequest, query?: string, opts: ContextOptions = {}): Promise<MemoryContext> {
|
|
150
|
+
return this.http.request<MemoryContext>('POST', 'api/v1/context', { user, body: { ingest: turn, ...(query ? { query } : {}), ...opts } });
|
|
92
151
|
}
|
|
93
152
|
|
|
94
153
|
/** The conversation ended (session closed, /new): extraction runs now. A conversation Recordare never had is done. */
|
|
@@ -159,6 +218,14 @@ export class RecordareClient {
|
|
|
159
218
|
await this.http.request('DELETE', `api/v1/episodes/${enc(id)}`, { user });
|
|
160
219
|
}
|
|
161
220
|
|
|
221
|
+
/**
|
|
222
|
+
* The person forgets a period (days or months, inclusive): its episodes and raw messages (`keepRaw`: kept but hidden
|
|
223
|
+
* and never extracted again), with what only they supported. Returns how much went.
|
|
224
|
+
*/
|
|
225
|
+
forgetPeriod(user: string, period: { from: string; to: string; keepRaw?: boolean }): Promise<{ episodes: number; messages: number; facts: number; notes: number }> {
|
|
226
|
+
return this.http.request('POST', 'api/v1/forget', { user, body: period });
|
|
227
|
+
}
|
|
228
|
+
|
|
162
229
|
digests(user: string, query: { level?: 'day' | 'month'; from?: string; to?: string } = {}): Promise<Digest[]> {
|
|
163
230
|
return this.http.request('GET', `api/v1/digests${qs(query)}`, { user });
|
|
164
231
|
}
|
|
@@ -167,6 +234,11 @@ export class RecordareClient {
|
|
|
167
234
|
return this.http.request('GET', `api/v1/facts${qs(query)}`, { user });
|
|
168
235
|
}
|
|
169
236
|
|
|
237
|
+
/** Who I am and who my contacts are: current facts and notes of the self and of each contact (no LLM). */
|
|
238
|
+
cards(user: string): Promise<Cards> {
|
|
239
|
+
return this.http.request<Cards>('GET', 'api/v1/cards', { user });
|
|
240
|
+
}
|
|
241
|
+
|
|
170
242
|
notes(user: string, query: { category?: Note['category']; pinned?: boolean; includePending?: boolean } = {}): Promise<Note[]> {
|
|
171
243
|
return this.http.request('GET', `api/v1/notes${qs(query)}`, { user });
|
|
172
244
|
}
|
|
@@ -190,6 +262,68 @@ export class RecordareClient {
|
|
|
190
262
|
await this.http.request('POST', `api/v1/${what}/${enc(id)}/${decision}`, { user });
|
|
191
263
|
}
|
|
192
264
|
|
|
265
|
+
/** The person writes an episode in the platform's UI (stated, theirs). Returns its id. */
|
|
266
|
+
async addEpisode(user: string, episode: { content: string; kind?: 'event' | 'plan'; occurredAt?: string; occurredUntil?: string;
|
|
267
|
+
datePrecision?: Exclude<Precision, 'unknown' | 'minute'>; place?: string; people?: string[] }): Promise<string> {
|
|
268
|
+
return (await this.http.request<{ id: string }>('POST', 'api/v1/episodes', { user, body: episode })).id;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The person writes a note in the platform's UI (stated, theirs). Returns its id. */
|
|
272
|
+
async addNote(user: string, note: { content: string; category?: Note['category'] }): Promise<string> {
|
|
273
|
+
return (await this.http.request<{ id: string }>('POST', 'api/v1/notes', { user, body: note })).id;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fact(user: string, id: string): Promise<FactDetail> {
|
|
277
|
+
return this.http.request('GET', `api/v1/facts/${enc(id)}`, { user });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
note(user: string, id: string): Promise<NoteDetail> {
|
|
281
|
+
return this.http.request('GET', `api/v1/notes/${enc(id)}`, { user });
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The person corrects a current fact (`value` null: not known any more); the wrong one stays in its history. */
|
|
285
|
+
async correctFact(user: string, id: string, fix: { value: string | null; validFrom?: string; datePrecision?: Exclude<Precision, 'unknown' | 'minute'> }): Promise<string> {
|
|
286
|
+
return (await this.http.request<{ id: string }>('POST', `api/v1/facts/${enc(id)}/corrections`, { user, body: fix })).id;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The person corrects a current note; the wrong one stays as `corrected`. */
|
|
290
|
+
async correctNote(user: string, id: string, fix: { content: string; category?: Note['category'] }): Promise<string> {
|
|
291
|
+
return (await this.http.request<{ id: string }>('POST', `api/v1/notes/${enc(id)}/corrections`, { user, body: fix })).id;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The notes change feed, for a platform keeping a copy: pass `next` back as `since`; `more` when the page was full. */
|
|
295
|
+
noteChanges(user: string, query: { since?: number; limit?: number } = {}): Promise<{ items: NoteChange[]; next: number; more: boolean }> {
|
|
296
|
+
return this.http.request('GET', `api/v1/notes/changes${qs(query)}`, { user });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
settings(user: string): Promise<MemorySettings> {
|
|
300
|
+
return this.http.request('GET', 'api/v1/settings', { user });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Needs `memory_settings`: the memory's personal token (never a client key); pass `user` empty with it. */
|
|
304
|
+
updateSettings(user: string, patch: { locale?: string; timezone?: string; qualityProfile?: MemorySettings['qualityProfile'] }): Promise<MemorySettings> {
|
|
305
|
+
return this.http.request('PATCH', 'api/v1/settings', { user, body: patch });
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** LLM calls and tokens spent on the memory between two local days (default: the last 30). */
|
|
309
|
+
usage(user: string, query: { from?: string; to?: string } = {}): Promise<Usage> {
|
|
310
|
+
return this.http.request('GET', `api/v1/usage${qs(query)}`, { user });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Starts a full export (needs `export`: the memory's personal token); poll `exportJob`, then `exportArchive`. */
|
|
314
|
+
startExport(user: string): Promise<ExportJob> {
|
|
315
|
+
return this.http.request('POST', 'api/v1/exports', { user });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
exportJob(user: string, id: string): Promise<ExportJob> {
|
|
319
|
+
return this.http.request('GET', `api/v1/exports/${enc(id)}`, { user });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** The finished export: one JSON archive (`format: "recordare-export/1"`). */
|
|
323
|
+
exportArchive(user: string, id: string): Promise<Record<string, unknown>> {
|
|
324
|
+
return this.http.request('GET', `api/v1/exports/${enc(id)}/archive`, { user });
|
|
325
|
+
}
|
|
326
|
+
|
|
193
327
|
/** Closes the MCP sessions (on shutdown). */
|
|
194
328
|
close(): Promise<void> {
|
|
195
329
|
return this.mcp.close();
|