Skip to content
Merged
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
16 changes: 11 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,19 @@ jobs:
- run: npm ci

- name: Bump patch version
env:
BEFORE_REF: ${{ github.event.before }}
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
npm version patch --no-git-tag-version
VERSION="$(node -p "require('./package.json').version")"
git add package.json package-lock.json
git commit -m "chore(release): ${VERSION}"
PREVIOUS_VERSION="$(git show "${BEFORE_REF}:package.json" | node -e 'let text=""; process.stdin.on("data", chunk => text += chunk); process.stdin.on("end", () => console.log(JSON.parse(text).version))')"
if [ "$VERSION" = "$PREVIOUS_VERSION" ]; then
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
npm version patch --no-git-tag-version
VERSION="$(node -p "require('./package.json').version")"
git add package.json package-lock.json
git commit -m "chore(release): ${VERSION}"
fi

# Build after the bump so the embedded PLUGIN_VERSION matches the publish.
- run: npm run build --if-present
Expand Down
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Changelog

## 1.0.20

- Use the official Supermemory 5.0.1 SDK for hosted capture, profile, search, and
connectivity checks; rebuild standalone hooks and status with the SDK included.
- Preserve existing namespace strings, configuration, credentials, session IDs,
metadata, cursors, recall budgets, fan-out, and MCP routing/approvals.
- Retain an exact-pinned SDK4 compatibility path for older custom servers and
legacy-only ancillary operations. Support explicit `v5`/`legacy` selection
without failed-request fallback or hosted rerouting.
- Validate document acceptance before committing a capture cursor and preserve
POST append/diff semantics for subsequent session deltas.
- Include SDK licenses with both npm artifacts and installed standalone scripts.
- Respect a PR-supplied release version instead of bumping it a second time;
ordinary main changes still trigger the publisher's next patch bump.
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,60 @@ write destination. Older user/personal overrides remain in the legacy read set.

## Configuration

### API v5 and existing installations

Version 1.0.20 bundles the official `supermemory@5.0.1` SDK for document capture,
profiles, search, and the status connectivity probe. Existing container-tag
strings become namespace paths without renaming stored data. Config keys,
credential files, session document IDs, capture cursors, legacy namespace reads,
and recall approvals stay in place; no historical backfill or namespace move runs.
Re-run `npx codex-supermemory@latest install` after upgrading to refresh the copied
standalone hooks and status script. Updating npm dependencies alone doesn't
update scripts already installed in `~/.codex/supermemory`.

The hosted root `https://api.supermemory.ai` defaults to v5, including equivalent
host casing and default-port spellings. Custom REST URLs, including URL prefixes,
default to the bundled official SDK4 compatibility path so servers older than
0.0.9 remain usable. After upgrading a custom server, set
`SUPERMEMORY_API_VERSION=v5` or `apiVersion: "v5"` in the existing config. Select
`legacy` explicitly to use v3/v4 where the server still supports them. Invalid
version selections fail the operation; a failed v5 request never switches API
versions or sends data to another host. REST URL selection remains independent
of the hosted MCP and browser-auth endpoints, including their existing env fields.

The v5 profile has no query; prompt recall runs profile and memory search in
parallel. Separated search explicitly uses the guide's legacy defaults (memories,
0.6 threshold, 10 candidates, no reranking/query rewriting), then preserves the
local 0.55 floor, cross-namespace deduplication, and top-five/configured cap. The
SDK search helper retains its existing hybrid mode and configured threshold and
limit. Server-side ranking and historical-data availability aren't verified by
local contract tests. Capture keeps its three-second budget and zero SDK retries,
uses POST append/diff with the existing session ID, and advances the cursor only
after valid acceptance. Dynamic processing is asynchronous; acceptance doesn't
prove extraction completion or exactly-once billing after a lost response.

The retained `dist/services/client.js` library exports still have their original
30-second local wait and two-retry SDK cap for calls without hook options. SDK5's
retryable statuses/backoff differ from SDK4, including no automatic 409 retry.
Installed automatic hooks don't use that unbounded policy: a conflict keeps the
capture cursor for a later attempt without an immediate retry. Explicit MCP
operations still use their independent transport rather than these SDK helpers.

Display-name updates (`/v3/container-tags/...`), account details (`/v3/session`),
and the latent exact-content forget helper (`/v4/memories`) remain legacy-only
ancillary operations against the configured REST server. They have no mechanical
v5 equivalent. A v5-only server may leave names/account details unavailable or
return an explicit forget error; the plugin does not substitute semantic deletion.
Interactive MCP tools keep their independent protocol, endpoints, and approvals.

### Environment variables

| Variable | Purpose |
| ------------------------------ | ------------------------------------------------------ |
| `SUPERMEMORY_CODEX_API_KEY` | Your Supermemory API key (browser auth is preferred). |
| `SUPERMEMORY_API_URL` | Override the Supermemory API base URL (takes precedence over config). |
| `SUPERMEMORY_BASE_URL` | Older REST URL alias, used when `SUPERMEMORY_API_URL` is unset. |
| `SUPERMEMORY_API_VERSION` | Select `v5` or `legacy`; overrides `apiVersion` in config. |
| `SUPERMEMORY_DEBUG` | Set to any truthy value to enable debug logging to `~/.codex-supermemory.log`. |

### `~/.codex/supermemory.json` (optional)
Expand All @@ -112,6 +160,7 @@ Drop this file in to override defaults:
| ------------------------ | ---------- | -------------- | -------------------------------------------------------------------------------------------- |
| `apiKey` | `string` | — | API key (env var takes precedence, browser auth is preferred). |
| `baseUrl` | `string` | `https://api.supermemory.ai` | Supermemory API base URL (`SUPERMEMORY_API_URL`/`SUPERMEMORY_BASE_URL` env vars take precedence). |
| `apiVersion` | `"v5" \| "legacy"` | auto | Hosted root API uses v5; custom REST URLs default to legacy compatibility. |
| `similarityThreshold` | `number` | `0.6` | Minimum similarity score for retrieved memories. |
| `maxMemories` | `number` | `5` | Max memories injected per prompt. |
| `maxProfileItems` | `number` | `5` | Max profile items considered from each persistent/recent section. |
Expand Down
10 changes: 10 additions & 0 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ if (typeof packageJson.version !== "string" || !packageJson.version) {
throw new Error("package.json must contain a version");
}

const sdkLicenses = ["supermemory", "supermemory-legacy"].map((name) => {
const sdkPackage = JSON.parse(readFileSync(`node_modules/${name}/package.json`, "utf-8"));
if (sdkPackage.version !== (name === "supermemory" ? "5.0.1" : "4.0.0")) {
throw new Error(`Unexpected ${name} version: ${sdkPackage.version}`);
}
return `${name} ${sdkPackage.version}\n${readFileSync(`node_modules/${name}/LICENSE`, "utf-8")}`;
}).join("\n\n");

const sharedConfig = {
bundle: true,
platform: "node",
Expand Down Expand Up @@ -36,6 +44,8 @@ const executableEntries = [
];

rmSync("dist", { recursive: true, force: true });
mkdirSync("dist", { recursive: true });
writeFileSync("dist/THIRD_PARTY_LICENSES", sdkLicenses);

const libraryEntries = [
{ in: "src/services/session.ts", out: "dist/services/session.js" },
Expand Down
7 changes: 5 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 17 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-supermemory",
"version": "1.0.18",
"version": "1.0.20",
"description": "Persistent memory for OpenAI Codex CLI \u2014 powered by Supermemory",
"type": "module",
"main": "dist/cli.js",
Expand Down Expand Up @@ -31,7 +31,8 @@
],
"dependencies": {
"@iarna/toml": "^2.2.5",
"supermemory": "4.0.0"
"supermemory": "5.0.1",
"supermemory-legacy": "npm:supermemory@4.0.0"
},
"devDependencies": {
"@types/iarna__toml": "^2.0.5",
Expand Down
1 change: 1 addition & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ function install() {
copyFileSync(mcpProxySrc, MCP_PROXY_SCRIPT);
copyFileSync(flushSrc, FLUSH_SCRIPT);
copyFileSync(sessionStartSrc, SESSION_START_SCRIPT);
copyFileSync(join(SCRIPT_DIR, "THIRD_PARTY_LICENSES"), join(SUPERMEMORY_HOOKS_DIR, "THIRD_PARTY_LICENSES"));

// Remove script names left by older package layouts.
for (const script of LEGACY_SUPERMEMORY_SCRIPTS) {
Expand Down
14 changes: 14 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const DEFAULT_RECALL_DIRECTIVE =
interface CodexSupermemoryConfig {
apiKey?: string;
baseUrl?: string;
apiVersion?: "v5" | "legacy";
similarityThreshold?: number;
maxMemories?: number;
maxProfileItems?: number;
Expand Down Expand Up @@ -193,6 +194,19 @@ export function getBaseUrl(): string {
return normalized;
}

export function getApiVersion(): "v5" | "legacy" {
const configured = process.env.SUPERMEMORY_API_VERSION ?? fileConfig.apiVersion;
if (configured !== undefined) {
if (configured === "v5" || configured === "legacy") return configured;
throw new Error('Invalid apiVersion: expected "v5" or "legacy"');
}
const url = new URL(getBaseUrl());
return url.origin === DEFAULT_BASE_URL && url.pathname === "/" &&
!url.username && !url.password && !url.search && !url.hash
? "v5"
: "legacy";
}

// Backwards-compatible alias for the credential-based accessor introduced on main.
export function getApiBaseUrl(): string {
return getBaseUrl();
Expand Down
81 changes: 81 additions & 0 deletions src/services/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Supermemory, { SupermemoryError, type FilterExpression, type RequestOptions } from "supermemory";
import { getApiKeyValue, getBaseUrl } from "../config.js";
import type { ProfileWithSearchResult, SearchResultItem, MemoryScope } from "./client.js";
import { memoryText, recallProvenance } from "./resultText.js";

export function createV5Client(fetchImpl?: typeof fetch): Supermemory {
return new Supermemory({
apiKey: getApiKeyValue(),
baseUrl: getBaseUrl(),
headers: { "x-sm-source": "codex" },
timeoutInSeconds: 60,
maxRetries: 2,
fetch: fetchImpl,
});
}

export function apiErrorMessage(error: unknown): string {
if (error instanceof SupermemoryError) {
return typeof error.statusCode === "number"
? `API request failed (${error.statusCode})`
: "API request failed";
}
return error instanceof Error ? error.message : String(error);
}

export function scopeFilter(scope?: MemoryScope): FilterExpression | undefined {
return scope ? { field: "sm_scope", operator: "eq", value: scope } : undefined;
}

export function profileFacts(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value.map((fact) => typeof fact === "string"
? fact
: fact && typeof fact === "object" ? memoryText(fact) : "")
.filter((fact) => fact.trim().length > 0);
}

export function normalizeSearchItem(item: SearchResultItem): SearchResultItem {
const provenance = recallProvenance(item);
return {
...item,
title: provenance.title,
filepath: provenance.filepath,
updatedAt: item.updatedAt ?? item.system?.updatedAt,
};
}

export async function readV5Profile(
client: Supermemory,
namespace: string,
query?: string,
scope?: MemoryScope,
requestOptions?: RequestOptions,
): Promise<ProfileWithSearchResult> {
const filter = scopeFilter(scope);
const [profile, search] = await Promise.all([
client.profile(namespace, { filter }, requestOptions),
query ? client.search(namespace, {
query,
filter,
searchMode: "memories",
threshold: 0.6,
limit: 10,
rerank: "none",
rewriteQuery: false,
include: { documents: false, related: false, forgotten: false },
}, requestOptions) : undefined,
]);
const results = search?.results.map(normalizeSearchItem).map((item) => ({
...item,
memory: memoryText(item),
})).filter((item) => item.memory.length > 0);
return {
success: true,
profile: {
static: profileFacts(profile.profile?.static),
dynamic: profileFacts(profile.profile?.dynamic),
},
searchResults: results ? { results, total: results.length, timing: search?.searchTime } : undefined,
};
}
Loading