memorix 1.9.8 → 1.9.10
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/CHANGELOG.md +17 -0
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/cli/index.js +125 -31
- package/dist/index.js +117 -31
- package/dist/maintenance-runner.js +22 -5
- package/dist/media-audio-runner.js +22 -5
- package/dist/media-video-runner.js +22 -5
- package/dist/memcode-runtime/CHANGELOG.md +17 -0
- package/dist/sdk.js +116 -30
- package/dist/vector-backfill-runner.js +22 -5
- package/docs/CONFIGURATION.md +22 -0
- package/docs/DEVELOPMENT.md +1 -1
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/README.md +1 -1
- package/docs/SETUP.md +1 -1
- package/package.json +1 -1
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/init.ts +7 -1
- package/src/cli/tui/ConfigureView.tsx +2 -0
- package/src/config/dotenv-loader.ts +1 -0
- package/src/config/resolved-config.ts +19 -5
- package/src/config/yaml-loader.ts +2 -2
- package/src/config.ts +5 -1
- package/src/dashboard/server.ts +3 -0
- package/src/hooks/installers/index.ts +89 -21
- package/src/llm/provider.ts +2 -1
- package/src/multimodal/image-loader.ts +2 -2
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -13,7 +13,7 @@ Memorix is a TypeScript project built around:
|
|
|
13
13
|
|
|
14
14
|
## Current Development Baseline
|
|
15
15
|
|
|
16
|
-
The current release line is **1.9.
|
|
16
|
+
The current release line is **1.9.10** and package metadata is kept synchronized
|
|
17
17
|
with the root package and MCP Registry manifest.
|
|
18
18
|
|
|
19
19
|
Contributors should assume the following areas are part of the current release line:
|
package/docs/INTEGRATIONS.md
CHANGED
|
@@ -55,7 +55,7 @@ Generated guidance also has scope:
|
|
|
55
55
|
| Cursor | `memorix setup --agent cursor --global` | Cursor MCP and rules config | Cursor MCP config, `.cursor/rules/memorix.mdc`, skills, and hook guidance | Reload Cursor after setup so it can pick up project config changes. |
|
|
56
56
|
| Grok Build | `memorix setup --agent grok --global` | `~/.grok/hooks/memorix.json` + `~/.grok/AGENTS.md` | Lifecycle hooks on SessionStart, SessionEnd, UserPromptSubmit, PostToolUse, PostToolUseFailure, PreCompact, PostCompact, Stop, and StopFailure, plus Grok project/global guidance | Uses Grok's own JSON hook directory. MCP stays host-owned unless you pass `--mcp http`, which writes `[mcp_servers.memorix]` with `url = "http://localhost:3211/mcp"` in Grok `config.toml`. Global hooks are trusted; project hooks require Grok `/hooks-trust`. Set `GROK_HOME` to use a custom Grok home. The command is `memorix hook --agent grok`. |
|
|
57
57
|
| Gemini CLI | `memorix setup --agent gemini-cli --global` | Gemini CLI extension | Extension under `~/.gemini/extensions/memorix` with MCP, `GEMINI.md` context, hooks, commands, and skills | Antigravity CLI has an official Gemini CLI migration path, but Gemini CLI remains an active standalone Google CLI target. |
|
|
58
|
-
| OpenCode | `memorix setup --agent opencode --global` | OpenCode local plugin file | Global setup writes `~/.config/opencode/plugins/memorix.js`, `~/.config/opencode/opencode.json`, skills, and `AGENTS.md` guidance; repo-local setup writes the `.opencode/` equivalents. | The generated plugin keeps the v1 named export and
|
|
58
|
+
| OpenCode | `memorix setup --agent opencode --global` | OpenCode local plugin file | Global setup writes `~/.config/opencode/plugins/memorix.js`, `~/.config/opencode/opencode.json`, skills, and `AGENTS.md` guidance; repo-local setup writes the `.opencode/` equivalents. | The generated plugin keeps the v1 named export, adds the v1 `server` field to its dual default export, and exposes the v2 `id`/`setup` contract. OpenCode 1.x lifecycle/file/command/message events use the generic `event` hook; v2 subscriptions use the host session ID when forwarding hooks. |
|
|
59
59
|
| OpenClaw | `memorix setup --agent openclaw --global` | OpenClaw compatible bundle | `~/.openclaw/extensions/memorix` with `.mcp.json`, skills, and OpenClaw `HOOK.md`/`handler.ts` hook pack | Setup attempts `openclaw plugins install <path> --force` and `openclaw hooks enable memorix` when OpenClaw is available. |
|
|
60
60
|
| Hermes Agent | `memorix setup --agent hermes --global` | Hermes plugin | Hermes home (`%LOCALAPPDATA%\hermes` on native Windows, `~/.hermes` elsewhere, or `HERMES_HOME`), plugin enablement in `config.yaml`, and `mcp_servers.memorix` | The plugin registers hooks, a slash command, a CLI command, and skills through Hermes' plugin context. Existing YAML keys are preserved. |
|
|
61
61
|
| Oh-my-Pi | `memorix setup --agent omp --global` | Oh-my-Pi `omp.extensions` package | `.omp/packages/memorix` or `~/.omp/agent/packages/memorix`, plus `.omp/mcp.json` or `~/.omp/agent/mcp.json` | Setup attempts `omp plugin link <path>`. This target uses Oh-my-Pi's package/extension manifest, not Pi's legacy package manifest. |
|
package/docs/README.md
CHANGED
|
@@ -126,7 +126,7 @@ Historical/deep-reference documents may describe older designs. If they conflict
|
|
|
126
126
|
|
|
127
127
|
## Current Product Line
|
|
128
128
|
|
|
129
|
-
The current product line is **1.9.
|
|
129
|
+
The current product line is **1.9.10**. Multi-device store sync is opt-in and
|
|
130
130
|
keeps local SQLite canonical; existing users do not upload anything until they
|
|
131
131
|
configure a relay provider explicitly.
|
|
132
132
|
The authoritative acceptance contract for the current implementation is
|
package/docs/SETUP.md
CHANGED
|
@@ -502,7 +502,7 @@ OpenCode can also use a local Memorix plugin generated by setup:
|
|
|
502
502
|
memorix setup --agent opencode --global
|
|
503
503
|
```
|
|
504
504
|
|
|
505
|
-
This writes the OpenCode plugin, skills, MCP config, and `AGENTS.md` guidance in the selected scope. The generated plugin exports
|
|
505
|
+
This writes the OpenCode plugin, skills, MCP config, and `AGENTS.md` guidance in the selected scope. The generated plugin exports the legacy v1 hook function and a dual default object with the v1 `server` field plus the v2 `id`/`setup` contract, so OpenCode v1 and v2 can load the same file during the migration window. OpenCode 1.x events are received through its generic `event` hook.
|
|
506
506
|
|
|
507
507
|
Antigravity setup installs a native plugin at `~/.gemini/config/plugins/memorix` for global setup or `.agents/plugins/memorix` for workspace setup. The plugin includes stdio MCP, official hooks, rules, and skills:
|
|
508
508
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memorix",
|
|
3
3
|
"mcpName": "io.github.AVIDS2/memorix",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.10",
|
|
5
5
|
"description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
package/server.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"url": "https://github.com/AVIDS2/memorix",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.9.
|
|
10
|
+
"version": "1.9.10",
|
|
11
11
|
"websiteUrl": "https://mem.rglens.com",
|
|
12
12
|
"icons": [
|
|
13
13
|
{
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "memorix",
|
|
25
|
-
"version": "1.9.
|
|
25
|
+
"version": "1.9.10",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"packageArguments": [
|
|
28
28
|
{
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type InitScope,
|
|
22
22
|
} from './init-shared.js';
|
|
23
23
|
|
|
24
|
-
type InitLlmProvider = 'none' | 'openai' | 'anthropic' | 'openrouter' | 'atlascloud' | 'custom';
|
|
24
|
+
type InitLlmProvider = 'none' | 'openai' | 'anthropic' | 'openrouter' | 'requesty' | 'atlascloud' | 'custom';
|
|
25
25
|
type InitEmbeddingProvider = 'off' | 'api' | 'fastembed';
|
|
26
26
|
type InitInjectMode = 'minimal' | 'full' | 'silent';
|
|
27
27
|
|
|
@@ -103,6 +103,7 @@ export default defineCommand({
|
|
|
103
103
|
{ value: 'openai', label: 'OpenAI', hint: 'gpt-4o-mini' },
|
|
104
104
|
{ value: 'anthropic', label: 'Anthropic', hint: 'claude-3-haiku' },
|
|
105
105
|
{ value: 'openrouter', label: 'OpenRouter', hint: 'multi-provider' },
|
|
106
|
+
{ value: 'requesty', label: 'Requesty', hint: 'multi-provider' },
|
|
106
107
|
{ value: 'atlascloud', label: 'Atlas Cloud', hint: 'DeepSeek V3.2' },
|
|
107
108
|
{ value: 'custom', label: 'Custom', hint: 'OpenAI-compatible endpoint' },
|
|
108
109
|
],
|
|
@@ -169,6 +170,8 @@ export default defineCommand({
|
|
|
169
170
|
envLines.push('MEMORIX_LLM_API_KEY=sk-ant-your-key-here');
|
|
170
171
|
} else if (llmProvider === 'openrouter') {
|
|
171
172
|
envLines.push('MEMORIX_LLM_API_KEY=sk-or-your-key-here');
|
|
173
|
+
} else if (llmProvider === 'requesty') {
|
|
174
|
+
envLines.push('MEMORIX_LLM_API_KEY=rqsty-your-key-here');
|
|
172
175
|
}
|
|
173
176
|
if (llmProvider === 'custom') {
|
|
174
177
|
envLines.push('# MEMORIX_LLM_BASE_URL=http://localhost:11434/v1');
|
|
@@ -197,6 +200,7 @@ export default defineCommand({
|
|
|
197
200
|
envLines.push('# OPENAI_API_KEY=sk-...');
|
|
198
201
|
envLines.push('# ANTHROPIC_API_KEY=sk-ant-...');
|
|
199
202
|
envLines.push('# OPENROUTER_API_KEY=sk-or-...');
|
|
203
|
+
envLines.push('# REQUESTY_API_KEY=rqsty-...');
|
|
200
204
|
envLines.push('');
|
|
201
205
|
|
|
202
206
|
mkdirSync(targetDir, { recursive: true });
|
|
@@ -293,6 +297,8 @@ export function buildInitTomlConfig(options: {
|
|
|
293
297
|
lines.push('model = "claude-3-haiku-20240307"');
|
|
294
298
|
} else if (options.llmProvider === 'openrouter') {
|
|
295
299
|
lines.push('# model = "openai/gpt-4o-mini"');
|
|
300
|
+
} else if (options.llmProvider === 'requesty') {
|
|
301
|
+
lines.push('# model = "openai/gpt-4o-mini"');
|
|
296
302
|
} else if (options.llmProvider === 'atlascloud') {
|
|
297
303
|
lines.push('model = "deepseek-ai/deepseek-v3.2"');
|
|
298
304
|
}
|
|
@@ -166,6 +166,7 @@ export function ConfigureView({ onBack }: ConfigureViewProps): React.ReactElemen
|
|
|
166
166
|
{ key: 'openai', label: 'OpenAI', hint: 'gpt-4o-mini' },
|
|
167
167
|
{ key: 'anthropic', label: 'Anthropic', hint: 'claude-3-haiku' },
|
|
168
168
|
{ key: 'openrouter', label: 'OpenRouter', hint: 'multi-provider' },
|
|
169
|
+
{ key: 'requesty', label: 'Requesty', hint: 'multi-provider' },
|
|
169
170
|
{ key: 'atlascloud', label: 'Atlas Cloud', hint: 'DeepSeek V3.2' },
|
|
170
171
|
{ key: 'custom', label: 'Custom endpoint', hint: 'OpenAI-compatible' },
|
|
171
172
|
{ key: 'disable', label: 'Disable LLM', hint: 'free heuristic mode', color: COLORS.warning },
|
|
@@ -447,6 +448,7 @@ export function ConfigureView({ onBack }: ConfigureViewProps): React.ReactElemen
|
|
|
447
448
|
function getDefaultModel(provider: string): string {
|
|
448
449
|
if (provider === 'anthropic') return 'claude-3-haiku-20240307';
|
|
449
450
|
if (provider === 'openrouter') return 'openai/gpt-4o-mini';
|
|
451
|
+
if (provider === 'requesty') return 'openai/gpt-4o-mini';
|
|
450
452
|
if (provider === 'atlascloud') return 'deepseek-ai/deepseek-v3.2';
|
|
451
453
|
return 'gpt-4o-mini';
|
|
452
454
|
}
|
|
@@ -143,6 +143,7 @@ export function getLoadedEnvValue(key: string): string | undefined {
|
|
|
143
143
|
// OPENAI_API_KEY — OpenAI compatibility fallback
|
|
144
144
|
// ANTHROPIC_API_KEY — Anthropic compatibility fallback
|
|
145
145
|
// OPENROUTER_API_KEY — OpenRouter compatibility fallback
|
|
146
|
+
// REQUESTY_API_KEY - Requesty memory preset and embedding endpoint key
|
|
146
147
|
// MINIMAX_API_KEY — MiniMax global image/video generation key
|
|
147
148
|
// MINIMAX_CN_API_KEY — MiniMax China-region image/video generation key
|
|
148
149
|
// MINIMAX_REGION — MiniMax region: global or cn
|
|
@@ -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
|
|
128
|
-
isAtlasCloud ? process.env.ATLASCLOUD_API_KEY : process.env.OPENAI_API_KEY,
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
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
|
}
|
package/src/dashboard/server.ts
CHANGED
|
@@ -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 });
|
|
@@ -466,7 +466,7 @@ function generateKiroHookFiles(): Array<{ filename: string; content: string }> {
|
|
|
466
466
|
* protocol used by all agents. spawnSync works in both Node.js and Bun
|
|
467
467
|
* runtimes (OpenCode may fall back to Node.js on Windows).
|
|
468
468
|
*/
|
|
469
|
-
const OPENCODE_PLUGIN_VERSION =
|
|
469
|
+
const OPENCODE_PLUGIN_VERSION = 9;
|
|
470
470
|
|
|
471
471
|
const AGENT_SKILL_DIRS: Partial<Record<AgentName, { project: string; global?: string }>> = {
|
|
472
472
|
cursor: { project: path.join('.cursor', 'skills'), global: path.join('.cursor', 'skills') },
|
|
@@ -537,6 +537,8 @@ function generateOpenCodePlugin(): string {
|
|
|
537
537
|
*/
|
|
538
538
|
import { spawnSync } from 'node:child_process';
|
|
539
539
|
|
|
540
|
+
const HOOK_TIMEOUT_MS = 30_000;
|
|
541
|
+
|
|
540
542
|
export const MemorixPlugin = async ({ project, client, $, directory, worktree }) => {
|
|
541
543
|
// Generate a stable session ID for this plugin lifetime
|
|
542
544
|
const sessionId = \`opencode-\${Date.now().toString(36)}-\${Math.random().toString(36).slice(2, 8)}\`;
|
|
@@ -562,14 +564,14 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
|
|
|
562
564
|
* - spawnSync is simpler: no stream lifecycle, no writer.close() bugs
|
|
563
565
|
* - stdin pipe via input option is reliable cross-platform
|
|
564
566
|
*/
|
|
565
|
-
function runHook(payload) {
|
|
566
|
-
payload.session_id =
|
|
567
|
+
function runHook(payload, activeSessionId = sessionId) {
|
|
568
|
+
payload.session_id = activeSessionId;
|
|
567
569
|
const data = JSON.stringify(payload);
|
|
568
570
|
const eventName = payload.hook_event_name || 'unknown';
|
|
569
571
|
try {
|
|
570
572
|
const result = spawnSync(hookCommand, ['hook'], {
|
|
571
573
|
input: data,
|
|
572
|
-
timeout:
|
|
574
|
+
timeout: HOOK_TIMEOUT_MS,
|
|
573
575
|
encoding: 'utf-8',
|
|
574
576
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
575
577
|
shell: process.platform === 'win32',
|
|
@@ -614,7 +616,87 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
|
|
|
614
616
|
.trim();
|
|
615
617
|
}
|
|
616
618
|
|
|
619
|
+
function flushAssistantResponse(activeSessionId = sessionId) {
|
|
620
|
+
if (!pendingAssistantResponse?.text) return;
|
|
621
|
+
const deliveryKey = pendingAssistantResponse.id
|
|
622
|
+
? \`\${pendingAssistantResponse.id}:\${pendingAssistantResponse.text}\`
|
|
623
|
+
: pendingAssistantResponse.text;
|
|
624
|
+
if (deliveryKey === lastDeliveredAssistantKey) return;
|
|
625
|
+
runHook({
|
|
626
|
+
agent: 'opencode',
|
|
627
|
+
hook_event_name: 'message.updated',
|
|
628
|
+
ai_response: pendingAssistantResponse.text,
|
|
629
|
+
message_id: pendingAssistantResponse.id,
|
|
630
|
+
cwd: directory,
|
|
631
|
+
}, activeSessionId);
|
|
632
|
+
lastDeliveredAssistantKey = deliveryKey;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function eventSessionId(event) {
|
|
636
|
+
const properties = event?.properties ?? {};
|
|
637
|
+
const value = properties.sessionID ?? properties.sessionId ?? properties.info?.id;
|
|
638
|
+
return typeof value === 'string' && value ? value : sessionId;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function handleEvent({ event }) {
|
|
642
|
+
const properties = event?.properties ?? {};
|
|
643
|
+
const activeSessionId = eventSessionId(event);
|
|
644
|
+
switch (event?.type) {
|
|
645
|
+
case 'session.created':
|
|
646
|
+
runHook({
|
|
647
|
+
agent: 'opencode',
|
|
648
|
+
hook_event_name: 'session.created',
|
|
649
|
+
cwd: directory,
|
|
650
|
+
}, activeSessionId);
|
|
651
|
+
break;
|
|
652
|
+
case 'session.idle':
|
|
653
|
+
flushAssistantResponse(activeSessionId);
|
|
654
|
+
runHook({
|
|
655
|
+
agent: 'opencode',
|
|
656
|
+
hook_event_name: 'session.idle',
|
|
657
|
+
cwd: directory,
|
|
658
|
+
}, activeSessionId);
|
|
659
|
+
break;
|
|
660
|
+
case 'file.edited':
|
|
661
|
+
runHook({
|
|
662
|
+
agent: 'opencode',
|
|
663
|
+
hook_event_name: 'file.edited',
|
|
664
|
+
file_path: properties.file ?? properties.path ?? '',
|
|
665
|
+
cwd: directory,
|
|
666
|
+
}, activeSessionId);
|
|
667
|
+
break;
|
|
668
|
+
case 'command.executed':
|
|
669
|
+
runHook({
|
|
670
|
+
agent: 'opencode',
|
|
671
|
+
hook_event_name: 'command.executed',
|
|
672
|
+
command: properties.name ?? properties.command ?? '',
|
|
673
|
+
cwd: directory,
|
|
674
|
+
}, activeSessionId);
|
|
675
|
+
break;
|
|
676
|
+
case 'message.updated': {
|
|
677
|
+
const message = properties.info ?? properties.message ?? properties;
|
|
678
|
+
if (message?.role !== 'assistant') break;
|
|
679
|
+
const text = extractMessageText(message);
|
|
680
|
+
if (!text) break;
|
|
681
|
+
pendingAssistantResponse = { id: message.id, text };
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
case 'session.compacted':
|
|
685
|
+
runHook({
|
|
686
|
+
agent: 'opencode',
|
|
687
|
+
hook_event_name: 'session.compacted',
|
|
688
|
+
cwd: directory,
|
|
689
|
+
}, activeSessionId);
|
|
690
|
+
break;
|
|
691
|
+
default:
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
617
696
|
return {
|
|
697
|
+
// OpenCode 1.x delivers lifecycle/file/command/message events through this
|
|
698
|
+
// generic hook. The named keys below remain for older compatible hosts.
|
|
699
|
+
event: handleEvent,
|
|
618
700
|
/** Session created — record session start */
|
|
619
701
|
'session.created': async ({ session }) => {
|
|
620
702
|
runHook({
|
|
@@ -626,21 +708,7 @@ export const MemorixPlugin = async ({ project, client, $, directory, worktree })
|
|
|
626
708
|
|
|
627
709
|
/** Session idle — record session end */
|
|
628
710
|
'session.idle': async ({ session }) => {
|
|
629
|
-
|
|
630
|
-
const deliveryKey = pendingAssistantResponse.id
|
|
631
|
-
? \`\${pendingAssistantResponse.id}:\${pendingAssistantResponse.text}\`
|
|
632
|
-
: pendingAssistantResponse.text;
|
|
633
|
-
if (deliveryKey !== lastDeliveredAssistantKey) {
|
|
634
|
-
runHook({
|
|
635
|
-
agent: 'opencode',
|
|
636
|
-
hook_event_name: 'message.updated',
|
|
637
|
-
ai_response: pendingAssistantResponse.text,
|
|
638
|
-
message_id: pendingAssistantResponse.id,
|
|
639
|
-
cwd: directory,
|
|
640
|
-
});
|
|
641
|
-
lastDeliveredAssistantKey = deliveryKey;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
711
|
+
flushAssistantResponse();
|
|
644
712
|
runHook({
|
|
645
713
|
agent: 'opencode',
|
|
646
714
|
hook_event_name: 'session.idle',
|
|
@@ -748,7 +816,7 @@ function v2RunHook(payload, directory, sessionId) {
|
|
|
748
816
|
try {
|
|
749
817
|
const result = spawnSync(v2HookCommand, ['hook'], {
|
|
750
818
|
input: JSON.stringify(nextPayload),
|
|
751
|
-
timeout:
|
|
819
|
+
timeout: HOOK_TIMEOUT_MS,
|
|
752
820
|
encoding: 'utf-8',
|
|
753
821
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
754
822
|
shell: process.platform === 'win32',
|
|
@@ -872,7 +940,7 @@ function setup(ctx) {
|
|
|
872
940
|
return () => controller.abort();
|
|
873
941
|
}
|
|
874
942
|
|
|
875
|
-
export default { id: 'memorix', setup };
|
|
943
|
+
export default { id: 'memorix', server: MemorixPlugin, setup };
|
|
876
944
|
`;
|
|
877
945
|
}
|
|
878
946
|
|
package/src/llm/provider.ts
CHANGED
|
@@ -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
|
);
|