dsh-supermemory 0.1.0

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.
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,96 @@
1
+ import { r as getApiKey } from "./settings-BysiYjKM.mjs";
2
+ import readline from "node:readline";
3
+ //#region src/mcp-proxy.ts
4
+ /**
5
+ * Bridges DSH's stdio MCP transport to the hosted Supermemory MCP server,
6
+ * authenticating with the same credentials file the plugin uses — one browser
7
+ * login covers both. Messages are forwarded sequentially to preserve JSON-RPC
8
+ * ordering; SSE responses are unwrapped back into stdout lines.
9
+ */
10
+ const MCP_URL = process.env.SUPERMEMORY_MCP_URL || "https://mcp.supermemory.ai/mcp";
11
+ const REQUEST_TIMEOUT_MS = 3e4;
12
+ let sessionId = null;
13
+ function send(message) {
14
+ process.stdout.write(`${JSON.stringify(message)}\n`);
15
+ }
16
+ function sendError(id, code, message) {
17
+ if (id === void 0 || id === null) return;
18
+ send({
19
+ jsonrpc: "2.0",
20
+ id,
21
+ error: {
22
+ code,
23
+ message
24
+ }
25
+ });
26
+ }
27
+ function emitSseData(text) {
28
+ for (const event of text.split("\n\n")) for (const line of event.split("\n")) if (line.startsWith("data:")) {
29
+ const data = line.slice(5).trim();
30
+ if (data) process.stdout.write(`${data}\n`);
31
+ }
32
+ }
33
+ async function forward(message, apiKey) {
34
+ const headers = {
35
+ Authorization: `Bearer ${apiKey}`,
36
+ "Content-Type": "application/json",
37
+ Accept: "application/json, text/event-stream"
38
+ };
39
+ if (sessionId) headers["Mcp-Session-Id"] = sessionId;
40
+ const response = await fetch(MCP_URL, {
41
+ method: "POST",
42
+ headers,
43
+ body: JSON.stringify(message),
44
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
45
+ });
46
+ const newSessionId = response.headers.get("mcp-session-id");
47
+ if (newSessionId) sessionId = newSessionId;
48
+ if (response.status === 202) return;
49
+ if (!response.ok) {
50
+ const text = await response.text().catch(() => "");
51
+ sendError(message.id, -32e3, `Supermemory MCP ${response.status}: ${text.slice(0, 200) || "request failed"}`);
52
+ return;
53
+ }
54
+ const contentType = response.headers.get("content-type") || "";
55
+ const body = await response.text();
56
+ if (!body.trim()) return;
57
+ if (contentType.includes("text/event-stream")) emitSseData(body);
58
+ else process.stdout.write(`${body.trim()}\n`);
59
+ }
60
+ function main() {
61
+ let apiKey = null;
62
+ let keyError = null;
63
+ try {
64
+ apiKey = getApiKey(process.cwd());
65
+ } catch (err) {
66
+ keyError = err;
67
+ }
68
+ let queue = Promise.resolve();
69
+ const rl = readline.createInterface({ input: process.stdin });
70
+ rl.on("line", (line) => {
71
+ if (!line.trim()) return;
72
+ let message;
73
+ try {
74
+ message = JSON.parse(line);
75
+ } catch {
76
+ return;
77
+ }
78
+ queue = queue.then(async () => {
79
+ if (keyError) {
80
+ sendError(message.id, -32001, "Supermemory is not authenticated. Start a DSH session with the supermemory plugin to log in, or set SUPERMEMORY_CC_API_KEY.");
81
+ return;
82
+ }
83
+ try {
84
+ await forward(message, apiKey);
85
+ } catch (err) {
86
+ sendError(message.id, -32e3, `Supermemory MCP proxy error: ${err.message}`);
87
+ }
88
+ });
89
+ });
90
+ rl.on("close", () => {
91
+ queue.then(() => process.exit(0));
92
+ });
93
+ }
94
+ main();
95
+ //#endregion
96
+ export {};
@@ -0,0 +1,296 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { execFile, execSync } from "node:child_process";
6
+ import http from "node:http";
7
+ //#region src/lib/auth.ts
8
+ /**
9
+ * Templates ship at the package root. The built bundle lives one level down
10
+ * (`lib/*.mjs`) and the sources two (`src/lib/*.ts`), so both candidates are
11
+ * tried. Reading is deferred to the browser flow: nothing else in the plugin
12
+ * needs the pages, and a missing file must not break plugin load.
13
+ */
14
+ const TEMPLATE_DIRS = ["../templates/", "../../templates/"].map((relative) => fileURLToPath(new URL(relative, import.meta.url)));
15
+ function readTemplate(name) {
16
+ for (const dir of TEMPLATE_DIRS) try {
17
+ return fs.readFileSync(path.join(dir, name), "utf-8");
18
+ } catch {}
19
+ return `<!DOCTYPE html><html><body><p>Supermemory: ${name} is missing from this installation.</p></body></html>`;
20
+ }
21
+ const SETTINGS_DIR$1 = path.join(os.homedir(), ".supermemory-claude");
22
+ const CREDENTIALS_FILE = path.join(SETTINGS_DIR$1, "credentials.json");
23
+ const AUTH_BASE_URL = process.env.SUPERMEMORY_AUTH_URL || "https://console.supermemory.ai/auth/connect";
24
+ const AUTH_PORT = 19876;
25
+ const AUTH_TIMEOUT = 25e3;
26
+ function execFileAsync(command, args) {
27
+ return new Promise((resolve, reject) => {
28
+ execFile(command, args, { windowsHide: true }, (err) => {
29
+ if (err) reject(err);
30
+ else resolve();
31
+ });
32
+ });
33
+ }
34
+ async function openUrl(url) {
35
+ const target = url.toString();
36
+ if (!/^https?:\/\//i.test(target)) throw new Error("Refusing to open non-http URL");
37
+ if (process.platform === "win32") {
38
+ try {
39
+ await execFileAsync("rundll32.exe", ["url.dll,FileProtocolHandler", target]);
40
+ return;
41
+ } catch {}
42
+ await execFileAsync("cmd.exe", [
43
+ "/c",
44
+ "start",
45
+ "\"\"",
46
+ target
47
+ ]);
48
+ return;
49
+ }
50
+ if (process.platform === "darwin") {
51
+ await execFileAsync("open", [target]);
52
+ return;
53
+ }
54
+ await execFileAsync("xdg-open", [target]);
55
+ }
56
+ function ensureDir() {
57
+ if (!fs.existsSync(SETTINGS_DIR$1)) fs.mkdirSync(SETTINGS_DIR$1, { recursive: true });
58
+ }
59
+ function loadCredentials() {
60
+ try {
61
+ if (fs.existsSync(CREDENTIALS_FILE)) {
62
+ const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
63
+ if (data.apiKey) return data;
64
+ }
65
+ } catch {}
66
+ return null;
67
+ }
68
+ function saveCredentials(apiKey) {
69
+ ensureDir();
70
+ const data = {
71
+ apiKey,
72
+ savedAt: (/* @__PURE__ */ new Date()).toISOString()
73
+ };
74
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2));
75
+ }
76
+ /**
77
+ * Open the browser login and resolve with the API key the callback delivers.
78
+ * The loopback listener, port, and query parameters match the Claude Code
79
+ * plugin's flow, so a login started from either tool writes the same
80
+ * credentials file.
81
+ */
82
+ function startAuthFlow() {
83
+ return new Promise((resolve, reject) => {
84
+ let resolved = false;
85
+ const server = http.createServer((req, res) => {
86
+ const url = new URL(req.url ?? "/", `http://localhost:${AUTH_PORT}`);
87
+ if (url.pathname === "/callback") {
88
+ const apiKey = url.searchParams.get("apikey") || url.searchParams.get("api_key");
89
+ if (apiKey?.startsWith("sm_")) {
90
+ saveCredentials(apiKey);
91
+ res.writeHead(200, { "Content-Type": "text/html" });
92
+ res.end(readTemplate("auth-success.html"));
93
+ resolved = true;
94
+ server.close();
95
+ resolve(apiKey);
96
+ } else {
97
+ res.writeHead(400, { "Content-Type": "text/html" });
98
+ res.end(readTemplate("auth-error.html"));
99
+ }
100
+ } else {
101
+ res.writeHead(404);
102
+ res.end("Not found");
103
+ }
104
+ });
105
+ server.listen(AUTH_PORT, "127.0.0.1", () => {
106
+ openUrl(`${AUTH_BASE_URL}?callback=${encodeURIComponent(`http://localhost:${AUTH_PORT}/callback`)}&client=claude_code`).catch((error) => {
107
+ if (!resolved) {
108
+ server.close();
109
+ reject(/* @__PURE__ */ new Error(`Failed to open browser: ${error.message}`));
110
+ }
111
+ });
112
+ });
113
+ server.on("error", (err) => {
114
+ if (!resolved) reject(/* @__PURE__ */ new Error(`Failed to start auth server: ${err.message}`));
115
+ });
116
+ setTimeout(() => {
117
+ if (!resolved) {
118
+ server.close();
119
+ reject(/* @__PURE__ */ new Error("AUTH_TIMEOUT"));
120
+ }
121
+ }, AUTH_TIMEOUT).unref?.();
122
+ });
123
+ }
124
+ //#endregion
125
+ //#region src/lib/git-utils.ts
126
+ /**
127
+ * Resolve the repository root for `cwd`. Linked worktrees normally collapse to
128
+ * the main checkout so every worktree shares one memory container;
129
+ * `SUPERMEMORY_ISOLATE_WORKTREES=true` keeps them separate.
130
+ */
131
+ function getGitRoot(cwd) {
132
+ const isolateWorktrees = process.env.SUPERMEMORY_ISOLATE_WORKTREES === "true";
133
+ try {
134
+ if (isolateWorktrees) return execSync("git rev-parse --show-toplevel", {
135
+ cwd,
136
+ encoding: "utf-8",
137
+ stdio: [
138
+ "pipe",
139
+ "pipe",
140
+ "pipe"
141
+ ]
142
+ }).trim() || null;
143
+ const gitCommonDir = execSync("git rev-parse --git-common-dir", {
144
+ cwd,
145
+ encoding: "utf-8",
146
+ stdio: [
147
+ "pipe",
148
+ "pipe",
149
+ "pipe"
150
+ ]
151
+ }).trim();
152
+ if (gitCommonDir === ".git") return execSync("git rev-parse --show-toplevel", {
153
+ cwd,
154
+ encoding: "utf-8",
155
+ stdio: [
156
+ "pipe",
157
+ "pipe",
158
+ "pipe"
159
+ ]
160
+ }).trim() || null;
161
+ const resolved = path.resolve(cwd, gitCommonDir);
162
+ if (path.basename(resolved) === ".git" && !resolved.includes(`${path.sep}.git${path.sep}`)) return path.dirname(resolved);
163
+ return execSync("git rev-parse --show-toplevel", {
164
+ cwd,
165
+ encoding: "utf-8",
166
+ stdio: [
167
+ "pipe",
168
+ "pipe",
169
+ "pipe"
170
+ ]
171
+ }).trim() || null;
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+ //#endregion
177
+ //#region src/lib/project-config.ts
178
+ /**
179
+ * Project-local overrides live beside the Claude Code plugin's own file so a
180
+ * repository configured for one tool is already configured for the other.
181
+ */
182
+ const CONFIG_DIR = path.join(".claude", ".supermemory-claude");
183
+ const CONFIG_FILE = "config.json";
184
+ function getConfigPath(cwd) {
185
+ const basePath = getGitRoot(cwd) || cwd;
186
+ return path.join(basePath, CONFIG_DIR, CONFIG_FILE);
187
+ }
188
+ function loadProjectConfig(cwd) {
189
+ try {
190
+ const configPath = getConfigPath(cwd);
191
+ if (fs.existsSync(configPath)) return JSON.parse(fs.readFileSync(configPath, "utf-8"));
192
+ } catch {}
193
+ return null;
194
+ }
195
+ //#endregion
196
+ //#region src/lib/settings.ts
197
+ const BASE_URL = "https://api.supermemory.ai";
198
+ /**
199
+ * Shared with the Claude Code plugin on purpose: one browser login, one
200
+ * credentials file, and one settings document serve both harnesses.
201
+ */
202
+ const SETTINGS_DIR = path.join(os.homedir(), ".supermemory-claude");
203
+ const SETTINGS_FILE = path.join(SETTINGS_DIR, "settings.json");
204
+ const DEFAULT_SETTINGS = {
205
+ includeTools: [],
206
+ maxProfileItems: 5,
207
+ debug: false,
208
+ injectProfile: true,
209
+ recallDirective: null,
210
+ signalExtraction: false,
211
+ signalKeywords: [
212
+ "remember",
213
+ "implementation",
214
+ "refactor",
215
+ "architecture",
216
+ "decision",
217
+ "important",
218
+ "bug",
219
+ "fix",
220
+ "solved",
221
+ "solution",
222
+ "pattern",
223
+ "approach",
224
+ "design",
225
+ "tradeoff",
226
+ "migrate",
227
+ "upgrade",
228
+ "deprecate"
229
+ ],
230
+ signalTurnsBefore: 3
231
+ };
232
+ function loadSettings() {
233
+ const settings = { ...DEFAULT_SETTINGS };
234
+ try {
235
+ if (fs.existsSync(SETTINGS_FILE)) Object.assign(settings, JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8")));
236
+ } catch (err) {
237
+ console.error(`Settings: Failed to load ${SETTINGS_FILE}: ${err.message}`);
238
+ }
239
+ if (process.env.SUPERMEMORY_DEBUG === "true") settings.debug = true;
240
+ return settings;
241
+ }
242
+ function getApiKey(cwd, projectConfig) {
243
+ if (process.env.SUPERMEMORY_CC_API_KEY) return process.env.SUPERMEMORY_CC_API_KEY;
244
+ const resolved = projectConfig ?? loadProjectConfig(cwd || process.cwd());
245
+ if (resolved?.apiKey) return resolved.apiKey;
246
+ const credentials = loadCredentials();
247
+ if (credentials?.apiKey) return credentials.apiKey;
248
+ throw new Error("NO_API_KEY");
249
+ }
250
+ function normalizeBaseUrl(baseUrl) {
251
+ if (typeof baseUrl !== "string" || !baseUrl.trim()) return null;
252
+ const trimmed = baseUrl.trim();
253
+ try {
254
+ const url = new URL(trimmed);
255
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
256
+ return trimmed;
257
+ } catch {
258
+ return null;
259
+ }
260
+ }
261
+ function getBaseUrl(cwd, projectConfig) {
262
+ const resolved = projectConfig ?? loadProjectConfig(cwd || process.cwd());
263
+ const normalized = normalizeBaseUrl(process.env.SUPERMEMORY_API_URL || resolved?.baseUrl || BASE_URL);
264
+ if (!normalized) throw new Error("Invalid baseUrl: expected an absolute http(s) URL");
265
+ return normalized;
266
+ }
267
+ function debugLog(settings, message, data) {
268
+ if (settings.debug) {
269
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
270
+ console.error(data ? `[${timestamp}] ${message}: ${JSON.stringify(data)}` : `[${timestamp}] ${message}`);
271
+ }
272
+ }
273
+ function getIncludeTools(cwd) {
274
+ const settings = loadSettings();
275
+ const projectConfig = loadProjectConfig(cwd || process.cwd());
276
+ return [.../* @__PURE__ */ new Set([...settings.includeTools || [], ...projectConfig?.includeTools || []])].map((t) => t.toLowerCase());
277
+ }
278
+ function shouldIncludeTool(toolName, includeList) {
279
+ if (includeList.length === 0) return false;
280
+ return includeList.includes(toolName.toLowerCase());
281
+ }
282
+ function getSignalConfig(cwd) {
283
+ const settings = loadSettings();
284
+ const projectConfig = loadProjectConfig(cwd || process.cwd());
285
+ return {
286
+ enabled: projectConfig?.signalExtraction !== void 0 ? projectConfig.signalExtraction : settings.signalExtraction || false,
287
+ keywords: [.../* @__PURE__ */ new Set([...settings.signalKeywords || DEFAULT_SETTINGS.signalKeywords, ...projectConfig?.signalKeywords || []])].map((k) => k.toLowerCase()),
288
+ turnsBefore: projectConfig?.signalTurnsBefore || settings.signalTurnsBefore || DEFAULT_SETTINGS.signalTurnsBefore
289
+ };
290
+ }
291
+ function getRecallConfig(cwd) {
292
+ const settings = loadSettings();
293
+ return { directive: loadProjectConfig(cwd || process.cwd())?.recallDirective || settings.recallDirective || null };
294
+ }
295
+ //#endregion
296
+ export { getIncludeTools as a, loadSettings as c, getGitRoot as d, AUTH_BASE_URL as f, startAuthFlow as h, getBaseUrl as i, shouldIncludeTool as l, loadCredentials as m, debugLog as n, getRecallConfig as o, CREDENTIALS_FILE as p, getApiKey as r, getSignalConfig as s, SETTINGS_FILE as t, loadProjectConfig as u };
@@ -0,0 +1,35 @@
1
+ import { Readable } from "node:stream";
2
+ //#region src/statusline.d.ts
3
+ declare const SAVING_TTL_MS: number;
4
+ declare const ERROR_TTL_MS: number;
5
+ declare const CONTEXT_TTL_MS: number;
6
+ declare const STATUSLINE_INPUT_TIMEOUT_MS = 500;
7
+ declare const TICK_MS = 1000;
8
+ interface StateRecord {
9
+ version?: number;
10
+ event?: string;
11
+ updatedAt?: number;
12
+ status?: string;
13
+ memoryItemsLoaded?: number;
14
+ count?: number;
15
+ results?: number;
16
+ memories?: number;
17
+ }
18
+ interface StatuslineState {
19
+ context?: StateRecord | null;
20
+ capture?: StateRecord | null;
21
+ search?: StateRecord | null;
22
+ }
23
+ declare function readState(sessionId: unknown): StatuslineState;
24
+ declare function getStatusLabel(state: StatuslineState, now?: number): string | null;
25
+ declare function renderStatusline(state: StatuslineState, options?: {
26
+ now?: number;
27
+ color?: boolean;
28
+ }): string;
29
+ declare function readStatuslineInput(input?: Readable & {
30
+ isTTY?: boolean;
31
+ }, options?: {
32
+ timeoutMs?: number;
33
+ }): Promise<Record<string, unknown>>;
34
+ //#endregion
35
+ export { CONTEXT_TTL_MS, ERROR_TTL_MS, SAVING_TTL_MS, STATUSLINE_INPUT_TIMEOUT_MS, StatuslineState, TICK_MS, getStatusLabel, readState, readStatuslineInput, renderStatusline };
@@ -0,0 +1,203 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import crypto from "node:crypto";
5
+ //#region src/statusline.ts
6
+ /**
7
+ * Self-contained statusline renderer. Reads per-session state written by the
8
+ * plugin from the fixed ~/.supermemory-claude/statusline directory.
9
+ *
10
+ * DSH's own status line is host-owned, so this renderer is not installed into
11
+ * a harness setting the way the Claude Code plugin installs one. It stays a
12
+ * first-class entry point (`dsh-supermemory/statusline`) so any status bar that
13
+ * can run a command — tmux, starship, a wrapper shell, or Claude Code itself —
14
+ * renders the identical line from the identical state.
15
+ */
16
+ const STATE_ROOT = path.join(os.homedir(), ".supermemory-claude", "statusline", "statusline-state");
17
+ const SCHEMA_VERSION = 1;
18
+ const SAVING_TTL_MS = 3e4;
19
+ const ERROR_TTL_MS = 6e4;
20
+ const CONTEXT_TTL_MS = 864e5;
21
+ const STATUSLINE_INPUT_TIMEOUT_MS = 500;
22
+ const BLUE = "\x1B[38;2;124;120;250m";
23
+ const WHITE = "\x1B[97m";
24
+ const BOLD = "\x1B[1m";
25
+ const RESET = "\x1B[0m";
26
+ const TICK_MS = 1e3;
27
+ const EMPHASIS_TICKS = 2;
28
+ const PANE_TICKS = 4;
29
+ const CREST_STRIDE = 3;
30
+ const SPINNER_STRIDE = 3;
31
+ const SPINNER = [
32
+ "⠋",
33
+ "⠙",
34
+ "⠹",
35
+ "⠸",
36
+ "⠼",
37
+ "⠴",
38
+ "⠦",
39
+ "⠧",
40
+ "⠇",
41
+ "⠏"
42
+ ];
43
+ const SHIMMER_HI = "\x1B[38;2;224;223;255m";
44
+ const SHIMMER_MID = "\x1B[38;2;170;167;255m";
45
+ const GRAY = "\x1B[38;5;245m";
46
+ function formatAge(ms) {
47
+ const s = Math.max(1, Math.floor(ms / 1e3));
48
+ if (s < 60) return `${s}s`;
49
+ const m = Math.floor(s / 60);
50
+ return m < 60 ? `${m}m` : `${Math.floor(m / 60)}h`;
51
+ }
52
+ function shimmer(word, tick) {
53
+ const crest = tick * CREST_STRIDE % word.length;
54
+ let out = "";
55
+ for (let i = 0; i < word.length; i++) {
56
+ const d = Math.abs(i - crest);
57
+ out += (d === 0 ? SHIMMER_HI : d <= 2 ? SHIMMER_MID : BLUE) + word[i];
58
+ }
59
+ return out + RESET;
60
+ }
61
+ function readEvent(sessionDir, event) {
62
+ try {
63
+ const record = JSON.parse(fs.readFileSync(path.join(sessionDir, `${event}.json`), "utf8"));
64
+ if (record?.version !== SCHEMA_VERSION || record?.event !== event || !Number.isFinite(record?.updatedAt)) return null;
65
+ return record;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ function readState(sessionId) {
71
+ if (typeof sessionId !== "string" || !sessionId.trim()) return {};
72
+ const sessionDir = path.join(STATE_ROOT, crypto.createHash("sha256").update(sessionId.trim()).digest("hex"));
73
+ return {
74
+ context: readEvent(sessionDir, "context"),
75
+ capture: readEvent(sessionDir, "capture"),
76
+ search: readEvent(sessionDir, "search")
77
+ };
78
+ }
79
+ function isFresh(record, ttl, now, contextUpdatedAt = 0) {
80
+ return Boolean(record && typeof record.updatedAt === "number" && record.updatedAt >= contextUpdatedAt && now - record.updatedAt >= 0 && now - record.updatedAt < ttl);
81
+ }
82
+ function getStatus(state, now) {
83
+ const { context, capture, search } = state;
84
+ const generation = Number.isFinite(context?.updatedAt) ? context.updatedAt : 0;
85
+ if (capture?.status === "saving" && isFresh(capture, 3e4, now, generation)) return { kind: "saving" };
86
+ if (capture?.status === "error" && isFresh(capture, 6e4, now, generation)) return { kind: "error" };
87
+ const contextReady = isFresh(context, 864e5, now) && context.status === "ready";
88
+ const parts = [];
89
+ if (contextReady && (context.memoryItemsLoaded ?? 0) > 0) parts.push(`${context.memoryItemsLoaded} loaded`);
90
+ let savedAt;
91
+ let recalledAt;
92
+ if ((capture?.count ?? 0) > 0 && (capture.updatedAt ?? 0) >= generation) {
93
+ parts.push(`${capture.count} captured`);
94
+ savedAt = capture.updatedAt;
95
+ }
96
+ if ((search?.count ?? 0) > 0 && (search.updatedAt ?? 0) >= generation) {
97
+ parts.push((search.memories ?? 0) > 0 ? `${search.memories} recalled` : `${search.count} ${search.count === 1 ? "recall" : "recalls"}`);
98
+ recalledAt = search.updatedAt;
99
+ }
100
+ if (parts.length > 0) return {
101
+ kind: "tally",
102
+ parts,
103
+ ...savedAt !== void 0 ? { savedAt } : {},
104
+ ...recalledAt !== void 0 ? { recalledAt } : {}
105
+ };
106
+ return contextReady ? { kind: "ready" } : null;
107
+ }
108
+ function getStatusLabel(state, now = Date.now()) {
109
+ const status = getStatus(state, now);
110
+ if (!status) return null;
111
+ if (status.kind === "saving") return "saving session";
112
+ if (status.kind === "error") return "session sync failed";
113
+ if (status.kind === "ready") return "ready";
114
+ return status.parts.join(" · ");
115
+ }
116
+ function renderStatusline(state, options = {}) {
117
+ const now = options.now ?? Date.now();
118
+ const status = getStatus(state, now);
119
+ if (!status) return "";
120
+ if (options.color === false) return `◪ supermemory · ${getStatusLabel(state, now)}`;
121
+ const tick = Math.floor(now / TICK_MS);
122
+ const brand = `${BLUE}${BOLD}◪${RESET} ${BOLD}${shimmer("supermemory", tick)}${RESET}`;
123
+ if (status.kind === "saving") {
124
+ const spin = SPINNER[tick * SPINNER_STRIDE % SPINNER.length];
125
+ return `${brand} ${BLUE}${spin}${RESET} ${WHITE}saving session${RESET}`;
126
+ }
127
+ if (status.kind === "error") return `${brand} ${WHITE}· session sync failed${RESET}`;
128
+ if (status.kind === "ready") return `${brand} ${WHITE}· ready${RESET}`;
129
+ const panes = [null];
130
+ if (status.savedAt) panes.push(`saved ${formatAge(now - status.savedAt)} ago`);
131
+ if (status.recalledAt) panes.push(`recalled ${formatAge(now - status.recalledAt)} ago`);
132
+ const pane = panes[Math.floor(tick / PANE_TICKS) % panes.length];
133
+ if (pane) return `${brand} ${WHITE}·${RESET} ${WHITE}${pane}${RESET}`;
134
+ const emphasized = Math.floor(tick / EMPHASIS_TICKS) % status.parts.length;
135
+ const parts = status.parts.map((part, i) => i === emphasized ? `${WHITE}${BOLD}${part}${RESET}` : `${GRAY}${part}${RESET}`);
136
+ return `${brand} ${WHITE}·${RESET} ${parts.join(`${GRAY} · ${RESET}`)}`;
137
+ }
138
+ function readStatuslineInput(input = process.stdin, options = {}) {
139
+ const timeoutMs = options.timeoutMs ?? 500;
140
+ return new Promise((resolve, reject) => {
141
+ let data = "";
142
+ let settled = false;
143
+ let timer;
144
+ const cleanup = () => {
145
+ clearTimeout(timer);
146
+ input.off("data", onData);
147
+ input.off("end", onEnd);
148
+ input.off("error", onError);
149
+ try {
150
+ input.pause();
151
+ input.unref?.();
152
+ } catch {}
153
+ };
154
+ const finish = (callback, value) => {
155
+ if (settled) return;
156
+ settled = true;
157
+ cleanup();
158
+ callback(value);
159
+ };
160
+ const parse = (final) => {
161
+ const value = data.trim();
162
+ if (!value) {
163
+ if (final) finish(resolve, {});
164
+ return;
165
+ }
166
+ try {
167
+ finish(resolve, JSON.parse(value));
168
+ } catch (error) {
169
+ if (final) finish(reject, /* @__PURE__ */ new Error(`Failed to parse statusline JSON: ${error.message}`));
170
+ }
171
+ };
172
+ function onData(chunk) {
173
+ data += chunk;
174
+ parse(false);
175
+ }
176
+ function onEnd() {
177
+ parse(true);
178
+ }
179
+ function onError(error) {
180
+ finish(reject, error);
181
+ }
182
+ input.setEncoding("utf8");
183
+ input.on("data", onData);
184
+ input.on("end", onEnd);
185
+ input.on("error", onError);
186
+ if (input.isTTY) {
187
+ finish(resolve, {});
188
+ return;
189
+ }
190
+ if (!settled) timer = setTimeout(() => parse(true), timeoutMs);
191
+ });
192
+ }
193
+ async function main() {
194
+ try {
195
+ const sessionId = (await readStatuslineInput()).session_id;
196
+ if (!sessionId) return;
197
+ const output = renderStatusline(readState(sessionId));
198
+ if (output) process.stdout.write(output);
199
+ } catch {}
200
+ }
201
+ if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`) main();
202
+ //#endregion
203
+ export { CONTEXT_TTL_MS, ERROR_TTL_MS, SAVING_TTL_MS, STATUSLINE_INPUT_TIMEOUT_MS, TICK_MS, getStatusLabel, readState, readStatuslineInput, renderStatusline };