@otto-assistant/bridge 0.4.100 → 0.4.101

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.
@@ -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 = getOutputTokenTotal(latestAssistantMessage.tokens);
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 = createSessionState();
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, time gap, onboarding tutorial
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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@otto-assistant/bridge",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.100",
5
+ "version": "0.4.101",
6
6
  "scripts": {
7
7
  "dev": "tsx src/bin.ts",
8
8
  "prepublishOnly": "pnpm generate && pnpm tsc",