@otto-assistant/bridge 0.4.100 → 0.4.102
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/anthropic-auth-plugin.js +227 -178
- package/dist/commands/login.js +6 -4
- package/dist/context-awareness-plugin.js +8 -38
- package/dist/kimaki-opencode-plugin.js +3 -1
- package/dist/memory-overview-plugin.js +126 -0
- package/dist/opencode.js +15 -1
- package/package.json +1 -1
- package/src/anthropic-auth-plugin.ts +574 -453
- package/src/commands/login.ts +6 -4
- package/src/context-awareness-plugin.ts +11 -42
- package/src/kimaki-opencode-plugin.ts +3 -1
- package/src/memory-overview-plugin.ts +161 -0
- package/src/opencode.ts +22 -1
package/dist/commands/login.js
CHANGED
|
@@ -39,6 +39,8 @@ const PROVIDER_POPULARITY_ORDER = [
|
|
|
39
39
|
'xai',
|
|
40
40
|
'groq',
|
|
41
41
|
'deepseek',
|
|
42
|
+
'opencode',
|
|
43
|
+
'opencode-go',
|
|
42
44
|
'mistral',
|
|
43
45
|
'openrouter',
|
|
44
46
|
'fireworks-ai',
|
|
@@ -47,12 +49,12 @@ const PROVIDER_POPULARITY_ORDER = [
|
|
|
47
49
|
'azure',
|
|
48
50
|
'google-vertex',
|
|
49
51
|
'google-vertex-anthropic',
|
|
50
|
-
'cohere',
|
|
52
|
+
// 'cohere',
|
|
51
53
|
'cerebras',
|
|
52
|
-
'perplexity',
|
|
54
|
+
// 'perplexity',
|
|
53
55
|
'cloudflare-workers-ai',
|
|
54
|
-
'novita-ai',
|
|
55
|
-
'huggingface',
|
|
56
|
+
// 'novita-ai',
|
|
57
|
+
// 'huggingface',
|
|
56
58
|
'deepinfra',
|
|
57
59
|
'github-models',
|
|
58
60
|
'lmstudio',
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// OpenCode plugin that injects synthetic message parts for context awareness:
|
|
2
2
|
// - Git branch / detached HEAD changes
|
|
3
3
|
// - Working directory (pwd) changes (e.g. after /new-worktree mid-session)
|
|
4
|
-
// - MEMORY.md table of contents on first message
|
|
5
4
|
// - MEMORY.md reminder after a large assistant reply
|
|
6
5
|
// - Onboarding tutorial instructions (when TUTORIAL_WELCOME_TEXT detected)
|
|
7
6
|
//
|
|
@@ -16,26 +15,13 @@
|
|
|
16
15
|
// Exported from kimaki-opencode-plugin.ts — each export is treated as a separate
|
|
17
16
|
// plugin by OpenCode's plugin loader.
|
|
18
17
|
import crypto from 'node:crypto';
|
|
19
|
-
import fs from 'node:fs';
|
|
20
|
-
import path from 'node:path';
|
|
21
18
|
import * as errore from 'errore';
|
|
22
19
|
import { createPluginLogger, formatPluginErrorWithStack, setPluginLogFilePath, } from './plugin-logger.js';
|
|
23
20
|
import { setDataDir } from './config.js';
|
|
24
21
|
import { initSentry, notifyError } from './sentry.js';
|
|
25
22
|
import { execAsync } from './exec-async.js';
|
|
26
|
-
import { condenseMemoryMd } from './condense-memory.js';
|
|
27
23
|
import { ONBOARDING_TUTORIAL_INSTRUCTIONS, TUTORIAL_WELCOME_TEXT, } from './onboarding-tutorial.js';
|
|
28
24
|
const logger = createPluginLogger('OPENCODE');
|
|
29
|
-
function createSessionState() {
|
|
30
|
-
return {
|
|
31
|
-
gitState: undefined,
|
|
32
|
-
memoryInjected: false,
|
|
33
|
-
lastMemoryReminderAssistantMessageId: undefined,
|
|
34
|
-
tutorialInjected: false,
|
|
35
|
-
resolvedDirectory: undefined,
|
|
36
|
-
announcedDirectory: undefined,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
25
|
// ── Pure derivation functions ────────────────────────────────────
|
|
40
26
|
// These take state + fresh input and return whether to inject.
|
|
41
27
|
// No side effects, no mutations — easy to test with fixtures.
|
|
@@ -66,9 +52,6 @@ export function shouldInjectPwd({ currentDir, previousDir, announcedDir, }) {
|
|
|
66
52
|
};
|
|
67
53
|
}
|
|
68
54
|
const MEMORY_REMINDER_OUTPUT_TOKENS = 12_000;
|
|
69
|
-
function getOutputTokenTotal(tokens) {
|
|
70
|
-
return Math.max(0, tokens.output + tokens.reasoning);
|
|
71
|
-
}
|
|
72
55
|
export function shouldInjectMemoryReminderFromLatestAssistant({ lastMemoryReminderAssistantMessageId, latestAssistantMessage, threshold = MEMORY_REMINDER_OUTPUT_TOKENS, }) {
|
|
73
56
|
if (!latestAssistantMessage) {
|
|
74
57
|
return { inject: false };
|
|
@@ -85,7 +68,7 @@ export function shouldInjectMemoryReminderFromLatestAssistant({ lastMemoryRemind
|
|
|
85
68
|
if (lastMemoryReminderAssistantMessageId === latestAssistantMessage.id) {
|
|
86
69
|
return { inject: false };
|
|
87
70
|
}
|
|
88
|
-
const outputTokens =
|
|
71
|
+
const outputTokens = Math.max(0, latestAssistantMessage.tokens.output + latestAssistantMessage.tokens.reasoning);
|
|
89
72
|
if (outputTokens < threshold) {
|
|
90
73
|
return { inject: false };
|
|
91
74
|
}
|
|
@@ -184,7 +167,13 @@ const contextAwarenessPlugin = async ({ directory, client }) => {
|
|
|
184
167
|
if (existing) {
|
|
185
168
|
return existing;
|
|
186
169
|
}
|
|
187
|
-
const state =
|
|
170
|
+
const state = {
|
|
171
|
+
gitState: undefined,
|
|
172
|
+
lastMemoryReminderAssistantMessageId: undefined,
|
|
173
|
+
tutorialInjected: false,
|
|
174
|
+
resolvedDirectory: undefined,
|
|
175
|
+
announcedDirectory: undefined,
|
|
176
|
+
};
|
|
188
177
|
sessions.set(sessionID, state);
|
|
189
178
|
return state;
|
|
190
179
|
}
|
|
@@ -274,25 +263,6 @@ const contextAwarenessPlugin = async ({ directory, client }) => {
|
|
|
274
263
|
synthetic: true,
|
|
275
264
|
});
|
|
276
265
|
}
|
|
277
|
-
// -- MEMORY.md injection --
|
|
278
|
-
if (!state.memoryInjected) {
|
|
279
|
-
state.memoryInjected = true;
|
|
280
|
-
const memoryPath = path.join(effectiveDirectory, 'MEMORY.md');
|
|
281
|
-
const memoryContent = await fs.promises
|
|
282
|
-
.readFile(memoryPath, 'utf-8')
|
|
283
|
-
.catch(() => null);
|
|
284
|
-
if (memoryContent) {
|
|
285
|
-
const condensed = condenseMemoryMd(memoryContent);
|
|
286
|
-
output.parts.push({
|
|
287
|
-
id: `prt_${crypto.randomUUID()}`,
|
|
288
|
-
sessionID,
|
|
289
|
-
messageID,
|
|
290
|
-
type: 'text',
|
|
291
|
-
text: `<system-reminder>Project memory from MEMORY.md (condensed table of contents, line numbers shown):\n${condensed}\nOnly headings are shown above — section bodies are hidden. Use Grep to search MEMORY.md for specific topics, or Read with offset and limit to read a section's content. When writing to MEMORY.md, keep titles concise (under 10 words) and content brief (2-3 sentences max). Only track non-obvious learnings that prevent future mistakes and are not already documented in code comments or AGENTS.md. Do not duplicate information that is self-evident from the code.</system-reminder>`,
|
|
292
|
-
synthetic: true,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
266
|
const memoryReminder = shouldInjectMemoryReminderFromLatestAssistant({
|
|
297
267
|
lastMemoryReminderAssistantMessageId: state.lastMemoryReminderAssistantMessageId,
|
|
298
268
|
latestAssistantMessage,
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Plugins are split into focused modules:
|
|
7
7
|
// - ipc-tools-plugin: file upload + action buttons (IPC-based Discord tools)
|
|
8
|
-
// - context-awareness-plugin: branch, pwd, memory
|
|
8
|
+
// - context-awareness-plugin: branch, pwd, memory reminder, onboarding tutorial
|
|
9
|
+
// - memory-overview-plugin: frozen MEMORY.md heading overview per session
|
|
9
10
|
// - opencode-interrupt-plugin: interrupt queued messages at step boundaries
|
|
10
11
|
// - kitty-graphics-plugin: extract Kitty Graphics Protocol images from bash output
|
|
11
12
|
export { ipcToolsPlugin } from './ipc-tools-plugin.js';
|
|
12
13
|
export { contextAwarenessPlugin } from './context-awareness-plugin.js';
|
|
14
|
+
export { memoryOverviewPlugin } from './memory-overview-plugin.js';
|
|
13
15
|
export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js';
|
|
14
16
|
export { systemPromptDriftPlugin } from './system-prompt-drift-plugin.js';
|
|
15
17
|
export { anthropicAuthPlugin } from './anthropic-auth-plugin.js';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// OpenCode plugin that snapshots the MEMORY.md heading overview once per
|
|
2
|
+
// session and injects that frozen snapshot on the first real user message.
|
|
3
|
+
// The snapshot is cached by session ID so later MEMORY.md edits do not change
|
|
4
|
+
// the prompt for the same session and do not invalidate OpenCode's cache.
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import * as errore from 'errore';
|
|
9
|
+
import { createPluginLogger, formatPluginErrorWithStack, setPluginLogFilePath, } from './plugin-logger.js';
|
|
10
|
+
import { condenseMemoryMd } from './condense-memory.js';
|
|
11
|
+
import { initSentry, notifyError } from './sentry.js';
|
|
12
|
+
const logger = createPluginLogger('OPENCODE');
|
|
13
|
+
function createSessionState() {
|
|
14
|
+
return {
|
|
15
|
+
hasFrozenOverview: false,
|
|
16
|
+
frozenOverviewText: null,
|
|
17
|
+
injected: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function buildMemoryOverviewReminder({ condensed }) {
|
|
21
|
+
return `<system-reminder>Project memory from MEMORY.md (condensed table of contents, line numbers shown):\n${condensed}\nOnly headings are shown above — section bodies are hidden. Use Grep to search MEMORY.md for specific topics, or Read with offset and limit to read a section's content. When writing to MEMORY.md, keep titles concise (under 10 words) and content brief (2-3 sentences max). Only track non-obvious learnings that prevent future mistakes and are not already documented in code comments or AGENTS.md. Do not duplicate information that is self-evident from the code.</system-reminder>`;
|
|
22
|
+
}
|
|
23
|
+
async function freezeMemoryOverview({ directory, state, }) {
|
|
24
|
+
if (state.hasFrozenOverview) {
|
|
25
|
+
return state.frozenOverviewText;
|
|
26
|
+
}
|
|
27
|
+
const memoryPath = path.join(directory, 'MEMORY.md');
|
|
28
|
+
const memoryContentResult = await fs.promises.readFile(memoryPath, 'utf-8').catch(() => {
|
|
29
|
+
return null;
|
|
30
|
+
});
|
|
31
|
+
if (!memoryContentResult) {
|
|
32
|
+
state.hasFrozenOverview = true;
|
|
33
|
+
state.frozenOverviewText = null;
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const condensed = condenseMemoryMd(memoryContentResult);
|
|
37
|
+
state.hasFrozenOverview = true;
|
|
38
|
+
state.frozenOverviewText = buildMemoryOverviewReminder({ condensed });
|
|
39
|
+
return state.frozenOverviewText;
|
|
40
|
+
}
|
|
41
|
+
const memoryOverviewPlugin = async ({ directory }) => {
|
|
42
|
+
initSentry();
|
|
43
|
+
const dataDir = process.env.KIMAKI_DATA_DIR;
|
|
44
|
+
if (dataDir) {
|
|
45
|
+
setPluginLogFilePath(dataDir);
|
|
46
|
+
}
|
|
47
|
+
const sessions = new Map();
|
|
48
|
+
function getOrCreateSessionState({ sessionID }) {
|
|
49
|
+
const existing = sessions.get(sessionID);
|
|
50
|
+
if (existing) {
|
|
51
|
+
return existing;
|
|
52
|
+
}
|
|
53
|
+
const state = createSessionState();
|
|
54
|
+
sessions.set(sessionID, state);
|
|
55
|
+
return state;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
'chat.message': async (input, output) => {
|
|
59
|
+
const result = await errore.tryAsync({
|
|
60
|
+
try: async () => {
|
|
61
|
+
const state = getOrCreateSessionState({ sessionID: input.sessionID });
|
|
62
|
+
if (state.injected) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const firstPart = output.parts.find((part) => {
|
|
66
|
+
if (part.type !== 'text') {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return part.synthetic !== true;
|
|
70
|
+
});
|
|
71
|
+
if (!firstPart || firstPart.type !== 'text' || firstPart.text.trim().length === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const overviewText = await freezeMemoryOverview({ directory, state });
|
|
75
|
+
state.injected = true;
|
|
76
|
+
if (!overviewText) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
output.parts.push({
|
|
80
|
+
id: `prt_${crypto.randomUUID()}`,
|
|
81
|
+
sessionID: input.sessionID,
|
|
82
|
+
messageID: firstPart.messageID,
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: overviewText,
|
|
85
|
+
synthetic: true,
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
catch: (error) => {
|
|
89
|
+
return new Error('memory overview chat.message hook failed', {
|
|
90
|
+
cause: error,
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
if (!(result instanceof Error)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
logger.warn(`[memory-overview-plugin] ${formatPluginErrorWithStack(result)}`);
|
|
98
|
+
void notifyError(result, 'memory overview plugin chat.message hook failed');
|
|
99
|
+
},
|
|
100
|
+
event: async ({ event }) => {
|
|
101
|
+
const result = await errore.tryAsync({
|
|
102
|
+
try: async () => {
|
|
103
|
+
if (event.type !== 'session.deleted') {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const id = event.properties?.info?.id;
|
|
107
|
+
if (!id) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
sessions.delete(id);
|
|
111
|
+
},
|
|
112
|
+
catch: (error) => {
|
|
113
|
+
return new Error('memory overview event hook failed', {
|
|
114
|
+
cause: error,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
if (!(result instanceof Error)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
logger.warn(`[memory-overview-plugin] ${formatPluginErrorWithStack(result)}`);
|
|
122
|
+
void notifyError(result, 'memory overview plugin event hook failed');
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
export { memoryOverviewPlugin };
|
package/dist/opencode.js
CHANGED
|
@@ -335,6 +335,9 @@ async function startSingleServer() {
|
|
|
335
335
|
const opencodeConfigDir = path
|
|
336
336
|
.join(os.homedir(), '.config', 'opencode')
|
|
337
337
|
.replaceAll('\\', '/');
|
|
338
|
+
const opensrcDir = path
|
|
339
|
+
.join(os.homedir(), '.opensrc')
|
|
340
|
+
.replaceAll('\\', '/');
|
|
338
341
|
const kimakiDataDir = path
|
|
339
342
|
.join(os.homedir(), '.kimaki')
|
|
340
343
|
.replaceAll('\\', '/');
|
|
@@ -349,6 +352,8 @@ async function startSingleServer() {
|
|
|
349
352
|
[`${tmpdir}/*`]: 'allow',
|
|
350
353
|
[opencodeConfigDir]: 'allow',
|
|
351
354
|
[`${opencodeConfigDir}/*`]: 'allow',
|
|
355
|
+
[opensrcDir]: 'allow',
|
|
356
|
+
[`${opensrcDir}/*`]: 'allow',
|
|
352
357
|
[kimakiDataDir]: 'allow',
|
|
353
358
|
[`${kimakiDataDir}/*`]: 'allow',
|
|
354
359
|
};
|
|
@@ -388,11 +393,14 @@ async function startSingleServer() {
|
|
|
388
393
|
// priority chain, so project-level opencode.json can override kimaki defaults.
|
|
389
394
|
// OPENCODE_CONFIG_CONTENT was loaded last and overrode user project configs,
|
|
390
395
|
// causing issue #90 (project permissions not being respected).
|
|
396
|
+
const isDev = import.meta.url.endsWith('.ts') || import.meta.url.endsWith('.tsx');
|
|
391
397
|
const opencodeConfig = {
|
|
392
398
|
$schema: 'https://opencode.ai/config.json',
|
|
393
399
|
lsp: false,
|
|
394
400
|
formatter: false,
|
|
395
|
-
plugin: [
|
|
401
|
+
plugin: [
|
|
402
|
+
new URL(isDev ? './kimaki-opencode-plugin.ts' : './kimaki-opencode-plugin.js', import.meta.url).href,
|
|
403
|
+
],
|
|
396
404
|
permission: {
|
|
397
405
|
edit: 'allow',
|
|
398
406
|
bash: 'allow',
|
|
@@ -668,6 +676,12 @@ export function buildSessionPermissions({ directory, originalRepoDirectory, }) {
|
|
|
668
676
|
.join(os.homedir(), '.config', 'opencode')
|
|
669
677
|
.replaceAll('\\', '/');
|
|
670
678
|
rules.push({ permission: 'external_directory', pattern: opencodeConfigDir, action: 'allow' }, { permission: 'external_directory', pattern: `${opencodeConfigDir}/*`, action: 'allow' });
|
|
679
|
+
// Allow ~/.opensrc so agents can inspect cached opensrc checkouts without
|
|
680
|
+
// permission prompts.
|
|
681
|
+
const opensrcDir = path
|
|
682
|
+
.join(os.homedir(), '.opensrc')
|
|
683
|
+
.replaceAll('\\', '/');
|
|
684
|
+
rules.push({ permission: 'external_directory', pattern: opensrcDir, action: 'allow' }, { permission: 'external_directory', pattern: `${opensrcDir}/*`, action: 'allow' });
|
|
671
685
|
// Allow ~/.kimaki so the agent can access kimaki data dir (logs, db, etc.)
|
|
672
686
|
// without permission prompts.
|
|
673
687
|
const kimakiDataDir = path
|