@mojoaar/opencode-mimir 0.29.1 → 0.29.3

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/dist/index.d.ts CHANGED
@@ -6,6 +6,12 @@ export declare const MAX_TRANSCRIPT = 32000;
6
6
  export declare const MIN_TRANSCRIPT = 500;
7
7
  /** Message text is shaped differently by each provider, so read what we can and skip the rest. */
8
8
  export declare function transcript(messages: unknown[]): string;
9
+ /**
10
+ * The session API hands back message records, not the provider shapes the
11
+ * context hook uses: a user turn keeps its words in `text`, an assistant turn
12
+ * in `content`. System and synthetic records are instructions, not conversation.
13
+ */
14
+ export declare function sessionTranscript(messages: readonly unknown[]): string;
9
15
  /**
10
16
  * One injection point and two tools. The hook is what puts the project's
11
17
  * digest in front of the model before the first turn, so a session starts
package/dist/index.js CHANGED
@@ -20,6 +20,8 @@ export const MIN_TRANSCRIPT = 500;
20
20
  const LEARN_HINT = 'This session was just compacted. Keep what was decided, what broke and any convention, and skip what Mimir already has.';
21
21
  const LEARN_NOW_HINT = 'The user asked for this session to be kept now. Keep what was decided, what broke and any convention, and skip what Mimir already has.';
22
22
  const NOTHING_TO_SEND = 'Nothing worth handing over yet.';
23
+ const ALREADY = 'This session was already handed over.';
24
+ const UNREADABLE = 'Mimir could not read this session, so nothing was sent.';
23
25
  const SENT = 'Mimir is writing what this session settled.';
24
26
  const QUEUE_OFF = 'Mimir took it, but its queue is not running.';
25
27
  const UNREACHABLE = 'Mimir is unreachable, so nothing was sent.';
@@ -52,6 +54,41 @@ function textOf(value) {
52
54
  .join('\n')
53
55
  .trim();
54
56
  }
57
+ /**
58
+ * The session API hands back message records, not the provider shapes the
59
+ * context hook uses: a user turn keeps its words in `text`, an assistant turn
60
+ * in `content`. System and synthetic records are instructions, not conversation.
61
+ */
62
+ export function sessionTranscript(messages) {
63
+ const lines = [];
64
+ for (const message of messages) {
65
+ if (typeof message !== 'object' || message === null)
66
+ continue;
67
+ const record = message;
68
+ if (record.type === 'user') {
69
+ const body = textOf(record.text);
70
+ if (body)
71
+ lines.push(`user: ${body}`);
72
+ }
73
+ else if (record.type === 'assistant') {
74
+ const body = assistantText(record.content);
75
+ if (body)
76
+ lines.push(`assistant: ${body}`);
77
+ }
78
+ }
79
+ return lines.join('\n\n').slice(-MAX_TRANSCRIPT);
80
+ }
81
+ /** Reasoning and tool parts are not conversation, and they are the bulk of it. */
82
+ function assistantText(content) {
83
+ if (!Array.isArray(content))
84
+ return '';
85
+ return content
86
+ .filter((part) => part?.type === 'text')
87
+ .map((part) => part.text)
88
+ .filter((part) => typeof part === 'string')
89
+ .join('\n')
90
+ .trim();
91
+ }
55
92
  /**
56
93
  * One injection point and two tools. The hook is what puts the project's
57
94
  * digest in front of the model before the first turn, so a session starts
@@ -62,8 +99,12 @@ export default Plugin.define({
62
99
  id: 'mimir',
63
100
  async setup(ctx) {
64
101
  const config = resolveConfig(ctx.options);
65
- if (!config)
102
+ if (!config) {
103
+ // This API has no logger, and an inert plugin is otherwise invisible:
104
+ // the digest, the tools and /mimir-learn simply never appear.
105
+ console.warn('mimir: no url, so no digest, tools or /mimir-learn. Set MIMIR_URL and MIMIR_TOKEN, or add options.url and options.tokenFile to the plugin entry.');
66
106
  return;
107
+ }
67
108
  // A folder without a git remote still needs a stable key, or every write
68
109
  // lands in a fresh project. The server maps absolute paths to `local:<hash>`.
69
110
  const repo = detectRepo(ctx.location.directory) ?? ctx.location.directory;
@@ -87,15 +128,9 @@ export default Plugin.define({
87
128
  // The reminder rides the message count. A shrink means a compaction just
88
129
  // happened, which is the worst moment to lose a detail, so it nudges early.
89
130
  const counts = new Map();
90
- // The command below is handed no messages, only a session id, so the hook
91
- // keeps the newest transcript for it. One string per live session, at most
92
- // 32 KB, dropped once it has been sent.
93
- const snapshots = new Map();
94
131
  const nudge = await ctx.session.hook('context', async (event) => {
95
132
  const state = counts.get(event.sessionID) ?? { turns: 0, nudgedAt: 0 };
96
133
  const turns = event.messages?.length ?? 0;
97
- if (turns > state.turns)
98
- snapshots.set(event.sessionID, transcript(event.messages ?? []));
99
134
  const due = turns < state.turns || turns - state.nudgedAt >= NUDGE_EVERY;
100
135
  counts.set(event.sessionID, { turns, nudgedAt: due ? turns : state.nudgedAt });
101
136
  if (due)
@@ -127,30 +162,51 @@ export default Plugin.define({
127
162
  compacted.delete(event.sessionID);
128
163
  });
129
164
  // A manual handover, for the sessions that end before they ever compact.
165
+ // One process hands a session over once, so a second command does not
166
+ // queue the same transcript again.
167
+ const sent = new Set();
130
168
  const command = await ctx.command.transform((editor) => {
131
169
  editor.add({
132
170
  name: 'mimir-learn',
133
171
  description: 'Hand this session to Mimir now.',
134
172
  execute: async ({ sessionID }) => {
135
- const text = snapshots.get(sessionID) ?? '';
136
173
  let notice = NOTHING_TO_SEND;
137
- if (text.length >= MIN_TRANSCRIPT) {
138
- const sent = await callTool('memory_learn', {
139
- repo: repo ?? undefined,
140
- transcript: text,
141
- hint: LEARN_NOW_HINT,
142
- client: 'opencode',
143
- session: sessionID,
144
- }, { ...config, timeoutMs: 4000 }).catch(() => null);
145
- const queued = sent?.ok ? sent.result.queued : undefined;
146
- if (!sent?.ok)
147
- notice = UNREACHABLE;
148
- else if (queued === false)
149
- notice = QUEUE_OFF;
174
+ if (sent.has(sessionID)) {
175
+ notice = ALREADY;
176
+ }
177
+ else {
178
+ let history = null;
179
+ try {
180
+ history = await ctx.session.context({ sessionID });
181
+ }
182
+ catch {
183
+ history = null;
184
+ }
185
+ // The manual command is explicit, so any real text is worth a
186
+ // model call. The 500 character floor is for automatic compaction.
187
+ if (!Array.isArray(history)) {
188
+ notice = UNREADABLE;
189
+ }
150
190
  else {
151
- notice = SENT;
152
- // Kept on failure above, so a retry has something to send.
153
- snapshots.delete(sessionID);
191
+ const text = sessionTranscript(history);
192
+ if (text.trim() !== '') {
193
+ const result = await callTool('memory_learn', {
194
+ repo: repo ?? undefined,
195
+ transcript: text,
196
+ hint: LEARN_NOW_HINT,
197
+ client: 'opencode',
198
+ session: sessionID,
199
+ }, { ...config, timeoutMs: 4000 }).catch(() => null);
200
+ const queued = result?.ok ? result.result.queued : undefined;
201
+ if (!result?.ok)
202
+ notice = UNREACHABLE;
203
+ else if (queued === false)
204
+ notice = QUEUE_OFF;
205
+ else {
206
+ notice = SENT;
207
+ sent.add(sessionID);
208
+ }
209
+ }
154
210
  }
155
211
  }
156
212
  await ctx.session.synthetic({ sessionID, text: notice }).catch(() => null);
package/dist/tools.js CHANGED
@@ -10,11 +10,11 @@ export async function registerTools(ctx, config, repo) {
10
10
  if (config.tools === 'auto' && (await servedByMcp(ctx, config.url)))
11
11
  return IDLE;
12
12
  return ctx.tool.transform((editor) => {
13
- editor.add(digestTool(repo));
14
- editor.add(rememberTool(repo));
15
- editor.add(searchTool(repo));
16
- editor.add(readTool(repo));
17
- editor.add(learnTool(repo));
13
+ editor.add(digestTool(repo, config));
14
+ editor.add(rememberTool(repo, config));
15
+ editor.add(searchTool(repo, config));
16
+ editor.add(readTool(repo, config));
17
+ editor.add(learnTool(repo, config));
18
18
  });
19
19
  }
20
20
  const IDLE = { dispose: async () => { } };
@@ -40,7 +40,7 @@ function sameEndpoint(actual, expected) {
40
40
  function strip(value) {
41
41
  return (value ?? '').replace(/\/+$/, '');
42
42
  }
43
- function digestTool(repo) {
43
+ function digestTool(repo, config) {
44
44
  return {
45
45
  name: 'mimir_digest',
46
46
  description: 'Load what this project already knows from Mimir. Call once at the start of a session, before searching for anything.',
@@ -57,7 +57,7 @@ function digestTool(repo) {
57
57
  },
58
58
  async execute(input) {
59
59
  const args = input;
60
- const result = await callTool('memory_digest', { budget_tokens: Number(args.budget_tokens ?? 800), repo: repo ?? undefined }, { timeoutMs: 5000 });
60
+ const result = await callTool('memory_digest', { budget_tokens: Number(args.budget_tokens ?? 800), repo: repo ?? undefined }, { url: config.url, token: config.token, timeoutMs: 5000 });
61
61
  if (!result.ok)
62
62
  return { content: `Mimir is unavailable (${result.error}). Carry on without it.` };
63
63
  const digest = result.result;
@@ -68,7 +68,7 @@ function digestTool(repo) {
68
68
  // A project created from a repo a teammate already knows about splits the
69
69
  // knowledge in two until someone asks the owner for a grant.
70
70
  const NEW_PROJECT = ' Started a new project. If a teammate already uses this repository, request access at /app/requests on your Mimir install.';
71
- function rememberTool(repo) {
71
+ function rememberTool(repo, config) {
72
72
  return {
73
73
  name: 'mimir_remember',
74
74
  description: 'Store one durable fact in Mimir. Use a path like "deploy/rollback" or "auth/token-rotation"; rewriting the same path replaces what was there.',
@@ -97,7 +97,7 @@ function rememberTool(repo) {
97
97
  repo: repo ?? undefined,
98
98
  client: 'opencode',
99
99
  session: context?.sessionID,
100
- }, { timeoutMs: 8000 });
100
+ }, { url: config.url, token: config.token, timeoutMs: 8000 });
101
101
  if (!result.ok)
102
102
  return { content: `Could not store that (${result.error}).` };
103
103
  const written = result.result;
@@ -108,7 +108,7 @@ function rememberTool(repo) {
108
108
  },
109
109
  };
110
110
  }
111
- function searchTool(repo) {
111
+ function searchTool(repo, config) {
112
112
  return {
113
113
  name: 'mimir_search',
114
114
  description: 'Search what this project has already settled in Mimir. Returns paths, titles and summaries; read a path with the MCP server if you need the body.',
@@ -123,7 +123,7 @@ function searchTool(repo) {
123
123
  },
124
124
  async execute(input) {
125
125
  const args = input;
126
- const result = await callTool('memory_search', { query: typeof args.query === 'string' ? args.query : '', path_prefix: typeof args.path_prefix === 'string' ? args.path_prefix : undefined, repo: repo ?? undefined }, { timeoutMs: 8000 });
126
+ const result = await callTool('memory_search', { query: typeof args.query === 'string' ? args.query : '', path_prefix: typeof args.path_prefix === 'string' ? args.path_prefix : undefined, repo: repo ?? undefined }, { url: config.url, token: config.token, timeoutMs: 8000 });
127
127
  if (!result.ok)
128
128
  return { content: `Mimir is unavailable (${result.error}). Carry on without it.` };
129
129
  const hits = result.result;
@@ -143,7 +143,7 @@ function renderMemory(memory) {
143
143
  const head = day ? `${memory.path ?? ''} · updated ${day}` : (memory.path ?? '');
144
144
  return `${head}\n# ${memory.title ?? ''}\n\n${memory.body ?? ''}`;
145
145
  }
146
- function readTool(repo) {
146
+ function readTool(repo, config) {
147
147
  return {
148
148
  name: 'mimir_read',
149
149
  description: 'Read one memory in full by its path. Search returns paths and summaries; this is the body behind one.',
@@ -155,14 +155,14 @@ function readTool(repo) {
155
155
  },
156
156
  async execute(input) {
157
157
  const args = input;
158
- const result = await callTool('memory_read', { path: String(args.path ?? ''), repo: repo ?? undefined }, { timeoutMs: 8000 });
158
+ const result = await callTool('memory_read', { path: String(args.path ?? ''), repo: repo ?? undefined }, { url: config.url, token: config.token, timeoutMs: 8000 });
159
159
  if (!result.ok)
160
160
  return { content: 'Nothing is stored at that path.' };
161
161
  return { content: renderMemory(result.result) };
162
162
  },
163
163
  };
164
164
  }
165
- function learnTool(repo) {
165
+ function learnTool(repo, config) {
166
166
  return {
167
167
  name: 'mimir_learn',
168
168
  description: 'Hand the part of this conversation worth keeping to Mimir. The project model writes at most three memories, in the background, so this answers at once.',
@@ -183,7 +183,7 @@ function learnTool(repo) {
183
183
  repo: repo ?? undefined,
184
184
  client: 'opencode',
185
185
  session: context?.sessionID,
186
- }, { timeoutMs: 8000 });
186
+ }, { url: config.url, token: config.token, timeoutMs: 8000 });
187
187
  if (!result.ok)
188
188
  return { content: `Could not hand that over (${result.error}).` };
189
189
  const queued = result.result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojoaar/opencode-mimir",
3
- "version": "0.29.1",
3
+ "version": "0.29.3",
4
4
  "description": "Mimir long-term memory for OpenCode sessions",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -26,6 +26,8 @@ const LEARN_HINT =
26
26
  const LEARN_NOW_HINT =
27
27
  'The user asked for this session to be kept now. Keep what was decided, what broke and any convention, and skip what Mimir already has.'
28
28
  const NOTHING_TO_SEND = 'Nothing worth handing over yet.'
29
+ const ALREADY = 'This session was already handed over.'
30
+ const UNREADABLE = 'Mimir could not read this session, so nothing was sent.'
29
31
  const SENT = 'Mimir is writing what this session settled.'
30
32
  const QUEUE_OFF = 'Mimir took it, but its queue is not running.'
31
33
  const UNREACHABLE = 'Mimir is unreachable, so nothing was sent.'
@@ -56,6 +58,38 @@ function textOf(value: unknown): string {
56
58
  .trim()
57
59
  }
58
60
 
61
+ /**
62
+ * The session API hands back message records, not the provider shapes the
63
+ * context hook uses: a user turn keeps its words in `text`, an assistant turn
64
+ * in `content`. System and synthetic records are instructions, not conversation.
65
+ */
66
+ export function sessionTranscript(messages: readonly unknown[]): string {
67
+ const lines: string[] = []
68
+ for (const message of messages) {
69
+ if (typeof message !== 'object' || message === null) continue
70
+ const record = message as { type?: unknown; text?: unknown; content?: unknown }
71
+ if (record.type === 'user') {
72
+ const body = textOf(record.text)
73
+ if (body) lines.push(`user: ${body}`)
74
+ } else if (record.type === 'assistant') {
75
+ const body = assistantText(record.content)
76
+ if (body) lines.push(`assistant: ${body}`)
77
+ }
78
+ }
79
+ return lines.join('\n\n').slice(-MAX_TRANSCRIPT)
80
+ }
81
+
82
+ /** Reasoning and tool parts are not conversation, and they are the bulk of it. */
83
+ function assistantText(content: unknown): string {
84
+ if (!Array.isArray(content)) return ''
85
+ return content
86
+ .filter((part) => (part as { type?: unknown })?.type === 'text')
87
+ .map((part) => (part as { text?: unknown }).text)
88
+ .filter((part): part is string => typeof part === 'string')
89
+ .join('\n')
90
+ .trim()
91
+ }
92
+
59
93
  /**
60
94
  * One injection point and two tools. The hook is what puts the project's
61
95
  * digest in front of the model before the first turn, so a session starts
@@ -66,7 +100,14 @@ export default Plugin.define({
66
100
  id: 'mimir',
67
101
  async setup(ctx) {
68
102
  const config = resolveConfig(ctx.options)
69
- if (!config) return
103
+ if (!config) {
104
+ // This API has no logger, and an inert plugin is otherwise invisible:
105
+ // the digest, the tools and /mimir-learn simply never appear.
106
+ console.warn(
107
+ 'mimir: no url, so no digest, tools or /mimir-learn. Set MIMIR_URL and MIMIR_TOKEN, or add options.url and options.tokenFile to the plugin entry.',
108
+ )
109
+ return
110
+ }
70
111
 
71
112
  // A folder without a git remote still needs a stable key, or every write
72
113
  // lands in a fresh project. The server maps absolute paths to `local:<hash>`.
@@ -97,14 +138,9 @@ export default Plugin.define({
97
138
  // The reminder rides the message count. A shrink means a compaction just
98
139
  // happened, which is the worst moment to lose a detail, so it nudges early.
99
140
  const counts = new Map<string, { turns: number; nudgedAt: number }>()
100
- // The command below is handed no messages, only a session id, so the hook
101
- // keeps the newest transcript for it. One string per live session, at most
102
- // 32 KB, dropped once it has been sent.
103
- const snapshots = new Map<string, string>()
104
141
  const nudge = await ctx.session.hook('context', async (event) => {
105
142
  const state = counts.get(event.sessionID) ?? { turns: 0, nudgedAt: 0 }
106
143
  const turns = event.messages?.length ?? 0
107
- if (turns > state.turns) snapshots.set(event.sessionID, transcript(event.messages ?? []))
108
144
  const due = turns < state.turns || turns - state.nudgedAt >= NUDGE_EVERY
109
145
  counts.set(event.sessionID, { turns, nudgedAt: due ? turns : state.nudgedAt })
110
146
 
@@ -141,32 +177,51 @@ export default Plugin.define({
141
177
  })
142
178
 
143
179
  // A manual handover, for the sessions that end before they ever compact.
180
+ // One process hands a session over once, so a second command does not
181
+ // queue the same transcript again.
182
+ const sent = new Set<string>()
144
183
  const command = await ctx.command.transform((editor) => {
145
184
  editor.add({
146
185
  name: 'mimir-learn',
147
186
  description: 'Hand this session to Mimir now.',
148
187
  execute: async ({ sessionID }) => {
149
- const text = snapshots.get(sessionID) ?? ''
150
188
  let notice = NOTHING_TO_SEND
151
- if (text.length >= MIN_TRANSCRIPT) {
152
- const sent = await callTool(
153
- 'memory_learn',
154
- {
155
- repo: repo ?? undefined,
156
- transcript: text,
157
- hint: LEARN_NOW_HINT,
158
- client: 'opencode',
159
- session: sessionID,
160
- },
161
- { ...config, timeoutMs: 4000 },
162
- ).catch(() => null)
163
- const queued = sent?.ok ? (sent.result as { queued?: boolean }).queued : undefined
164
- if (!sent?.ok) notice = UNREACHABLE
165
- else if (queued === false) notice = QUEUE_OFF
166
- else {
167
- notice = SENT
168
- // Kept on failure above, so a retry has something to send.
169
- snapshots.delete(sessionID)
189
+ if (sent.has(sessionID)) {
190
+ notice = ALREADY
191
+ } else {
192
+ let history: readonly unknown[] | null = null
193
+ try {
194
+ history = await ctx.session.context({ sessionID })
195
+ } catch {
196
+ history = null
197
+ }
198
+
199
+ // The manual command is explicit, so any real text is worth a
200
+ // model call. The 500 character floor is for automatic compaction.
201
+ if (!Array.isArray(history)) {
202
+ notice = UNREADABLE
203
+ } else {
204
+ const text = sessionTranscript(history)
205
+ if (text.trim() !== '') {
206
+ const result = await callTool(
207
+ 'memory_learn',
208
+ {
209
+ repo: repo ?? undefined,
210
+ transcript: text,
211
+ hint: LEARN_NOW_HINT,
212
+ client: 'opencode',
213
+ session: sessionID,
214
+ },
215
+ { ...config, timeoutMs: 4000 },
216
+ ).catch(() => null)
217
+ const queued = result?.ok ? (result.result as { queued?: boolean }).queued : undefined
218
+ if (!result?.ok) notice = UNREACHABLE
219
+ else if (queued === false) notice = QUEUE_OFF
220
+ else {
221
+ notice = SENT
222
+ sent.add(sessionID)
223
+ }
224
+ }
170
225
  }
171
226
  }
172
227
  await ctx.session.synthetic({ sessionID, text: notice }).catch(() => null)
package/src/tools.ts CHANGED
@@ -36,11 +36,11 @@ export async function registerTools(
36
36
  if (config.tools === 'auto' && (await servedByMcp(ctx, config.url))) return IDLE
37
37
 
38
38
  return ctx.tool.transform((editor) => {
39
- editor.add(digestTool(repo))
40
- editor.add(rememberTool(repo))
41
- editor.add(searchTool(repo))
42
- editor.add(readTool(repo))
43
- editor.add(learnTool(repo))
39
+ editor.add(digestTool(repo, config))
40
+ editor.add(rememberTool(repo, config))
41
+ editor.add(searchTool(repo, config))
42
+ editor.add(readTool(repo, config))
43
+ editor.add(learnTool(repo, config))
44
44
  })
45
45
  }
46
46
 
@@ -70,7 +70,7 @@ function strip(value: string | undefined): string {
70
70
  return (value ?? '').replace(/\/+$/, '')
71
71
  }
72
72
 
73
- function digestTool(repo: string | null): ToolDefinition {
73
+ function digestTool(repo: string | null, config: MimirConfig): ToolDefinition {
74
74
  return {
75
75
  name: 'mimir_digest',
76
76
  description:
@@ -91,7 +91,7 @@ function digestTool(repo: string | null): ToolDefinition {
91
91
  const result = await callTool(
92
92
  'memory_digest',
93
93
  { budget_tokens: Number(args.budget_tokens ?? 800), repo: repo ?? undefined },
94
- { timeoutMs: 5000 },
94
+ { url: config.url, token: config.token, timeoutMs: 5000 },
95
95
  )
96
96
  if (!result.ok) return { content: `Mimir is unavailable (${result.error}). Carry on without it.` }
97
97
  const digest = result.result as { text?: string }
@@ -104,7 +104,7 @@ function digestTool(repo: string | null): ToolDefinition {
104
104
  // knowledge in two until someone asks the owner for a grant.
105
105
  const NEW_PROJECT = ' Started a new project. If a teammate already uses this repository, request access at /app/requests on your Mimir install.'
106
106
 
107
- function rememberTool(repo: string | null): ToolDefinition {
107
+ function rememberTool(repo: string | null, config: MimirConfig): ToolDefinition {
108
108
  return {
109
109
  name: 'mimir_remember',
110
110
  description:
@@ -137,7 +137,7 @@ function rememberTool(repo: string | null): ToolDefinition {
137
137
  client: 'opencode',
138
138
  session: context?.sessionID,
139
139
  },
140
- { timeoutMs: 8000 },
140
+ { url: config.url, token: config.token, timeoutMs: 8000 },
141
141
  )
142
142
  if (!result.ok) return { content: `Could not store that (${result.error}).` }
143
143
  const written = result.result as { path?: string; created?: boolean; projectCreated?: boolean }
@@ -149,7 +149,7 @@ function rememberTool(repo: string | null): ToolDefinition {
149
149
  }
150
150
  }
151
151
 
152
- function searchTool(repo: string | null): ToolDefinition {
152
+ function searchTool(repo: string | null, config: MimirConfig): ToolDefinition {
153
153
  return {
154
154
  name: 'mimir_search',
155
155
  description:
@@ -168,7 +168,7 @@ function searchTool(repo: string | null): ToolDefinition {
168
168
  const result = await callTool(
169
169
  'memory_search',
170
170
  { query: typeof args.query === 'string' ? args.query : '', path_prefix: typeof args.path_prefix === 'string' ? args.path_prefix : undefined, repo: repo ?? undefined },
171
- { timeoutMs: 8000 },
171
+ { url: config.url, token: config.token, timeoutMs: 8000 },
172
172
  )
173
173
  if (!result.ok) return { content: `Mimir is unavailable (${result.error}). Carry on without it.` }
174
174
  const hits = result.result as { path: string; title: string; summary: string | null }[]
@@ -189,7 +189,7 @@ function renderMemory(memory: { path?: string; title?: string; body?: string; up
189
189
  return `${head}\n# ${memory.title ?? ''}\n\n${memory.body ?? ''}`
190
190
  }
191
191
 
192
- function readTool(repo: string | null): ToolDefinition {
192
+ function readTool(repo: string | null, config: MimirConfig): ToolDefinition {
193
193
  return {
194
194
  name: 'mimir_read',
195
195
  description:
@@ -205,7 +205,7 @@ function readTool(repo: string | null): ToolDefinition {
205
205
  const result = await callTool(
206
206
  'memory_read',
207
207
  { path: String(args.path ?? ''), repo: repo ?? undefined },
208
- { timeoutMs: 8000 },
208
+ { url: config.url, token: config.token, timeoutMs: 8000 },
209
209
  )
210
210
  if (!result.ok) return { content: 'Nothing is stored at that path.' }
211
211
  return { content: renderMemory(result.result as { path?: string; title?: string; body?: string; updatedAt?: string }) }
@@ -213,7 +213,7 @@ function readTool(repo: string | null): ToolDefinition {
213
213
  }
214
214
  }
215
215
 
216
- function learnTool(repo: string | null): ToolDefinition {
216
+ function learnTool(repo: string | null, config: MimirConfig): ToolDefinition {
217
217
  return {
218
218
  name: 'mimir_learn',
219
219
  description:
@@ -238,7 +238,7 @@ function learnTool(repo: string | null): ToolDefinition {
238
238
  client: 'opencode',
239
239
  session: context?.sessionID,
240
240
  },
241
- { timeoutMs: 8000 },
241
+ { url: config.url, token: config.token, timeoutMs: 8000 },
242
242
  )
243
243
  if (!result.ok) return { content: `Could not hand that over (${result.error}).` }
244
244
  const queued = result.result as { queued?: boolean; reason?: string; projectCreated?: boolean }