@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 +6 -0
- package/dist/index.js +80 -24
- package/dist/tools.js +15 -15
- package/package.json +1 -1
- package/src/index.ts +81 -26
- package/src/tools.ts +15 -15
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 (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
session
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
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)
|
|
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 (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
else
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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 }
|