@mem9/opencode 0.1.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.
@@ -0,0 +1,73 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ export interface Mem9PathInput {
5
+ configDir: string;
6
+ dataDir: string;
7
+ projectDir: string;
8
+ mem9Home: string;
9
+ }
10
+
11
+ export interface Mem9ResolvedPaths {
12
+ globalConfigFile: string;
13
+ projectConfigFile: string;
14
+ credentialsFile: string;
15
+ logDir: string;
16
+ }
17
+
18
+ export interface OpenCodeBasePaths {
19
+ configDir: string;
20
+ dataDir: string;
21
+ }
22
+
23
+ function normalizeDir(value: string | undefined): string | undefined {
24
+ if (typeof value !== "string") {
25
+ return undefined;
26
+ }
27
+
28
+ const trimmed = value.trim();
29
+ return trimmed.length > 0 ? trimmed : undefined;
30
+ }
31
+
32
+ export function resolveMem9Home(
33
+ env: Record<string, string | undefined>,
34
+ homeDir = os.homedir(),
35
+ ): string {
36
+ const override = normalizeDir(env.MEM9_HOME);
37
+ if (override) {
38
+ return override;
39
+ }
40
+
41
+ return path.join(homeDir, ".mem9");
42
+ }
43
+
44
+ export function resolveOpenCodeBasePaths(
45
+ env: Record<string, string | undefined>,
46
+ homeDir = os.homedir(),
47
+ platform = process.platform,
48
+ ): OpenCodeBasePaths {
49
+ if (platform === "win32") {
50
+ const configRoot = normalizeDir(env.APPDATA) ?? path.join(homeDir, "AppData", "Roaming");
51
+ const dataRoot = normalizeDir(env.LOCALAPPDATA) ?? path.join(homeDir, "AppData", "Local");
52
+ return {
53
+ configDir: path.join(configRoot, "opencode"),
54
+ dataDir: path.join(dataRoot, "opencode"),
55
+ };
56
+ }
57
+
58
+ const configRoot = normalizeDir(env.XDG_CONFIG_HOME) ?? path.join(homeDir, ".config");
59
+ const dataRoot = normalizeDir(env.XDG_DATA_HOME) ?? path.join(homeDir, ".local", "share");
60
+ return {
61
+ configDir: path.join(configRoot, "opencode"),
62
+ dataDir: path.join(dataRoot, "opencode"),
63
+ };
64
+ }
65
+
66
+ export function resolveMem9Paths(input: Mem9PathInput): Mem9ResolvedPaths {
67
+ return {
68
+ globalConfigFile: path.join(input.configDir, "mem9.json"),
69
+ projectConfigFile: path.join(input.projectDir, ".opencode", "mem9.json"),
70
+ credentialsFile: path.join(input.mem9Home, ".credentials.json"),
71
+ logDir: path.join(input.dataDir, "plugins", "mem9", "log"),
72
+ };
73
+ }
@@ -0,0 +1 @@
1
+ export const PLUGIN_ID = "mem9";
@@ -0,0 +1,492 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { Mem9ResolvedPaths } from "./platform-paths.js";
4
+ import { parseCredentialsFile, stringifyCredentialsFile } from "./credentials-store.js";
5
+ import { DEFAULT_SCOPE_CONFIG } from "./defaults.js";
6
+ import {
7
+ DEFAULT_API_URL,
8
+ type Mem9ConfigFile,
9
+ type Mem9CredentialsFile,
10
+ type Mem9Profile,
11
+ } from "./types.js";
12
+
13
+ export type SetupScope = "user" | "project";
14
+
15
+ export interface SetupProfileSummary {
16
+ profileId: string;
17
+ label: string;
18
+ baseUrl: string;
19
+ hasApiKey: boolean;
20
+ }
21
+
22
+ export interface ScopeConfigState {
23
+ profileId?: string;
24
+ debug: boolean;
25
+ defaultTimeoutMs: number;
26
+ searchTimeoutMs: number;
27
+ }
28
+
29
+ export interface SetupState {
30
+ suggestedProfileId: string;
31
+ suggestedNewProfileId: string;
32
+ suggestedLabel: string;
33
+ suggestedBaseUrl: string;
34
+ profiles: SetupProfileSummary[];
35
+ usableProfiles: SetupProfileSummary[];
36
+ scopeStates: Record<SetupScope, ScopeConfigState>;
37
+ }
38
+
39
+ export interface SetupRequest {
40
+ paths: Mem9ResolvedPaths;
41
+ profileId: string;
42
+ label: string;
43
+ baseUrl: string;
44
+ apiKey: string;
45
+ }
46
+
47
+ export interface ScopeConfigRequest {
48
+ paths: Mem9ResolvedPaths;
49
+ scope: SetupScope;
50
+ profileId: string;
51
+ debug: boolean;
52
+ defaultTimeoutMs: number;
53
+ searchTimeoutMs: number;
54
+ }
55
+
56
+ export interface ProvisionApiKeyOptions {
57
+ baseUrl: string;
58
+ fetchImpl?: typeof fetch;
59
+ timeoutMs?: number;
60
+ }
61
+
62
+ function isRecord(value: unknown): value is Record<string, unknown> {
63
+ return typeof value === "object" && value !== null && !Array.isArray(value);
64
+ }
65
+
66
+ function normalizeOptionalString(value: unknown): string | undefined {
67
+ if (typeof value !== "string") {
68
+ return undefined;
69
+ }
70
+
71
+ const trimmed = value.trim();
72
+ return trimmed.length > 0 ? trimmed : undefined;
73
+ }
74
+
75
+ function normalizeBaseUrl(value: unknown): string | undefined {
76
+ const normalized = normalizeOptionalString(value);
77
+ return normalized ? normalized.replace(/\/+$/, "") : undefined;
78
+ }
79
+
80
+ function normalizeTimeoutMs(value: unknown, fallback: number): number {
81
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
82
+ return fallback;
83
+ }
84
+
85
+ return Math.floor(value);
86
+ }
87
+
88
+ function requireString(value: string, field: string): string {
89
+ const trimmed = value.trim();
90
+ if (trimmed.length === 0) {
91
+ throw new Error(`${field} is required`);
92
+ }
93
+ return trimmed;
94
+ }
95
+
96
+ function isErrnoException(error: unknown): error is NodeJS.ErrnoException {
97
+ return error instanceof Error;
98
+ }
99
+
100
+ function resolveScopeConfigFile(
101
+ paths: Mem9ResolvedPaths,
102
+ scope: SetupScope,
103
+ ): string {
104
+ return scope === "user" ? paths.globalConfigFile : paths.projectConfigFile;
105
+ }
106
+
107
+ function hasApiKey(profile: unknown): boolean {
108
+ if (!isRecord(profile)) {
109
+ return false;
110
+ }
111
+
112
+ return Boolean(normalizeOptionalString(profile.apiKey));
113
+ }
114
+
115
+ function normalizeProfileRecord(
116
+ profileId: string,
117
+ profile: Mem9Profile | undefined,
118
+ ): Mem9Profile {
119
+ return {
120
+ label:
121
+ normalizeOptionalString(profile?.label)
122
+ ?? (profileId === "default" ? "Personal" : profileId),
123
+ baseUrl: normalizeBaseUrl(profile?.baseUrl) ?? DEFAULT_API_URL,
124
+ apiKey: typeof profile?.apiKey === "string" ? profile.apiKey : "",
125
+ };
126
+ }
127
+
128
+ function buildDefaultProfileId(
129
+ profiles: Record<string, Mem9Profile>,
130
+ preferredProfileId: string | undefined,
131
+ ): string {
132
+ const preferred = normalizeOptionalString(preferredProfileId);
133
+ if (preferred) {
134
+ return preferred;
135
+ }
136
+
137
+ const profileIds = Object.keys(profiles).sort((left, right) =>
138
+ left.localeCompare(right),
139
+ );
140
+ if (profileIds.includes("default")) {
141
+ return "default";
142
+ }
143
+
144
+ return profileIds[0] ?? "default";
145
+ }
146
+
147
+ function buildSuggestedNewProfileId(
148
+ profiles: Record<string, Mem9Profile>,
149
+ preferredProfileId: string,
150
+ ): string {
151
+ const existingProfile = profiles[preferredProfileId];
152
+ if (!existingProfile || !hasApiKey(existingProfile)) {
153
+ return preferredProfileId;
154
+ }
155
+
156
+ let suffix = 2;
157
+ while (profiles[`${preferredProfileId}-${suffix}`]) {
158
+ suffix += 1;
159
+ }
160
+
161
+ return `${preferredProfileId}-${suffix}`;
162
+ }
163
+
164
+ function sortProfileSummaries(
165
+ left: SetupProfileSummary,
166
+ right: SetupProfileSummary,
167
+ ): number {
168
+ if (left.profileId === "default") {
169
+ return -1;
170
+ }
171
+ if (right.profileId === "default") {
172
+ return 1;
173
+ }
174
+ if (left.hasApiKey !== right.hasApiKey) {
175
+ return left.hasApiKey ? -1 : 1;
176
+ }
177
+ return left.profileId.localeCompare(right.profileId);
178
+ }
179
+
180
+ function buildProfiles(
181
+ profiles: Record<string, Mem9Profile>,
182
+ ): SetupProfileSummary[] {
183
+ return Object.entries(profiles)
184
+ .map(([profileId, profile]) => {
185
+ const normalized = normalizeProfileRecord(profileId, profile);
186
+ return {
187
+ profileId,
188
+ label: normalized.label,
189
+ baseUrl: normalized.baseUrl,
190
+ hasApiKey: normalized.apiKey.trim().length > 0,
191
+ };
192
+ })
193
+ .sort(sortProfileSummaries);
194
+ }
195
+
196
+ function normalizeConfigLayer(
197
+ value: Record<string, unknown> | undefined,
198
+ ): Mem9ConfigFile {
199
+ return {
200
+ schemaVersion: 1,
201
+ profileId: normalizeOptionalString(value?.profileId),
202
+ debug: typeof value?.debug === "boolean" ? value.debug : undefined,
203
+ defaultTimeoutMs:
204
+ typeof value?.defaultTimeoutMs === "number" ? value.defaultTimeoutMs : undefined,
205
+ searchTimeoutMs:
206
+ typeof value?.searchTimeoutMs === "number" ? value.searchTimeoutMs : undefined,
207
+ };
208
+ }
209
+
210
+ function buildScopeState(
211
+ config: Mem9ConfigFile,
212
+ usableProfiles: SetupProfileSummary[],
213
+ fallbackProfileId?: string,
214
+ ): ScopeConfigState {
215
+ const usableProfileIds = new Set(
216
+ usableProfiles.map((profile) => profile.profileId),
217
+ );
218
+ const configuredProfileId = normalizeOptionalString(config.profileId);
219
+ const resolvedProfileId =
220
+ (configuredProfileId && usableProfileIds.has(configuredProfileId))
221
+ ? configuredProfileId
222
+ : usableProfiles[0]?.profileId ?? fallbackProfileId;
223
+
224
+ return {
225
+ profileId: resolvedProfileId,
226
+ debug: config.debug ?? DEFAULT_SCOPE_CONFIG.debug,
227
+ defaultTimeoutMs: normalizeTimeoutMs(
228
+ config.defaultTimeoutMs,
229
+ DEFAULT_SCOPE_CONFIG.defaultTimeoutMs,
230
+ ),
231
+ searchTimeoutMs: normalizeTimeoutMs(
232
+ config.searchTimeoutMs,
233
+ DEFAULT_SCOPE_CONFIG.searchTimeoutMs,
234
+ ),
235
+ };
236
+ }
237
+
238
+ function mergeConfigLayers(
239
+ ...layers: Mem9ConfigFile[]
240
+ ): Mem9ConfigFile {
241
+ const merged: Mem9ConfigFile = {
242
+ schemaVersion: 1,
243
+ };
244
+
245
+ for (const layer of layers) {
246
+ if (layer.profileId !== undefined) {
247
+ merged.profileId = layer.profileId;
248
+ }
249
+ if (layer.debug !== undefined) {
250
+ merged.debug = layer.debug;
251
+ }
252
+ if (layer.defaultTimeoutMs !== undefined) {
253
+ merged.defaultTimeoutMs = layer.defaultTimeoutMs;
254
+ }
255
+ if (layer.searchTimeoutMs !== undefined) {
256
+ merged.searchTimeoutMs = layer.searchTimeoutMs;
257
+ }
258
+ }
259
+
260
+ return merged;
261
+ }
262
+
263
+ function createScopeConfig(
264
+ existing: Record<string, unknown> | undefined,
265
+ input: ScopeConfigState,
266
+ ): Record<string, unknown> {
267
+ return {
268
+ ...(existing ?? {}),
269
+ schemaVersion: 1,
270
+ profileId: input.profileId,
271
+ debug: input.debug,
272
+ defaultTimeoutMs: normalizeTimeoutMs(
273
+ input.defaultTimeoutMs,
274
+ DEFAULT_SCOPE_CONFIG.defaultTimeoutMs,
275
+ ),
276
+ searchTimeoutMs: normalizeTimeoutMs(
277
+ input.searchTimeoutMs,
278
+ DEFAULT_SCOPE_CONFIG.searchTimeoutMs,
279
+ ),
280
+ };
281
+ }
282
+
283
+ async function readJsonObject(filePath: string): Promise<Record<string, unknown> | undefined> {
284
+ try {
285
+ const raw = await readFile(filePath, "utf8");
286
+ const parsed = JSON.parse(raw) as unknown;
287
+ if (!isRecord(parsed)) {
288
+ throw new Error("invalid json object");
289
+ }
290
+ return parsed;
291
+ } catch (error) {
292
+ if (isErrnoException(error) && error.code === "ENOENT") {
293
+ return undefined;
294
+ }
295
+ throw error;
296
+ }
297
+ }
298
+
299
+ async function readCredentialsFile(filePath: string): Promise<Mem9CredentialsFile> {
300
+ try {
301
+ const raw = await readFile(filePath, "utf8");
302
+ return parseCredentialsFile(raw);
303
+ } catch (error) {
304
+ if (isErrnoException(error) && error.code === "ENOENT") {
305
+ return {
306
+ schemaVersion: 1,
307
+ profiles: {},
308
+ };
309
+ }
310
+ throw error;
311
+ }
312
+ }
313
+
314
+ async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
315
+ await mkdir(path.dirname(filePath), { recursive: true });
316
+ await writeFile(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
317
+ }
318
+
319
+ export async function loadSetupState(paths: Mem9ResolvedPaths): Promise<SetupState> {
320
+ const [globalConfigRaw, projectConfigRaw, credentials] = await Promise.all([
321
+ readJsonObject(paths.globalConfigFile),
322
+ readJsonObject(paths.projectConfigFile),
323
+ readCredentialsFile(paths.credentialsFile),
324
+ ]);
325
+ const globalConfig = normalizeConfigLayer(globalConfigRaw);
326
+ const projectConfig = normalizeConfigLayer(projectConfigRaw);
327
+ const profiles = buildProfiles(credentials.profiles);
328
+ const usableProfiles = profiles.filter((profile) => profile.hasApiKey);
329
+
330
+ const suggestedProfileId = buildDefaultProfileId(
331
+ credentials.profiles,
332
+ globalConfig.profileId,
333
+ );
334
+ const suggestedProfile = normalizeProfileRecord(
335
+ suggestedProfileId,
336
+ credentials.profiles[suggestedProfileId],
337
+ );
338
+ const userScopeState = buildScopeState(
339
+ globalConfig,
340
+ usableProfiles,
341
+ suggestedProfileId,
342
+ );
343
+ const projectScopeState = buildScopeState(
344
+ mergeConfigLayers(globalConfig, projectConfig),
345
+ usableProfiles,
346
+ userScopeState.profileId ?? suggestedProfileId,
347
+ );
348
+
349
+ return {
350
+ suggestedProfileId,
351
+ suggestedNewProfileId: buildSuggestedNewProfileId(
352
+ credentials.profiles,
353
+ suggestedProfileId,
354
+ ),
355
+ suggestedLabel: suggestedProfile.label,
356
+ suggestedBaseUrl: suggestedProfile.baseUrl,
357
+ profiles,
358
+ usableProfiles,
359
+ scopeStates: {
360
+ user: userScopeState,
361
+ project: projectScopeState,
362
+ },
363
+ };
364
+ }
365
+
366
+ export async function writeSetupFiles(input: SetupRequest): Promise<void> {
367
+ const profileId = requireString(input.profileId, "profileId");
368
+ const label = requireString(input.label, "label");
369
+ const baseUrl = requireString(normalizeBaseUrl(input.baseUrl) ?? "", "baseUrl");
370
+ const apiKey = requireString(input.apiKey, "apiKey");
371
+
372
+ const [credentials, globalConfigRaw] = await Promise.all([
373
+ readCredentialsFile(input.paths.credentialsFile),
374
+ readJsonObject(input.paths.globalConfigFile),
375
+ ]);
376
+ const globalConfig = buildScopeState(
377
+ normalizeConfigLayer(globalConfigRaw),
378
+ [],
379
+ profileId,
380
+ );
381
+
382
+ const nextCredentials: Mem9CredentialsFile = {
383
+ schemaVersion: 1,
384
+ profiles: {
385
+ ...credentials.profiles,
386
+ [profileId]: {
387
+ label,
388
+ baseUrl,
389
+ apiKey,
390
+ },
391
+ },
392
+ };
393
+
394
+ await Promise.all([
395
+ writeJsonFile(
396
+ input.paths.globalConfigFile,
397
+ createScopeConfig(globalConfigRaw, {
398
+ ...globalConfig,
399
+ profileId,
400
+ }),
401
+ ),
402
+ (async () => {
403
+ await mkdir(path.dirname(input.paths.credentialsFile), { recursive: true });
404
+ await writeFile(
405
+ input.paths.credentialsFile,
406
+ stringifyCredentialsFile(nextCredentials),
407
+ "utf8",
408
+ );
409
+ })(),
410
+ ]);
411
+ }
412
+
413
+ export async function writeScopeConfig(
414
+ input: ScopeConfigRequest,
415
+ ): Promise<void> {
416
+ const profileId = requireString(input.profileId, "profileId");
417
+ const configFile = resolveScopeConfigFile(input.paths, input.scope);
418
+ const [existing, credentials] = await Promise.all([
419
+ readJsonObject(configFile),
420
+ readCredentialsFile(input.paths.credentialsFile),
421
+ ]);
422
+ const profile = credentials.profiles[profileId];
423
+
424
+ if (!profile || !hasApiKey(profile)) {
425
+ throw new Error(`mem9 profile "${profileId}" is unavailable.`);
426
+ }
427
+
428
+ await writeJsonFile(
429
+ configFile,
430
+ createScopeConfig(existing, {
431
+ profileId,
432
+ debug: input.debug,
433
+ defaultTimeoutMs: input.defaultTimeoutMs,
434
+ searchTimeoutMs: input.searchTimeoutMs,
435
+ }),
436
+ );
437
+ }
438
+
439
+ export async function provisionApiKey(
440
+ options: ProvisionApiKeyOptions,
441
+ ): Promise<string> {
442
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
443
+ if (typeof fetchImpl !== "function") {
444
+ throw new Error("Global fetch is unavailable, so mem9 setup cannot request an API key.");
445
+ }
446
+
447
+ const baseUrl = requireString(normalizeBaseUrl(options.baseUrl) ?? "", "baseUrl");
448
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SCOPE_CONFIG.defaultTimeoutMs;
449
+
450
+ let response: Response;
451
+ try {
452
+ response = await fetchImpl(`${baseUrl}/v1alpha1/mem9s`, {
453
+ method: "POST",
454
+ headers: {
455
+ "Content-Type": "application/json",
456
+ },
457
+ signal: AbortSignal.timeout(timeoutMs),
458
+ });
459
+ } catch (error) {
460
+ if (error instanceof Error && error.name === "TimeoutError") {
461
+ throw new Error(`mem9 setup timed out after ${timeoutMs}ms while requesting an API key.`);
462
+ }
463
+
464
+ throw new Error(
465
+ `mem9 setup could not request an API key: ${error instanceof Error ? error.message : String(error)}`,
466
+ );
467
+ }
468
+
469
+ if (!response.ok) {
470
+ throw new Error(`mem9 setup could not request an API key (HTTP ${response.status}).`);
471
+ }
472
+
473
+ let payload: unknown;
474
+ try {
475
+ payload = await response.json();
476
+ } catch (error) {
477
+ throw new Error(
478
+ `mem9 setup received invalid JSON while requesting an API key: ${error instanceof Error ? error.message : String(error)}`,
479
+ );
480
+ }
481
+
482
+ if (!isRecord(payload)) {
483
+ throw new Error("mem9 setup did not receive an API key from the server.");
484
+ }
485
+
486
+ const apiKey = normalizeOptionalString(payload.id);
487
+ if (!apiKey) {
488
+ throw new Error("mem9 setup did not receive an API key from the server.");
489
+ }
490
+
491
+ return apiKey;
492
+ }
@@ -0,0 +1,74 @@
1
+ /** Default mem9 API endpoint. */
2
+ export const DEFAULT_API_URL = "https://api.mem9.ai";
3
+
4
+ export interface Mem9ConfigFile {
5
+ schemaVersion: 1;
6
+ profileId?: string;
7
+ debug?: boolean;
8
+ defaultTimeoutMs?: number;
9
+ searchTimeoutMs?: number;
10
+ }
11
+
12
+ export interface Mem9Profile {
13
+ label: string;
14
+ baseUrl: string;
15
+ apiKey: string;
16
+ }
17
+
18
+ export interface Mem9CredentialsFile {
19
+ schemaVersion: 1;
20
+ profiles: Record<string, Mem9Profile>;
21
+ }
22
+
23
+ export interface Memory {
24
+ id: string;
25
+ content: string;
26
+ source?: string | null;
27
+ tags?: string[] | null;
28
+ metadata?: Record<string, unknown> | null;
29
+ version?: number;
30
+ updated_by?: string | null;
31
+ created_at: string;
32
+ updated_at: string;
33
+ score?: number;
34
+
35
+ // Smart memory pipeline fields (server mode)
36
+ memory_type?: string;
37
+ state?: string;
38
+ agent_id?: string;
39
+ session_id?: string;
40
+
41
+ relative_age?: string;
42
+ }
43
+
44
+ export interface SearchResult {
45
+ memories: Memory[];
46
+ total: number;
47
+ limit: number;
48
+ offset: number;
49
+ }
50
+
51
+ export interface CreateMemoryInput {
52
+ content: string;
53
+ source?: string;
54
+ tags?: string[];
55
+ metadata?: Record<string, unknown>;
56
+ }
57
+
58
+ export interface UpdateMemoryInput {
59
+ content?: string;
60
+ source?: string;
61
+ tags?: string[];
62
+ metadata?: Record<string, unknown>;
63
+ }
64
+
65
+ export interface SearchInput {
66
+ q?: string;
67
+ tags?: string;
68
+ source?: string;
69
+ limit?: number;
70
+ offset?: number;
71
+ memory_type?: string;
72
+ }
73
+
74
+ export type StoreResult = Memory;