@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.
@@ -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/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: [new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href],
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
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.102",
6
6
  "scripts": {
7
7
  "dev": "tsx src/bin.ts",
8
8
  "prepublishOnly": "pnpm generate && pnpm tsc",