diff --git a/bundle/capture.js b/bundle/capture.js index c394ba03..d1547057 100755 --- a/bundle/capture.js +++ b/bundle/capture.js @@ -3,7 +3,7 @@ // dist/src/hooks/capture.js import { readdirSync, readFileSync as readFileSync2 } from "node:fs"; import { join as join3 } from "node:path"; -import { homedir as homedir3, userInfo } from "node:os"; +import { homedir as homedir3 } from "node:os"; // dist/src/utils/stdin.js function readStdin() { @@ -25,7 +25,7 @@ function readStdin() { // dist/src/config.js import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; +import { homedir, userInfo } from "node:os"; function loadConfig() { const home = homedir(); const credPath = join(home, ".deeplake", "credentials.json"); @@ -45,7 +45,7 @@ function loadConfig() { token, orgId, orgName: creds?.orgName ?? orgId, - userName: creds?.userName ?? "user", + userName: creds?.userName || userInfo().username || "unknown", workspaceId: process.env.DEEPLAKE_WORKSPACE_ID ?? creds?.workspaceId ?? "default", apiUrl: process.env.DEEPLAKE_API_URL ?? creds?.apiUrl ?? "https://api.deeplake.ai", tableName: process.env.DEEPLAKE_TABLE ?? "memory", @@ -381,15 +381,17 @@ var DeeplakeFs = class _DeeplakeFs { ]; for (const row of rows) { const p = row["path"]; - const match = p.match(/\/summaries\/(.+)\.md$/); + const match = p.match(/\/summaries\/([^/]+)\/([^/]+)\.md$/); if (!match) continue; - const sessionId = match[1]; + const summaryUser = match[1]; + const sessionId = match[2]; + const relPath = `summaries/${summaryUser}/${sessionId}.md`; const project = row["project"] || ""; const description = row["description"] || ""; const creationDate = row["creation_date"] || ""; const lastUpdateDate = row["last_update_date"] || ""; - lines.push(`| [${sessionId}](summaries/${sessionId}.md) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`); + lines.push(`| [${sessionId}](${relPath}) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`); } lines.push(""); return lines.join("\n"); @@ -683,15 +685,8 @@ var DeeplakeFs = class _DeeplakeFs { var log3 = (msg) => log("capture", msg); var CAPTURE = process.env.DEEPLAKE_CAPTURE !== "false"; function buildSessionPath(config, sessionId) { - let userName = "user"; - let orgName = "org"; - try { - const creds = JSON.parse(readFileSync2(join3(homedir3(), ".deeplake", "credentials.json"), "utf-8")); - userName = creds.userName ?? creds.orgName ?? userInfo().username ?? "user"; - orgName = creds.orgName ?? "org"; - } catch { - userName = userInfo().username ?? "user"; - } + const userName = config.userName; + const orgName = config.orgName; const workspace = config.workspaceId ?? "default"; let slug = sessionId; try { diff --git a/bundle/commands/auth-login.js b/bundle/commands/auth-login.js index 2b2e11b4..a7c47256 100755 --- a/bundle/commands/auth-login.js +++ b/bundle/commands/auth-login.js @@ -151,7 +151,7 @@ async function removeMember(userId, token, orgId, apiUrl = DEFAULT_API_URL) { async function login(apiUrl = DEFAULT_API_URL) { const { token: authToken } = await deviceFlowLogin(apiUrl); const user = await apiGet("/me", authToken, apiUrl); - const userName = user.name || (user.email ? user.email.split("@")[0] : "user"); + const userName = user.name || (user.email ? user.email.split("@")[0] : "unknown"); process.stderr.write(` Logged in as: ${userName} `); diff --git a/bundle/post-tool-use.js b/bundle/post-tool-use.js index b0f06d10..3745b2f3 100755 --- a/bundle/post-tool-use.js +++ b/bundle/post-tool-use.js @@ -20,7 +20,7 @@ function readStdin() { // dist/src/config.js import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; +import { homedir, userInfo } from "node:os"; function loadConfig() { const home = homedir(); const credPath = join(home, ".deeplake", "credentials.json"); @@ -40,7 +40,7 @@ function loadConfig() { token, orgId, orgName: creds?.orgName ?? orgId, - userName: creds?.userName ?? "user", + userName: creds?.userName || userInfo().username || "unknown", workspaceId: process.env.DEEPLAKE_WORKSPACE_ID ?? creds?.workspaceId ?? "default", apiUrl: process.env.DEEPLAKE_API_URL ?? creds?.apiUrl ?? "https://api.deeplake.ai", tableName: process.env.DEEPLAKE_TABLE ?? "memory", @@ -376,15 +376,17 @@ var DeeplakeFs = class _DeeplakeFs { ]; for (const row of rows) { const p = row["path"]; - const match = p.match(/\/summaries\/(.+)\.md$/); + const match = p.match(/\/summaries\/([^/]+)\/([^/]+)\.md$/); if (!match) continue; - const sessionId = match[1]; + const summaryUser = match[1]; + const sessionId = match[2]; + const relPath = `summaries/${summaryUser}/${sessionId}.md`; const project = row["project"] || ""; const description = row["description"] || ""; const creationDate = row["creation_date"] || ""; const lastUpdateDate = row["last_update_date"] || ""; - lines.push(`| [${sessionId}](summaries/${sessionId}.md) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`); + lines.push(`| [${sessionId}](${relPath}) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`); } lines.push(""); return lines.join("\n"); @@ -687,7 +689,7 @@ async function main() { const table = process.env["DEEPLAKE_TABLE"] ?? "memory"; const api = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, table); const fs = await DeeplakeFs.create(api, table, "/"); - const userName = config.userName ?? "user"; + const userName = config.userName; const sessionPath = `/sessions/${userName}/${userName}_${config.orgName ?? config.orgId}_${config.workspaceId}_${input.session_id}.jsonl`; const entry = { id: crypto.randomUUID(), diff --git a/bundle/pre-tool-use.js b/bundle/pre-tool-use.js index 25bc5663..3e44b180 100755 --- a/bundle/pre-tool-use.js +++ b/bundle/pre-tool-use.js @@ -28,7 +28,7 @@ function readStdin() { // dist/src/config.js import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; +import { homedir, userInfo } from "node:os"; function loadConfig() { const home = homedir(); const credPath = join(home, ".deeplake", "credentials.json"); @@ -48,7 +48,7 @@ function loadConfig() { token, orgId, orgName: creds?.orgName ?? orgId, - userName: creds?.userName ?? "user", + userName: creds?.userName || userInfo().username || "unknown", workspaceId: process.env.DEEPLAKE_WORKSPACE_ID ?? creds?.workspaceId ?? "default", apiUrl: process.env.DEEPLAKE_API_URL ?? creds?.apiUrl ?? "https://api.deeplake.ai", tableName: process.env.DEEPLAKE_TABLE ?? "memory", diff --git a/bundle/session-end.js b/bundle/session-end.js index 2e985306..0b984be2 100755 --- a/bundle/session-end.js +++ b/bundle/session-end.js @@ -26,7 +26,7 @@ function readStdin() { // dist/src/config.js import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; +import { homedir, userInfo } from "node:os"; function loadConfig() { const home = homedir(); const credPath = join(home, ".deeplake", "credentials.json"); @@ -46,7 +46,7 @@ function loadConfig() { token, orgId, orgName: creds?.orgName ?? orgId, - userName: creds?.userName ?? "user", + userName: creds?.userName || userInfo().username || "unknown", workspaceId: process.env.DEEPLAKE_WORKSPACE_ID ?? creds?.workspaceId ?? "default", apiUrl: process.env.DEEPLAKE_API_URL ?? creds?.apiUrl ?? "https://api.deeplake.ai", tableName: process.env.DEEPLAKE_TABLE ?? "memory", @@ -257,7 +257,7 @@ async function main() { wikiLog(`SessionEnd: processing ${sessionId} (${jsonlLines} lines, tmp: ${tmpDir})`); let prevOffset = 0; try { - const sumRows = await api.query(`SELECT content_text FROM "${table}" WHERE path = '${sqlStr(`/summaries/${sessionId}.md`)}' LIMIT 1`); + const sumRows = await api.query(`SELECT content_text FROM "${table}" WHERE path = '${sqlStr(`/summaries/${config.userName}/${sessionId}.md`)}' LIMIT 1`); if (sumRows.length > 0 && sumRows[0]["content_text"]) { const existing = sumRows[0]["content_text"]; const match = existing.match(/\*\*JSONL offset\*\*:\s*(\d+)/); @@ -331,6 +331,7 @@ LENGTH LIMIT: Keep the total summary under 4000 characters. Be dense and concise workspaceId: config.workspaceId, table, sessionId, + userName: config.userName, summaryPath: tmpSummary, project: cwd.split("/").pop() || "unknown", tmpDir @@ -368,14 +369,14 @@ async function upload(vpath, localPath) { } console.log("Uploaded " + vpath); } -await upload("/summaries/" + cfg.sessionId + ".md", cfg.summaryPath); +await upload("/summaries/" + cfg.userName + "/" + cfg.sessionId + ".md", cfg.summaryPath); // Update summary row metadata (description + last_update_date) for virtual index.md try { var summaryText = existsSync(cfg.summaryPath) ? readFileSync(cfg.summaryPath, "utf-8") : ""; var whatHappened = summaryText.match(/## What Happened\\n([\\s\\S]*?)(?=\\n##|$)/); var desc = whatHappened ? whatHappened[1].trim().slice(0, 300) : "completed"; var ts = new Date().toISOString(); - await query("UPDATE \\"" + cfg.table + "\\" SET description = E'" + esc(desc) + "', last_update_date = '" + ts + "' WHERE path = '/summaries/" + cfg.sessionId + ".md'"); + await query("UPDATE \\"" + cfg.table + "\\" SET description = E'" + esc(desc) + "', last_update_date = '" + ts + "' WHERE path = '/summaries/" + cfg.userName + "/" + cfg.sessionId + ".md'"); console.log("Updated description for " + cfg.sessionId); } catch(e) { console.log("Failed to update description: " + e.message); } console.log("Server upload complete for " + cfg.sessionId); diff --git a/bundle/session-start.js b/bundle/session-start.js index 2c53b239..73d2db6d 100755 --- a/bundle/session-start.js +++ b/bundle/session-start.js @@ -22,11 +22,16 @@ function loadCredentials() { return null; } } +function saveCredentials(creds) { + if (!existsSync(CONFIG_DIR)) + mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 }); + writeFileSync(CREDS_PATH, JSON.stringify({ ...creds, savedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), { mode: 384 }); +} // dist/src/config.js import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs"; import { join as join2 } from "node:path"; -import { homedir as homedir2 } from "node:os"; +import { homedir as homedir2, userInfo } from "node:os"; function loadConfig() { const home = homedir2(); const credPath = join2(home, ".deeplake", "credentials.json"); @@ -46,7 +51,7 @@ function loadConfig() { token, orgId, orgName: creds?.orgName ?? orgId, - userName: creds?.userName ?? "user", + userName: creds?.userName || userInfo().username || "unknown", workspaceId: process.env.DEEPLAKE_WORKSPACE_ID ?? creds?.workspaceId ?? "default", apiUrl: process.env.DEEPLAKE_API_URL ?? creds?.apiUrl ?? "https://api.deeplake.ai", tableName: process.env.DEEPLAKE_TABLE ?? "memory", @@ -382,15 +387,17 @@ var DeeplakeFs = class _DeeplakeFs { ]; for (const row of rows) { const p = row["path"]; - const match = p.match(/\/summaries\/(.+)\.md$/); + const match = p.match(/\/summaries\/([^/]+)\/([^/]+)\.md$/); if (!match) continue; - const sessionId = match[1]; + const summaryUser = match[1]; + const sessionId = match[2]; + const relPath = `summaries/${summaryUser}/${sessionId}.md`; const project = row["project"] || ""; const description = row["description"] || ""; const creationDate = row["creation_date"] || ""; const lastUpdateDate = row["last_update_date"] || ""; - lines.push(`| [${sessionId}](summaries/${sessionId}.md) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`); + lines.push(`| [${sessionId}](${relPath}) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`); } lines.push(""); return lines.join("\n"); @@ -708,7 +715,7 @@ var context = `DEEPLAKE MEMORY: You have TWO memory sources. ALWAYS check BOTH w Deeplake memory structure: - ~/.deeplake/memory/index.md \u2014 START HERE, table of all sessions -- ~/.deeplake/memory/summaries/*.md \u2014 AI-generated wiki summaries per session +- ~/.deeplake/memory/summaries/username/*.md \u2014 AI-generated wiki summaries per session - ~/.deeplake/memory/sessions/username/*.jsonl \u2014 raw session data (last resort) SEARCH STRATEGY: Always read index.md first. Then read specific summaries. Only read raw JSONL if summaries don't have enough detail. Do NOT jump straight to JSONL files. @@ -740,11 +747,15 @@ async function createPlaceholder(fs, sessionId, cwd, userName, orgName, workspac await fs.mkdir("/summaries"); } catch { } + try { + await fs.mkdir(`/summaries/${userName}`); + } catch { + } try { await fs.mkdir("/sessions"); } catch { } - const summaryPath = `/summaries/${sessionId}.md`; + const summaryPath = `/summaries/${userName}/${sessionId}.md`; const summaryExists = await fs.exists(summaryPath); if (!summaryExists) { const now = (/* @__PURE__ */ new Date()).toISOString(); @@ -778,9 +789,10 @@ async function main() { log3(`credentials loaded: org=${creds.orgName ?? creds.orgId}`); if (creds.token && !creds.userName) { try { - const { userInfo } = await import("node:os"); - creds.userName = userInfo().username ?? "user"; - log3(`backfilled userName: ${creds.userName}`); + const { userInfo: userInfo2 } = await import("node:os"); + creds.userName = userInfo2().username ?? "unknown"; + saveCredentials(creds); + log3(`backfilled and persisted userName: ${creds.userName}`); } catch { } } diff --git a/bundle/shell/deeplake-shell.js b/bundle/shell/deeplake-shell.js index c25b3a90..71169fa5 100755 --- a/bundle/shell/deeplake-shell.js +++ b/bundle/shell/deeplake-shell.js @@ -66723,7 +66723,7 @@ function Bi3(e6) { // dist/src/config.js import { readFileSync, existsSync as existsSync2 } from "node:fs"; import { join as join4 } from "node:path"; -import { homedir } from "node:os"; +import { homedir, userInfo } from "node:os"; function loadConfig() { const home = homedir(); const credPath = join4(home, ".deeplake", "credentials.json"); @@ -66743,7 +66743,7 @@ function loadConfig() { token, orgId, orgName: creds?.orgName ?? orgId, - userName: creds?.userName ?? "user", + userName: creds?.userName || userInfo().username || "unknown", workspaceId: process.env.DEEPLAKE_WORKSPACE_ID ?? creds?.workspaceId ?? "default", apiUrl: process.env.DEEPLAKE_API_URL ?? creds?.apiUrl ?? "https://api.deeplake.ai", tableName: process.env.DEEPLAKE_TABLE ?? "memory", @@ -67079,15 +67079,17 @@ var DeeplakeFs = class _DeeplakeFs { ]; for (const row of rows) { const p22 = row["path"]; - const match2 = p22.match(/\/summaries\/(.+)\.md$/); + const match2 = p22.match(/\/summaries\/([^/]+)\/([^/]+)\.md$/); if (!match2) continue; - const sessionId = match2[1]; + const summaryUser = match2[1]; + const sessionId = match2[2]; + const relPath = `summaries/${summaryUser}/${sessionId}.md`; const project = row["project"] || ""; const description = row["description"] || ""; const creationDate = row["creation_date"] || ""; const lastUpdateDate = row["last_update_date"] || ""; - lines.push(`| [${sessionId}](summaries/${sessionId}.md) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`); + lines.push(`| [${sessionId}](${relPath}) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`); } lines.push(""); return lines.join("\n"); diff --git a/package.json b/package.json index 08e454d7..d1ea4005 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "deeplake-claude-code-plugins", - "version": "0.3.1", + "version": "0.3.2", "description": "Claude Code plugin for Deeplake memory — captures file ops and syncs to Deeplake cloud", "type": "module", "bin": { diff --git a/src/commands/auth.ts b/src/commands/auth.ts index 75c55862..c4db41d3 100644 --- a/src/commands/auth.ts +++ b/src/commands/auth.ts @@ -241,7 +241,7 @@ export async function login(apiUrl = DEFAULT_API_URL): Promise { // Step 2: Get user info const user = await apiGet("/me", authToken, apiUrl) as { id: string; name: string; email?: string }; - const userName = user.name || (user.email ? user.email.split("@")[0] : "user"); + const userName = user.name || (user.email ? user.email.split("@")[0] : "unknown"); process.stderr.write(`\nLogged in as: ${userName}\n`); // Step 3: List orgs and select diff --git a/src/config.ts b/src/config.ts index 2ad724bc..bff0322b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ import { readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; -import { homedir } from "node:os"; +import { homedir, userInfo } from "node:os"; export interface Config { token: string; @@ -43,7 +43,7 @@ export function loadConfig(): Config | null { token, orgId, orgName: creds?.orgName ?? orgId, - userName: creds?.userName ?? "user", + userName: creds?.userName || userInfo().username || "unknown", workspaceId: process.env.DEEPLAKE_WORKSPACE_ID ?? creds?.workspaceId ?? "default", apiUrl: process.env.DEEPLAKE_API_URL ?? creds?.apiUrl ?? "https://api.deeplake.ai", tableName: process.env.DEEPLAKE_TABLE ?? "memory", diff --git a/src/hooks/capture.ts b/src/hooks/capture.ts index 950c7cb0..68784b9c 100644 --- a/src/hooks/capture.ts +++ b/src/hooks/capture.ts @@ -2,7 +2,7 @@ import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; -import { homedir, userInfo } from "node:os"; +import { homedir } from "node:os"; import { readStdin } from "../utils/stdin.js"; import { loadConfig } from "../config.js"; import { DeeplakeApi } from "../deeplake-api.js"; @@ -35,17 +35,9 @@ const CAPTURE = process.env.DEEPLAKE_CAPTURE !== "false"; /** Build the session JSONL path matching the CLI convention: * /sessions//___.jsonl */ -function buildSessionPath(config: { orgId: string; workspaceId: string }, sessionId: string): string { - // Try to get userName from credentials.json (may have been saved by auth flow) - let userName = "user"; - let orgName = "org"; - try { - const creds = JSON.parse(readFileSync(join(homedir(), ".deeplake", "credentials.json"), "utf-8")); - userName = creds.userName ?? creds.orgName ?? userInfo().username ?? "user"; - orgName = creds.orgName ?? "org"; - } catch { - userName = userInfo().username ?? "user"; - } +function buildSessionPath(config: { userName: string; orgName: string; workspaceId: string }, sessionId: string): string { + const userName = config.userName; + const orgName = config.orgName; const workspace = config.workspaceId ?? "default"; // Try to extract slug from local Claude JSONL diff --git a/src/hooks/post-tool-use.ts b/src/hooks/post-tool-use.ts index 0c75288d..038b55a3 100644 --- a/src/hooks/post-tool-use.ts +++ b/src/hooks/post-tool-use.ts @@ -27,7 +27,7 @@ async function main(): Promise { const api = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, table); const fs = await DeeplakeFs.create(api, table, "/"); - const userName = config.userName ?? "user"; + const userName = config.userName; const sessionPath = `/sessions/${userName}/${userName}_${config.orgName ?? config.orgId}_${config.workspaceId}_${input.session_id}.jsonl`; const entry = { diff --git a/src/hooks/session-end.ts b/src/hooks/session-end.ts index 75444875..4e21204b 100644 --- a/src/hooks/session-end.ts +++ b/src/hooks/session-end.ts @@ -101,7 +101,7 @@ async function main(): Promise { let prevOffset = 0; try { const sumRows = await api.query( - `SELECT content_text FROM "${table}" WHERE path = '${sqlStr(`/summaries/${sessionId}.md`)}' LIMIT 1` + `SELECT content_text FROM "${table}" WHERE path = '${sqlStr(`/summaries/${config.userName}/${sessionId}.md`)}' LIMIT 1` ); if (sumRows.length > 0 && sumRows[0]["content_text"]) { const existing = sumRows[0]["content_text"] as string; @@ -181,6 +181,7 @@ LENGTH LIMIT: Keep the total summary under 4000 characters. Be dense and concise workspaceId: config.workspaceId, table, sessionId, + userName: config.userName, summaryPath: tmpSummary, project: cwd.split("/").pop() || "unknown", tmpDir, @@ -220,14 +221,14 @@ async function upload(vpath, localPath) { } console.log("Uploaded " + vpath); } -await upload("/summaries/" + cfg.sessionId + ".md", cfg.summaryPath); +await upload("/summaries/" + cfg.userName + "/" + cfg.sessionId + ".md", cfg.summaryPath); // Update summary row metadata (description + last_update_date) for virtual index.md try { var summaryText = existsSync(cfg.summaryPath) ? readFileSync(cfg.summaryPath, "utf-8") : ""; var whatHappened = summaryText.match(/## What Happened\\n([\\s\\S]*?)(?=\\n##|$)/); var desc = whatHappened ? whatHappened[1].trim().slice(0, 300) : "completed"; var ts = new Date().toISOString(); - await query("UPDATE \\"" + cfg.table + "\\" SET description = E'" + esc(desc) + "', last_update_date = '" + ts + "' WHERE path = '/summaries/" + cfg.sessionId + ".md'"); + await query("UPDATE \\"" + cfg.table + "\\" SET description = E'" + esc(desc) + "', last_update_date = '" + ts + "' WHERE path = '/summaries/" + cfg.userName + "/" + cfg.sessionId + ".md'"); console.log("Updated description for " + cfg.sessionId); } catch(e) { console.log("Failed to update description: " + e.message); } console.log("Server upload complete for " + cfg.sessionId); diff --git a/src/hooks/session-start.ts b/src/hooks/session-start.ts index 1fd5560b..0ad7cf33 100644 --- a/src/hooks/session-start.ts +++ b/src/hooks/session-start.ts @@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; import { mkdirSync, appendFileSync } from "node:fs"; import { homedir } from "node:os"; -import { loadCredentials, login } from "../commands/auth.js"; +import { loadCredentials, saveCredentials, login } from "../commands/auth.js"; import { loadConfig } from "../config.js"; import { DeeplakeApi } from "../deeplake-api.js"; import { DeeplakeFs } from "../shell/deeplake-fs.js"; @@ -28,7 +28,7 @@ const context = `DEEPLAKE MEMORY: You have TWO memory sources. ALWAYS check BOTH Deeplake memory structure: - ~/.deeplake/memory/index.md — START HERE, table of all sessions -- ~/.deeplake/memory/summaries/*.md — AI-generated wiki summaries per session +- ~/.deeplake/memory/summaries/username/*.md — AI-generated wiki summaries per session - ~/.deeplake/memory/sessions/username/*.jsonl — raw session data (last resort) SEARCH STRATEGY: Always read index.md first. Then read specific summaries. Only read raw JSONL if summaries don't have enough detail. Do NOT jump straight to JSONL files. @@ -59,9 +59,10 @@ function wikiLog(msg: string): void { async function createPlaceholder(fs: DeeplakeFs, sessionId: string, cwd: string, userName: string, orgName: string, workspaceId: string): Promise { // Ensure directories try { await fs.mkdir("/summaries"); } catch { /* exists */ } + try { await fs.mkdir(`/summaries/${userName}`); } catch { /* exists */ } try { await fs.mkdir("/sessions"); } catch { /* exists */ } - const summaryPath = `/summaries/${sessionId}.md`; + const summaryPath = `/summaries/${userName}/${sessionId}.md`; const summaryExists = await fs.exists(summaryPath); if (!summaryExists) { @@ -107,8 +108,9 @@ async function main(): Promise { if (creds.token && !creds.userName) { try { const { userInfo } = await import("node:os"); - creds.userName = userInfo().username ?? "user"; - log(`backfilled userName: ${creds.userName}`); + creds.userName = userInfo().username ?? "unknown"; + saveCredentials(creds); + log(`backfilled and persisted userName: ${creds.userName}`); } catch { /* non-fatal */ } } } diff --git a/src/hooks/stop.ts b/src/hooks/stop.ts index f854bf76..2d2852f3 100644 --- a/src/hooks/stop.ts +++ b/src/hooks/stop.ts @@ -27,7 +27,7 @@ async function main(): Promise { const api = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, table); const fs = await DeeplakeFs.create(api, table, "/"); - const userName = config.userName ?? "user"; + const userName = config.userName; const sessionPath = `/sessions/${userName}/${userName}_${config.orgName ?? config.orgId}_${config.workspaceId}_${input.session_id}.jsonl`; const entry = { diff --git a/src/hooks/user-prompt-submit.ts b/src/hooks/user-prompt-submit.ts index 9955fb2f..e71d4f54 100644 --- a/src/hooks/user-prompt-submit.ts +++ b/src/hooks/user-prompt-submit.ts @@ -23,7 +23,7 @@ async function main(): Promise { const api = new DeeplakeApi(config.token, config.apiUrl, config.orgId, config.workspaceId, table); const fs = await DeeplakeFs.create(api, table, "/"); - const userName = config.userName ?? "user"; + const userName = config.userName; const sessionPath = `/sessions/${userName}/${userName}_${config.orgName ?? config.orgId}_${config.workspaceId}_${input.session_id}.jsonl`; const entry = { diff --git a/src/shell/deeplake-fs.ts b/src/shell/deeplake-fs.ts index 566f53ab..a5407a34 100644 --- a/src/shell/deeplake-fs.ts +++ b/src/shell/deeplake-fs.ts @@ -225,15 +225,17 @@ export class DeeplakeFs implements IFileSystem { ]; for (const row of rows) { const p = row["path"] as string; - // Extract session ID from path: /summaries/.md - const match = p.match(/\/summaries\/(.+)\.md$/); + // Extract session ID from path: /summaries//.md + const match = p.match(/\/summaries\/([^/]+)\/([^/]+)\.md$/); if (!match) continue; - const sessionId = match[1]; + const summaryUser = match[1]; + const sessionId = match[2]; + const relPath = `summaries/${summaryUser}/${sessionId}.md`; const project = (row["project"] as string) || ""; const description = (row["description"] as string) || ""; const creationDate = (row["creation_date"] as string) || ""; const lastUpdateDate = (row["last_update_date"] as string) || ""; - lines.push(`| [${sessionId}](summaries/${sessionId}.md) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`); + lines.push(`| [${sessionId}](${relPath}) | ${creationDate} | ${lastUpdateDate} | ${project} | ${description} |`); } lines.push(""); return lines.join("\n"); diff --git a/tests/deeplake-fs.test.ts b/tests/deeplake-fs.test.ts index 85f0a81e..756974ef 100644 --- a/tests/deeplake-fs.test.ts +++ b/tests/deeplake-fs.test.ts @@ -665,15 +665,15 @@ describe("unsupported ops", () => { // ── Virtual index.md ───────────────────────────────────────────────────────── describe("virtual index.md", () => { /** Helper: create FS mounted at "/" with summary rows that have metadata columns set. */ - async function makeFsWithSummaries(summaries: { id: string; project: string; description: string; creationDate: string; lastUpdateDate: string; content: string }[], extraSeed: Record = {}) { + async function makeFsWithSummaries(summaries: { id: string; userName: string; project: string; description: string; creationDate: string; lastUpdateDate: string; content: string }[], extraSeed: Record = {}) { const seed: Record = { ...extraSeed }; for (const s of summaries) { - seed[`/summaries/${s.id}.md`] = s.content; + seed[`/summaries/${s.userName}/${s.id}.md`] = s.content; } const { fs, client } = await makeFs(seed, "/"); // Set metadata on summary rows for (const s of summaries) { - const row = client._rows.find(r => r.path === `/summaries/${s.id}.md`); + const row = client._rows.find(r => r.path === `/summaries/${s.userName}/${s.id}.md`); if (row) { row.project = s.project; row.description = s.description; @@ -686,8 +686,8 @@ describe("virtual index.md", () => { it("generates virtual index when no /index.md row exists", async () => { const { fs } = await makeFsWithSummaries([ - { id: "aaa-111", project: "my-project", description: "Fixed auth bug", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T11:00:00.000Z", content: "# Session aaa-111" }, - { id: "bbb-222", project: "other-proj", description: "in progress", creationDate: "2026-04-07T12:00:00.000Z", lastUpdateDate: "2026-04-07T12:00:00.000Z", content: "# Session bbb-222" }, + { id: "aaa-111", userName: "alice", project: "my-project", description: "Fixed auth bug", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T11:00:00.000Z", content: "# Session aaa-111" }, + { id: "bbb-222", userName: "alice", project: "other-proj", description: "in progress", creationDate: "2026-04-07T12:00:00.000Z", lastUpdateDate: "2026-04-07T12:00:00.000Z", content: "# Session bbb-222" }, ]); const content = await fs.readFile("/index.md"); expect(content).toContain("# Session Index"); @@ -701,7 +701,7 @@ describe("virtual index.md", () => { it("serves real /index.md row when it exists", async () => { const { fs } = await makeFsWithSummaries( - [{ id: "aaa-111", project: "proj", description: "desc", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T10:00:00.000Z", content: "# Session" }], + [{ id: "aaa-111", userName: "alice", project: "proj", description: "desc", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T10:00:00.000Z", content: "# Session" }], { "/index.md": "# My Custom Index\nHello" }, ); const content = await fs.readFile("/index.md"); @@ -715,7 +715,7 @@ describe("virtual index.md", () => { it("readdir('/') includes index.md even without a real row", async () => { const { fs } = await makeFsWithSummaries([ - { id: "aaa-111", project: "proj", description: "desc", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T10:00:00.000Z", content: "# Session" }, + { id: "aaa-111", userName: "alice", project: "proj", description: "desc", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T10:00:00.000Z", content: "# Session" }, ]); const entries = await fs.readdir("/"); expect(entries).toContain("index.md"); @@ -731,8 +731,8 @@ describe("virtual index.md", () => { it("virtual index shows all summary rows ordered", async () => { const { fs } = await makeFsWithSummaries([ - { id: "old-session", project: "proj-a", description: "Old work", creationDate: "2026-04-01T10:00:00.000Z", lastUpdateDate: "2026-04-01T11:00:00.000Z", content: "# Old" }, - { id: "new-session", project: "proj-b", description: "New work", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T12:00:00.000Z", content: "# New" }, + { id: "old-session", userName: "alice", project: "proj-a", description: "Old work", creationDate: "2026-04-01T10:00:00.000Z", lastUpdateDate: "2026-04-01T11:00:00.000Z", content: "# Old" }, + { id: "new-session", userName: "alice", project: "proj-b", description: "New work", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T12:00:00.000Z", content: "# New" }, ]); const content = await fs.readFile("/index.md"); // Both sessions should appear @@ -758,6 +758,41 @@ describe("virtual index.md", () => { const indexEntries = entries.filter(e => e === "index.md"); expect(indexEntries.length).toBe(1); }); + + it("virtual index uses summaries/username/id.md links for new paths", async () => { + const { fs } = await makeFsWithSummaries([ + { id: "sess-001", userName: "alice", project: "proj-a", description: "Did stuff", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T11:00:00.000Z", content: "# Session sess-001" }, + ]); + const content = await fs.readFile("/index.md"); + expect(content).toContain("summaries/alice/sess-001.md"); + expect(content).toContain("sess-001"); + expect(content).toContain("Did stuff"); + }); + + it("virtual index skips summaries without username in path", async () => { + const { fs, client } = await makeFsWithSummaries([ + { id: "new-sess", userName: "bob", project: "proj-b", description: "New session", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T12:00:00.000Z", content: "# New" }, + ]); + // Manually insert a legacy row (no username dir) + client._rows.push({ + id: "legacy", path: "/summaries/old-sess.md", filename: "old-sess.md", + content: Buffer.from("# Old"), content_text: "# Old", mime_type: "text/markdown", + size_bytes: 5, project: "proj-a", description: "Legacy", creation_date: "2026-04-01", last_update_date: "2026-04-01", + }); + const content = await fs.readFile("/index.md"); + expect(content).toContain("summaries/bob/new-sess.md"); + expect(content).not.toContain("old-sess"); + }); + + it("virtual index links multiple users correctly", async () => { + const { fs } = await makeFsWithSummaries([ + { id: "s1", userName: "alice", project: "proj", description: "Alice work", creationDate: "2026-04-07T10:00:00.000Z", lastUpdateDate: "2026-04-07T10:00:00.000Z", content: "# S1" }, + { id: "s2", userName: "bob", project: "proj", description: "Bob work", creationDate: "2026-04-07T11:00:00.000Z", lastUpdateDate: "2026-04-07T11:00:00.000Z", content: "# S2" }, + ]); + const content = await fs.readFile("/index.md"); + expect(content).toContain("summaries/alice/s1.md"); + expect(content).toContain("summaries/bob/s2.md"); + }); }); // ── writeFileWithMeta ──────────────────────────────────────────────────────── diff --git a/tests/session-summary.test.ts b/tests/session-summary.test.ts index c5a0b18d..343e7977 100644 --- a/tests/session-summary.test.ts +++ b/tests/session-summary.test.ts @@ -160,9 +160,10 @@ async function createPlaceholder( userName: string, orgName: string, workspaceId: string, ) { try { await fs.mkdir("/summaries"); } catch { /* exists */ } + try { await fs.mkdir(`/summaries/${userName}`); } catch { /* exists */ } try { await fs.mkdir("/sessions"); } catch { /* exists */ } - const summaryPath = `/summaries/${sessionId}.md`; + const summaryPath = `/summaries/${userName}/${sessionId}.md`; const summaryExists = await fs.exists(summaryPath); if (!summaryExists) { @@ -189,9 +190,9 @@ async function createPlaceholder( // ── Simulate session-end summary upload (mirrors upload.mjs logic) ─────────── async function uploadSummary( fs: DeeplakeFs, client: ReturnType, - sessionId: string, summaryContent: string, projectName: string, + sessionId: string, userName: string, summaryContent: string, projectName: string, ) { - const summaryPath = `/summaries/${sessionId}.md`; + const summaryPath = `/summaries/${userName}/${sessionId}.md`; await fs.writeFileWithMeta(summaryPath, summaryContent, { project: projectName, description: summaryContent.match(/## What Happened\n([\s\S]*?)(?=\n##|$)/)?.[1]?.trim().slice(0, 80) ?? "completed", @@ -228,7 +229,7 @@ describe("session summary — no global paths", () => { const { fs, client } = await makeFs({}); await createPlaceholder(fs, sessionId, globalPath, userName, orgName, workspaceId); - const row = client._rows.find(r => r.path === `/summaries/${sessionId}.md`); + const row = client._rows.find(r => r.path.endsWith(`/${sessionId}.md`) && r.path.startsWith("/summaries/")); expect(row).toBeDefined(); const content = row!.content_text; @@ -256,7 +257,7 @@ describe("session summary — Source field structure", () => { const sessionId = "abc-123-def"; await createPlaceholder(fs, sessionId, "/some/deep/path/my-repo", "alice", "acme-corp", "prod"); - const content = client._rows.find(r => r.path === `/summaries/${sessionId}.md`)!.content_text; + const content = client._rows.find(r => r.path.endsWith(`/${sessionId}.md`) && r.path.startsWith("/summaries/"))!.content_text; expect(content).toContain("- **Source**: /sessions/alice/alice_acme-corp_prod_abc-123-def.jsonl"); }); }); @@ -280,7 +281,7 @@ describe("session summary — resumed sessions update last_update_date", () => { await createPlaceholder(fs, sessionId, cwd, "testuser", "testorg", "default"); await fs.flush(); - const rowAfterStart = client._rows.find(r => r.path === `/summaries/${sessionId}.md`)!; + const rowAfterStart = client._rows.find(r => r.path.endsWith(`/${sessionId}.md`) && r.path.startsWith("/summaries/"))!; const initialDate = rowAfterStart.last_update_date; expect(rowAfterStart.description).toBe("in progress"); expect(rowAfterStart.project).toBe("my-app"); @@ -304,9 +305,9 @@ describe("session summary — resumed sessions update last_update_date", () => { "- Auth tokens now refresh automatically", ].join("\n"); - await uploadSummary(fs, client, sessionId, completedSummary, "my-app"); + await uploadSummary(fs, client, sessionId, "testuser", completedSummary, "my-app"); - const rowAfterEnd = client._rows.find(r => r.path === `/summaries/${sessionId}.md`)!; + const rowAfterEnd = client._rows.find(r => r.path.endsWith(`/${sessionId}.md`) && r.path.startsWith("/summaries/"))!; // last_update_date must have changed expect(rowAfterEnd.last_update_date).not.toBe(initialDate); // description must be extracted from What Happened section @@ -326,12 +327,12 @@ describe("session summary — resumed sessions update last_update_date", () => { // Round 1: placeholder await createPlaceholder(fs, sessionId, "/opt/repos/backend", "dev", "corp", "staging"); await fs.flush(); - const date1 = client._rows.find(r => r.path === `/summaries/${sessionId}.md`)!.last_update_date; + const date1 = client._rows.find(r => r.path.endsWith(`/${sessionId}.md`) && r.path.startsWith("/summaries/"))!.last_update_date; await new Promise(r => setTimeout(r, 50)); // Round 2: first summary - await uploadSummary(fs, client, sessionId, [ + await uploadSummary(fs, client, sessionId, "dev", [ `# Session ${sessionId}`, `- **Source**: /sessions/dev/dev_corp_staging_${sessionId}.jsonl`, `- **Project**: backend`, @@ -341,13 +342,13 @@ describe("session summary — resumed sessions update last_update_date", () => { "Initial API endpoint scaffolding.", ].join("\n"), "backend"); - const date2 = client._rows.find(r => r.path === `/summaries/${sessionId}.md`)!.last_update_date; + const date2 = client._rows.find(r => r.path.endsWith(`/${sessionId}.md`) && r.path.startsWith("/summaries/"))!.last_update_date; expect(date2).not.toBe(date1); await new Promise(r => setTimeout(r, 50)); // Round 3: resumed session with more content - await uploadSummary(fs, client, sessionId, [ + await uploadSummary(fs, client, sessionId, "dev", [ `# Session ${sessionId}`, `- **Source**: /sessions/dev/dev_corp_staging_${sessionId}.jsonl`, `- **Project**: backend`, @@ -357,7 +358,7 @@ describe("session summary — resumed sessions update last_update_date", () => { "Initial API endpoint scaffolding. Added auth middleware and rate limiting.", ].join("\n"), "backend"); - const rowFinal = client._rows.find(r => r.path === `/summaries/${sessionId}.md`)!; + const rowFinal = client._rows.find(r => r.path.endsWith(`/${sessionId}.md`) && r.path.startsWith("/summaries/"))!; const date3 = rowFinal.last_update_date; expect(date3).not.toBe(date2); expect(rowFinal.content_text).toContain("rate limiting");