memorix 1.9.7 → 1.9.9

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -0
  3. package/README.zh-CN.md +2 -0
  4. package/dist/cli/index.js +192 -11
  5. package/dist/index.js +184 -11
  6. package/dist/maintenance-runner.js +22 -5
  7. package/dist/media-audio-runner.js +22 -5
  8. package/dist/media-video-runner.js +22 -5
  9. package/dist/memcode-runtime/CHANGELOG.md +15 -0
  10. package/dist/sdk.js +183 -10
  11. package/dist/vector-backfill-runner.js +22 -5
  12. package/docs/CONFIGURATION.md +22 -0
  13. package/docs/DEVELOPMENT.md +1 -1
  14. package/docs/INTEGRATIONS.md +1 -1
  15. package/docs/README.md +1 -1
  16. package/docs/SETUP.md +1 -1
  17. package/package.json +1 -1
  18. package/plugins/antigravity/memorix/plugin.json +1 -1
  19. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  20. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  21. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  22. package/plugins/copilot/memorix/plugin.json +1 -1
  23. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  24. package/plugins/hermes/memorix/plugin.yaml +1 -1
  25. package/plugins/omp/memorix/package.json +1 -1
  26. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  27. package/plugins/pi/memorix/package.json +1 -1
  28. package/server.json +2 -2
  29. package/src/cli/commands/init.ts +7 -1
  30. package/src/cli/tui/ConfigureView.tsx +2 -0
  31. package/src/config/dotenv-loader.ts +1 -0
  32. package/src/config/resolved-config.ts +19 -5
  33. package/src/config/yaml-loader.ts +2 -2
  34. package/src/config.ts +5 -1
  35. package/src/dashboard/server.ts +3 -0
  36. package/src/hooks/installers/index.ts +160 -3
  37. package/src/llm/provider.ts +2 -1
  38. package/src/multimodal/image-loader.ts +2 -2
@@ -96,6 +96,7 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
96
96
  const legacy = loadFileConfig(homeDir);
97
97
  const embeddingBaseUrl = first(process.env.MEMORIX_EMBEDDING_BASE_URL, toml.embedding?.base_url, yaml.embedding?.baseUrl, legacy.embeddingApi?.baseUrl);
98
98
  const openRouterEmbeddingApiKey = isOpenRouterUrl(embeddingBaseUrl) ? process.env.OPENROUTER_API_KEY : undefined;
99
+ const requestyEmbeddingApiKey = isRequestyUrl(embeddingBaseUrl) ? process.env.REQUESTY_API_KEY : undefined;
99
100
  const memoryLlmProvider = first(
100
101
  process.env.MEMORIX_LLM_PROVIDER,
101
102
  toml.memory?.llm?.provider,
@@ -118,16 +119,18 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
118
119
  ? process.env.OPENROUTER_API_KEY
119
120
  : undefined;
120
121
  const isAtlasCloud = memoryLlmProvider === 'atlascloud';
122
+ const isRequesty = memoryLlmProvider === 'requesty';
123
+ const usesPresetKeyOnly = isAtlasCloud || isRequesty;
121
124
  const memoryLlmApiKey = first(
122
125
  process.env.MEMORIX_LLM_API_KEY,
123
126
  process.env.MEMORIX_API_KEY,
124
127
  toml.memory?.llm?.api_key,
125
128
  yaml.llm?.apiKey,
126
129
  legacy.llm?.apiKey,
127
- // Never borrow another provider's credentials for the Atlas Cloud preset.
128
- isAtlasCloud ? process.env.ATLASCLOUD_API_KEY : process.env.OPENAI_API_KEY,
129
- isAtlasCloud ? undefined : process.env.ANTHROPIC_API_KEY,
130
- isAtlasCloud ? undefined : openRouterMemoryLlmApiKey,
130
+ // Never borrow another provider's credentials for the Atlas Cloud or Requesty presets.
131
+ isAtlasCloud ? process.env.ATLASCLOUD_API_KEY : isRequesty ? process.env.REQUESTY_API_KEY : process.env.OPENAI_API_KEY,
132
+ usesPresetKeyOnly ? undefined : process.env.ANTHROPIC_API_KEY,
133
+ usesPresetKeyOnly ? undefined : openRouterMemoryLlmApiKey,
131
134
  );
132
135
 
133
136
  const resolved: ResolvedMemorixConfig = {
@@ -177,7 +180,7 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
177
180
  provider: first(process.env.MEMORIX_EMBEDDING, toml.embedding?.provider, yaml.embedding?.provider, legacy.embedding, 'off'),
178
181
  model: first(process.env.MEMORIX_EMBEDDING_MODEL, toml.embedding?.model, yaml.embedding?.model, legacy.embeddingApi?.model),
179
182
  baseUrl: embeddingBaseUrl,
180
- apiKey: first(process.env.MEMORIX_EMBEDDING_API_KEY, toml.embedding?.api_key, yaml.embedding?.apiKey, legacy.embeddingApi?.apiKey, openRouterEmbeddingApiKey),
183
+ apiKey: first(process.env.MEMORIX_EMBEDDING_API_KEY, toml.embedding?.api_key, yaml.embedding?.apiKey, legacy.embeddingApi?.apiKey, openRouterEmbeddingApiKey, requestyEmbeddingApiKey),
181
184
  dimensions: firstNumber(parseNumber(process.env.MEMORIX_EMBEDDING_DIMENSIONS), toml.embedding?.dimensions, yaml.embedding?.dimensions, legacy.embeddingApi?.dimensions),
182
185
  },
183
186
  rerank: resolveRerankLane({
@@ -323,6 +326,7 @@ function getEnvSourceNames(): string[] {
323
326
  'MEMORIX_CODEGRAPH_EXTERNAL_COMMAND',
324
327
  'MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS',
325
328
  'OPENROUTER_API_KEY',
329
+ 'REQUESTY_API_KEY',
326
330
  'ATLASCLOUD_API_KEY',
327
331
  ].filter((name) => process.env[name]);
328
332
  }
@@ -337,6 +341,16 @@ function isOpenRouterUrl(value: string | undefined): boolean {
337
341
  }
338
342
  }
339
343
 
344
+ function isRequestyUrl(value: string | undefined): boolean {
345
+ if (!value) return false;
346
+ try {
347
+ const url = new URL(value);
348
+ return url.hostname.toLowerCase() === 'requesty.ai' || url.hostname.toLowerCase().endsWith('.requesty.ai');
349
+ } catch {
350
+ return /(^|\.)requesty\.ai(?::|\/|$)/i.test(value);
351
+ }
352
+ }
353
+
340
354
  function isOpenRouterMemoryLane(provider: string | undefined, baseUrl: string | undefined): boolean {
341
355
  return provider?.trim().toLowerCase() === 'openrouter' || isOpenRouterUrl(baseUrl);
342
356
  }
@@ -25,7 +25,7 @@ import { getGlobalYamlPath, getProjectYamlPath } from './config-paths.js';
25
25
  export interface MemorixYamlConfig {
26
26
  /** LLM provider configuration */
27
27
  llm?: {
28
- provider?: 'openai' | 'anthropic' | 'openrouter' | string;
28
+ provider?: 'openai' | 'anthropic' | 'openrouter' | 'requesty' | string;
29
29
  model?: string;
30
30
  apiKey?: string;
31
31
  baseUrl?: string;
@@ -33,7 +33,7 @@ export interface MemorixYamlConfig {
33
33
 
34
34
  /** TUI / chat agent LLM provider configuration */
35
35
  agent?: {
36
- provider?: 'openai' | 'anthropic' | 'openrouter' | string;
36
+ provider?: 'openai' | 'anthropic' | 'openrouter' | 'requesty' | string;
37
37
  model?: string;
38
38
  apiKey?: string;
39
39
  baseUrl?: string;
package/src/config.ts CHANGED
@@ -93,7 +93,8 @@ export function getEmbeddingModel(): string {
93
93
  /**
94
94
  * Provider-aware embedding model default: OpenRouter serves many providers,
95
95
  * and the OpenAI-only default does not fit it. Qwen3-Embedding-8B is a
96
- * strong multilingual default there; everywhere else keep the OpenAI model.
96
+ * strong multilingual default there. Requesty expects vendor-prefixed ids,
97
+ * so it gets the prefixed OpenAI model. Everywhere else keep the OpenAI model.
97
98
  * An explicitly configured model always wins.
98
99
  */
99
100
  export function defaultEmbeddingModelFor(
@@ -106,6 +107,9 @@ export function defaultEmbeddingModelFor(
106
107
  if (url && (url.hostname === 'openrouter.ai' || url.hostname.endsWith('.openrouter.ai'))) {
107
108
  return 'qwen/qwen3-embedding-8b';
108
109
  }
110
+ if (url && (url.hostname === 'requesty.ai' || url.hostname.endsWith('.requesty.ai'))) {
111
+ return 'openai/text-embedding-3-small';
112
+ }
109
113
  } catch { /* treat unparsable base URLs as non-OpenRouter */ }
110
114
  return 'text-embedding-3-small';
111
115
  }
@@ -682,6 +682,9 @@ async function handleApi(
682
682
  ...(resolved.memory.llm.provider === 'openrouter' || resolved.memory.llm.baseUrl?.includes('openrouter.ai')
683
683
  ? ['OPENROUTER_API_KEY']
684
684
  : []),
685
+ ...(resolved.memory.llm.provider === 'requesty'
686
+ ? ['REQUESTY_API_KEY']
687
+ : []),
685
688
  ];
686
689
  if (llmKey) {
687
690
  const src = resolveDashboardKeySource({ value: llmKey, envKeys: memoryKeyEnv, configSource });
@@ -449,8 +449,10 @@ function generateKiroHookFiles(): Array<{ filename: string; content: string }> {
449
449
  * Format: .opencode/plugins/memorix.js — Bun-compatible JS module
450
450
  * See: https://opencode.ai/docs/plugins/
451
451
  *
452
- * Plugin contract (verified against official docs Apr 2026):
453
- * - Named export: export const MemorixPlugin = async (ctx) => { return { ... } }
452
+ * Plugin contracts:
453
+ * - OpenCode v1: named export `MemorixPlugin` returns an event-hook map.
454
+ * - OpenCode v2: default export contains `id` and `setup(ctx)`, with event
455
+ * subscriptions and tool hooks registered through the context.
454
456
  * - Return object keys are EVENT NAMES (e.g. "session.created", "file.edited")
455
457
  * - Each key maps to an async handler: (input, output) => { ... }
456
458
  * - Session/file/command events: handler receives ({ event }) for event-style hooks
@@ -464,7 +466,7 @@ function generateKiroHookFiles(): Array<{ filename: string; content: string }> {
464
466
  * protocol used by all agents. spawnSync works in both Node.js and Bun
465
467
  * runtimes (OpenCode may fall back to Node.js on Windows).
466
468
  */
467
- const OPENCODE_PLUGIN_VERSION = 7;
469
+ const OPENCODE_PLUGIN_VERSION = 8;
468
470
 
469
471
  const AGENT_SKILL_DIRS: Partial<Record<AgentName, { project: string; global?: string }>> = {
470
472
  cursor: { project: path.join('.cursor', 'skills'), global: path.join('.cursor', 'skills') },
@@ -716,6 +718,161 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
716
718
  },
717
719
  };
718
720
  };
721
+
722
+ /**
723
+ * OpenCode v2 compatibility layer. Keep this beside the v1 export so one
724
+ * generated file can be loaded by both host generations during migration.
725
+ */
726
+ const v2HookCommand = ${hookCommand};
727
+
728
+ function v2ReportHookFailure(eventName, detail) {
729
+ if (process.env.MEMORIX_HOOK_DEBUG !== '1') return;
730
+ console.error('[memorix-plugin] v2 hook delivery failed:', eventName, detail);
731
+ }
732
+
733
+ function v2EventData(event) {
734
+ return event && typeof event.data === 'object' && event.data !== null ? event.data : {};
735
+ }
736
+
737
+ function v2SessionId(event) {
738
+ const data = v2EventData(event);
739
+ const value = data.sessionID ?? data.sessionId ?? data.session_id
740
+ ?? event?.sessionID ?? event?.sessionId;
741
+ return typeof value === 'string' ? value : '';
742
+ }
743
+
744
+ function v2RunHook(payload, directory, sessionId) {
745
+ const nextPayload = { agent: 'opencode', cwd: directory, ...payload };
746
+ if (sessionId) nextPayload.session_id = sessionId;
747
+ const eventName = nextPayload.hook_event_name || 'unknown';
748
+ try {
749
+ const result = spawnSync(v2HookCommand, ['hook'], {
750
+ input: JSON.stringify(nextPayload),
751
+ timeout: 10_000,
752
+ encoding: 'utf-8',
753
+ stdio: ['pipe', 'pipe', 'pipe'],
754
+ shell: process.platform === 'win32',
755
+ windowsHide: true,
756
+ });
757
+ if (result.status !== 0) {
758
+ v2ReportHookFailure(eventName, {
759
+ exit: result.status,
760
+ stderr: (result.stderr || '').slice(0, 200),
761
+ error: result.error?.message,
762
+ });
763
+ }
764
+ } catch (error) {
765
+ v2ReportHookFailure(eventName, error?.message ?? error);
766
+ }
767
+ }
768
+
769
+ function v2SessionState(states, sessionId) {
770
+ const key = sessionId || '__unknown__';
771
+ let state = states.get(key);
772
+ if (!state) {
773
+ state = { pendingText: '', lastDeliveredKey: '' };
774
+ states.set(key, state);
775
+ }
776
+ return state;
777
+ }
778
+
779
+ function v2FlushAssistant(state, directory, sessionId) {
780
+ const text = state.pendingText.trim();
781
+ if (!text || text === state.lastDeliveredKey) return;
782
+ v2RunHook({ hook_event_name: 'message.updated', ai_response: text }, directory, sessionId);
783
+ state.lastDeliveredKey = text;
784
+ state.pendingText = '';
785
+ }
786
+
787
+ function v2FilePath(file) {
788
+ if (typeof file === 'string') return file;
789
+ if (!file || typeof file !== 'object') return '';
790
+ return file.path ?? file.file ?? file.name ?? '';
791
+ }
792
+
793
+ function v2HandleEvent(event, directory, states) {
794
+ if (!event || typeof event !== 'object') return;
795
+ const type = typeof event.type === 'string' ? event.type : '';
796
+ const data = v2EventData(event);
797
+ const sessionId = v2SessionId(event);
798
+ const state = v2SessionState(states, sessionId);
799
+ const run = (payload) => v2RunHook(payload, directory, sessionId);
800
+
801
+ switch (type) {
802
+ case 'session.created':
803
+ run({ hook_event_name: 'session.created' });
804
+ break;
805
+ case 'session.text.delta':
806
+ state.pendingText += typeof data.delta === 'string' ? data.delta : (typeof data.text === 'string' ? data.text : '');
807
+ break;
808
+ case 'session.message.updated':
809
+ case 'message.updated':
810
+ if (data.role === 'assistant' && typeof data.text === 'string') state.pendingText = data.text;
811
+ break;
812
+ case 'session.step.ended': {
813
+ const files = Array.isArray(data.files) ? data.files : [];
814
+ for (const file of files) {
815
+ const filePath = v2FilePath(file);
816
+ if (filePath) run({ hook_event_name: 'file.edited', file_path: filePath });
817
+ }
818
+ if (typeof data.text === 'string') state.pendingText += data.text;
819
+ break;
820
+ }
821
+ case 'session.idle':
822
+ v2FlushAssistant(state, directory, sessionId);
823
+ run({ hook_event_name: 'session.idle' });
824
+ break;
825
+ case 'session.compacted':
826
+ run({ hook_event_name: 'session.compacted' });
827
+ break;
828
+ default:
829
+ break;
830
+ }
831
+ }
832
+
833
+ function setup(ctx) {
834
+ const directory = ctx?.location?.directory || ctx?.location?.worktree || process.cwd();
835
+ const controller = new AbortController();
836
+ const states = new Map();
837
+
838
+ if (ctx?.tool?.hook) {
839
+ try {
840
+ Promise.resolve(ctx.tool.hook('execute.after', async (event) => {
841
+ const sessionId = v2SessionId(event);
842
+ const data = event && typeof event.input === 'object' ? event.input : {};
843
+ const tool = event?.tool ?? event?.name ?? '';
844
+ v2RunHook({
845
+ hook_event_name: 'tool.execute.after',
846
+ tool_name: tool,
847
+ tool_input: data,
848
+ }, directory, sessionId);
849
+ if (tool === 'shell') {
850
+ v2RunHook({
851
+ hook_event_name: 'command.executed',
852
+ command: data.command ?? data.cmd ?? '',
853
+ }, directory, sessionId);
854
+ }
855
+ })).catch((error) => v2ReportHookFailure('execute.after', error?.message ?? error));
856
+ } catch (error) {
857
+ v2ReportHookFailure('execute.after', error?.message ?? error);
858
+ }
859
+ }
860
+
861
+ if (ctx?.event?.subscribe) {
862
+ void (async () => {
863
+ try {
864
+ const stream = ctx.event.subscribe({ signal: controller.signal });
865
+ for await (const event of stream) v2HandleEvent(event, directory, states);
866
+ } catch (error) {
867
+ if (!controller.signal.aborted) v2ReportHookFailure('event.subscribe', error?.message ?? error);
868
+ }
869
+ })();
870
+ }
871
+
872
+ return () => controller.abort();
873
+ }
874
+
875
+ export default { id: 'memorix', setup };
719
876
  `;
720
877
  }
721
878
 
@@ -20,7 +20,7 @@ import {
20
20
  } from '../config.js';
21
21
 
22
22
  export interface LLMConfig {
23
- provider: 'openai' | 'anthropic' | 'openrouter' | 'atlascloud' | 'custom';
23
+ provider: 'openai' | 'anthropic' | 'openrouter' | 'requesty' | 'atlascloud' | 'custom';
24
24
  apiKey: string;
25
25
  model?: string;
26
26
  baseUrl?: string;
@@ -201,6 +201,7 @@ const PROVIDER_DEFAULTS: Record<string, { baseUrl: string; model: string }> = {
201
201
  openai: { baseUrl: 'https://api.openai.com/v1', model: 'gpt-4.1-nano' },
202
202
  anthropic: { baseUrl: 'https://api.anthropic.com/v1', model: 'claude-3-5-haiku-latest' },
203
203
  openrouter: { baseUrl: 'https://openrouter.ai/api/v1', model: 'openai/gpt-4.1-nano' },
204
+ requesty: { baseUrl: 'https://router.requesty.ai/v1', model: 'openai/gpt-4.1-nano' },
204
205
  atlascloud: { baseUrl: 'https://api.atlascloud.ai/v1', model: 'deepseek-ai/deepseek-v3.2' },
205
206
  custom: { baseUrl: 'http://localhost:11434/v1', model: 'llama3' },
206
207
  };
@@ -19,7 +19,7 @@ import {
19
19
  } from './image-payload.js';
20
20
 
21
21
  // Providers that use the OpenAI-compatible /chat/completions Vision endpoint
22
- const OPENAI_COMPATIBLE_PROVIDERS = new Set(['openai', 'openrouter', 'custom']);
22
+ const OPENAI_COMPATIBLE_PROVIDERS = new Set(['openai', 'openrouter', 'requesty', 'custom']);
23
23
  const MAX_VISION_PROMPT_CHARS = 12_000;
24
24
  const MAX_VISION_DESCRIPTION_CHARS = 12_000;
25
25
  const MAX_VISION_LABELS = 50;
@@ -137,7 +137,7 @@ export async function analyzeImage(input: ImageInput): Promise<ImageAnalysisResu
137
137
  const config = getLLMConfig()!;
138
138
  if (!OPENAI_COMPATIBLE_PROVIDERS.has(config.provider)) {
139
139
  throw new Error(
140
- `Image analysis requires an OpenAI-compatible provider (openai, openrouter, or custom). ` +
140
+ `Image analysis requires an OpenAI-compatible provider (openai, openrouter, requesty, or custom). ` +
141
141
  `Current provider "${config.provider}" uses a different API shape. ` +
142
142
  `Set MEMORIX_LLM_PROVIDER=openai or configure an OpenAI-compatible base URL.`,
143
143
  );