Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ Check the connection any time:
bunx opencode-supermemory@latest status
```

The installer also enables a persistent `◪ supermemory` footer in OpenCode's
TUI. It turns blue while OpenCode is running a turn and keeps the latest recall
or save activity visible. Native toasts show the same recall, save, failure, and
update events as they happen.

**Or let your agent do it** - paste this into OpenCode:

```
Expand Down
292 changes: 289 additions & 3 deletions bun.lock

Large diffs are not rendered by default.

26 changes: 24 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,22 @@
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./tui": {
"types": "./dist/tui.d.ts",
"import": "./dist/tui.js"
}
},
"bin": {
"opencode-supermemory": "./dist/cli.js"
},
"scripts": {
"generate:version": "node scripts/sync-version.mjs",
"build": "node scripts/sync-version.mjs && bun build ./src/index.ts --outdir ./dist --target node && bun build ./src/cli.ts --outfile ./dist/cli.js --target node && tsc --emitDeclarationOnly",
"build": "node scripts/sync-version.mjs && bun build ./src/index.ts --outdir ./dist --target node && bun build ./src/tui.tsx --outfile ./dist/tui.js --target bun --external @opentui/core --external @opentui/keymap --external @opentui/solid --external solid-js && bun build ./src/cli.ts --outfile ./dist/cli.js --target node && tsc --emitDeclarationOnly",
"dev": "tsc --watch",
"typecheck": "node scripts/sync-version.mjs && tsc --noEmit",
"test": "node scripts/sync-version.mjs && bun test"
Expand All @@ -30,16 +40,28 @@
"url": "https://github.com/supermemoryai/opencode-supermemory"
},
"devDependencies": {
"@opencode-ai/plugin": "^1.0.162",
"@opencode-ai/plugin": "^1.18.20",
"@opentui/core": "^0.5.8",
"@opentui/keymap": "^0.5.8",
"@opentui/solid": "^0.5.8",
"@types/bun": "latest",
"solid-js": "1.9.12",
"supermemory": "^4.0.0",
"typescript": "^5.7.3"
},
"peerDependencies": {
"@opentui/core": ">=0.4.5",
"@opentui/keymap": ">=0.4.5",
"@opentui/solid": ">=0.4.5",
"solid-js": "1.9.12"
},
"opencode": {
"type": "plugin",
"hooks": [
"chat.message",
"permission.ask",
"tool.execute.before",
"tool.execute.after",
"event"
]
},
Expand Down
23 changes: 23 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getTags } from "./services/tags.js";

const OPENCODE_CONFIG_DIR = join(homedir(), ".config", "opencode");
const OPENCODE_COMMAND_DIR = join(OPENCODE_CONFIG_DIR, "command");
const OPENCODE_TUI_CONFIG = join(OPENCODE_CONFIG_DIR, "tui.jsonc");
const OH_MY_OPENCODE_CONFIG = join(OPENCODE_CONFIG_DIR, "oh-my-opencode.json");
const PLUGIN_NAME = "opencode-supermemory@latest";
const DEFAULT_CONFIG_FILE = CONFIG_FILE ?? join(OPENCODE_CONFIG_DIR, "supermemory.json");
Expand Down Expand Up @@ -322,6 +323,26 @@ function createNewConfig(): boolean {
return true;
}

function configureTuiPlugin(): boolean {
const candidates = [
join(OPENCODE_CONFIG_DIR, "tui.jsonc"),
join(OPENCODE_CONFIG_DIR, "tui.json"),
];
const existing = candidates.find((path) => existsSync(path));
if (existing) return addPluginToConfig(existing);

mkdirSync(OPENCODE_CONFIG_DIR, { recursive: true });
writeFileSync(
OPENCODE_TUI_CONFIG,
`{
"plugin": ["${PLUGIN_NAME}"]
}
`,
);
console.log(`✓ Enabled persistent Supermemory footer in ${OPENCODE_TUI_CONFIG}`);
return true;
}

function createCommands(): boolean {
mkdirSync(OPENCODE_COMMAND_DIR, { recursive: true });

Expand Down Expand Up @@ -433,6 +454,8 @@ async function install(options: InstallOptions): Promise<number> {
}
}

configureTuiPlugin();

// Step 2: Create commands
console.log("\nStep 2: Create /supermemory-init, /supermemory-login, /supermemory-logout, and /supermemory-status commands");
if (options.tui) {
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,13 @@ export const SupermemoryPlugin: Plugin = async (ctx: PluginInput) => {
type: "text",
text: directRecall.context,
synthetic: true,
metadata: {
supermemory: {
activity: "recalled",
count: directRecall.count,
tokens: directRecall.tokens,
},
},
});
}

Expand Down
96 changes: 96 additions & 0 deletions src/tui.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/** @jsxImportSource @opentui/solid */
import type {
TuiPlugin,
TuiPluginModule,
} from "@opencode-ai/plugin/tui";
import { createSignal } from "solid-js";

const DEFAULT_ACTIVITY = "ready";

interface RecallActivity {
count: number;
tokens: number;
}

function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object"
? (value as Record<string, unknown>)
: null;
}

export function getRecallActivity(part: unknown): RecallActivity | null {
const value = asRecord(part);
if (!value || value.type !== "text") return null;

const metadata = asRecord(value.metadata);
const supermemory = asRecord(metadata?.supermemory);
if (supermemory?.activity === "recalled") {
const count = supermemory.count;
const tokens = supermemory.tokens;
if (typeof count === "number" && typeof tokens === "number") {
return { count, tokens };
}
}

const text = typeof value.text === "string" ? value.text : "";
if (!text.includes("<supermemory-context>")) return null;
const count = text.split("\n").filter((line) => line.startsWith("- ◪ ")).length;
return count > 0
? { count, tokens: Math.round(text.length / 4) }
: null;
}

function recallLabel(activity: RecallActivity): string {
return `recalled ${activity.count} ${activity.count === 1 ? "memory" : "memories"} (${activity.tokens} tok)`;
}

const tui: TuiPlugin = async (api) => {
const busySessions = new Set<string>();
const [running, setRunning] = createSignal(false);
const [activity, setActivity] = createSignal(DEFAULT_ACTIVITY);

api.event.on("session.status", (event) => {
const { sessionID, status } = event.properties;
if (status.type === "busy" || status.type === "retry") {
busySessions.add(sessionID);
} else {
busySessions.delete(sessionID);
}
setRunning(busySessions.size > 0);
});

api.event.on("session.idle", (event) => {
busySessions.delete(event.properties.sessionID);
setRunning(busySessions.size > 0);
});

api.event.on("message.part.updated", (event) => {
const recalled = getRecallActivity(event.properties.part);
if (recalled) setActivity(recallLabel(recalled));
});

api.event.on("tui.toast.show", (event) => {
const message = event.properties.message;
if (!message.startsWith("◪ supermemory · ")) return;
setActivity(message.slice("◪ supermemory · ".length));
});

api.slots.register({
slots: {
app_bottom: () => (
<box paddingLeft={1} flexDirection="row">
<text fg={running() ? api.theme.current.info : api.theme.current.success}>
{`◪ supermemory · ${running() ? "running" : activity()}`}
</text>
</box>
),
},
});
};

const plugin: TuiPluginModule & { id: string } = {
id: "supermemory.status",
tui,
};

export default plugin;
2 changes: 2 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"moduleResolution": "bundler",
"allowImportingTsExtensions": false,
"verbatimModuleSyntax": true,
"jsx": "react-jsx",
"jsxImportSource": "@opentui/solid",

// Output
"outDir": "./dist",
Expand Down
Loading