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
76 changes: 75 additions & 1 deletion src/services/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { log } from "./logger.js";
import type { MemoryType } from "../types/index.js";

const TIMEOUT_MS = 30000;
const SPACE_NAME_TIMEOUT_MS = 5000;
const API_URL =
process.env.SUPERMEMORY_API_URL ||
process.env.SUPERMEMORY_BASE_URL ||
"https://api.supermemory.ai";
const CODEX_SOURCE = "codex";

function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
let id: ReturnType<typeof setTimeout>;
Expand Down Expand Up @@ -191,7 +197,11 @@ export class SupermemoryClient {
} = {
content,
containerTag,
metadata: metadata as Record<string, string | number | boolean | string[]>,
metadata: {
sm_source: CODEX_SOURCE,
sm_client: CODEX_SOURCE,
...metadata,
} as Record<string, string | number | boolean | string[]>,
};
if (options?.customId) {
payload.customId = options.customId;
Expand All @@ -209,6 +219,70 @@ export class SupermemoryClient {
}
}

async updateContainerTagName(containerTag: string, name: string) {
log("updateContainerTagName: start", { containerTag, name });
try {
const currentResponse = await withTimeout(
fetch(`${API_URL}/v3/container-tags/${encodeURIComponent(containerTag)}`, {
headers: {
Authorization: `Bearer ${getApiKeyValue()}`,
},
}),
SPACE_NAME_TIMEOUT_MS
);

if (!currentResponse.ok) {
log("updateContainerTagName: skipped", {
containerTag,
status: currentResponse.status,
});
return { success: false as const, error: `HTTP ${currentResponse.status}` };
}

const current = (await currentResponse.json()) as { name?: string | null };
const currentName = current.name?.trim();
if (
currentName &&
currentName !== `Space ${containerTag}` &&
!currentName.startsWith("Codex · ")
) {
log("updateContainerTagName: kept custom name", { containerTag, currentName });
return { success: true as const };
}

if (currentName === name) {
return { success: true as const };
}

const response = await withTimeout(
fetch(`${API_URL}/v3/container-tags/${encodeURIComponent(containerTag)}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${getApiKeyValue()}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name }),
}),
SPACE_NAME_TIMEOUT_MS
);

if (!response.ok) {
log("updateContainerTagName: skipped", {
containerTag,
status: response.status,
});
return { success: false as const, error: `HTTP ${response.status}` };
}

log("updateContainerTagName: success", { containerTag, name });
return { success: true as const };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log("updateContainerTagName: error", { containerTag, error: errorMessage });
return { success: false as const, error: errorMessage };
}
}

async forgetMemory(content: string, containerTag: string): Promise<{ success: true; message: string; id?: string } | { success: false; error: string }> {
log("forgetMemory: start", { containerTag, contentLength: content.length });
try {
Expand Down
20 changes: 20 additions & 0 deletions src/services/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,20 @@ function getGitRoot(directory: string): string | null {
}
}

function getGitRepoName(directory: string): string | null {
try {
const remoteUrl = execSync("git remote get-url origin", {
cwd: directory,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
}).trim();
const match = remoteUrl.match(/[/:]([^/]+?)(?:\.git)?$/);
return match ? match[1] : null;
} catch {
return null;
}
}

export function getUserTag(): string {
if (CONFIG.userContainerTag) return CONFIG.userContainerTag;
const email = getGitEmail();
Expand All @@ -82,6 +96,12 @@ export function getProjectTag(directory: string): string {
return `${CONFIG.containerTagPrefix}_project_${sha256(basePath)}`;
}

export function getProjectName(directory: string): string {
const gitRoot = getGitRoot(directory);
const basePath = gitRoot || directory;
return getGitRepoName(basePath) || basename(basePath) || "unknown";
}

export function getTags(directory: string): { user: string; project: string } {
return {
user: getUserTag(),
Expand Down
5 changes: 4 additions & 1 deletion src/skills/save-memory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isConfigured } from "../config.js";
import { SupermemoryClient } from "../services/client.js";
import { getProjectTag } from "../services/tags.js";
import { getProjectName, getProjectTag } from "../services/tags.js";

async function main(): Promise<void> {
if (!isConfigured()) {
Expand All @@ -20,17 +20,20 @@ async function main(): Promise<void> {

const client = new SupermemoryClient();
const projectTag = getProjectTag(process.cwd());
const projectName = getProjectName(process.cwd());

try {
const metadata = {
type: "project-knowledge" as const,
source: "skill",
project: projectName,
timestamp: new Date().toISOString(),
};

const result = await client.addMemory(content, projectTag, metadata);

if (result.success) {
await client.updateContainerTagName(projectTag, `Codex · ${projectName}`);
console.log(`Memory saved (id: ${result.id}) to project '${projectTag}'`);
} else {
console.log(`Failed to save memory: ${result.error}`);
Expand Down