stackmem 1.0.22 → 1.0.25
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.
- package/dist/cli.js +97 -2
- package/dist/config.js +3 -0
- package/dist/storage.js +6 -0
- package/package.json +1 -1
- package/src/cli.ts +112 -2
- package/src/config.ts +4 -0
- package/src/storage.ts +7 -0
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import nodeFs from "node:fs";
|
|
3
3
|
import nodeOs from "node:os";
|
|
4
4
|
import nodePath from "node:path";
|
|
5
|
+
import { randomInt } from "node:crypto";
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
7
|
import { createInterface } from "node:readline/promises";
|
|
7
8
|
import { analyzeCodebase } from "./ast-index.js";
|
|
9
|
+
import { VAULT_URL } from "./config.js";
|
|
8
10
|
import { getDeviceId } from "./device.js";
|
|
9
11
|
import { applyContextBudget, buildSummaryContent, clusterMemories, computeDecayScore, computeMemoryLinks, decideSaveAction, estimateTokens, extractEntities, indicatesFailedApproach, isAuthRelatedFile, isCodeFile, isQualityMessage, redactSecrets, scoreMemoryRelevance, } from "./scoring.js";
|
|
10
12
|
import { supabase } from "./storage.js";
|
|
@@ -43,6 +45,7 @@ function usage() {
|
|
|
43
45
|
cm compress [project]
|
|
44
46
|
cm init <project> <path>
|
|
45
47
|
cm uninstall [project] [path]
|
|
48
|
+
cm link
|
|
46
49
|
cm sync
|
|
47
50
|
cm context [--project <name>] <task>
|
|
48
51
|
cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
|
|
@@ -130,6 +133,13 @@ function cmdHelp() {
|
|
|
130
133
|
Also removes the stackmem entry from ~/.claude.json's mcpServers.
|
|
131
134
|
[project] defaults to the .stackmem file at <path> if omitted.
|
|
132
135
|
|
|
136
|
+
cm link
|
|
137
|
+
Connect this device to your GitHub account in the hosted vault.
|
|
138
|
+
Prints a 6-digit code, then waits for you to enter it at
|
|
139
|
+
stackmem-vault.vercel.app/link while signed in. The code is
|
|
140
|
+
single-use and expires after 10 minutes; polling stops when it
|
|
141
|
+
does. Links are per-device, so run this once on each machine.
|
|
142
|
+
|
|
133
143
|
cm sync
|
|
134
144
|
Regenerate .cursorrules and .windsurfrules from the project's
|
|
135
145
|
current unresolved memories (decay-ranked, capped at a 2000
|
|
@@ -1472,6 +1482,73 @@ async function cmdSync(project) {
|
|
|
1472
1482
|
nodeFs.writeFileSync(nodePath.join(resolvedPath, ".windsurfrules"), rulesContent, "utf-8");
|
|
1473
1483
|
console.log("Synced memories to .cursorrules and .windsurfrules");
|
|
1474
1484
|
}
|
|
1485
|
+
// --- cm link ---------------------------------------------------------------
|
|
1486
|
+
// Mirrors the link_codes.expires_at default in the migration
|
|
1487
|
+
// (supabase/migrations/001_user_devices_and_link_codes.sql in the vault repo).
|
|
1488
|
+
// If that interval changes, change this too — polling past expires_at just
|
|
1489
|
+
// burns requests against a row the API will refuse.
|
|
1490
|
+
const LINK_CODE_TTL_MS = 10 * 60 * 1000;
|
|
1491
|
+
const LINK_POLL_INTERVAL_MS = 3_000;
|
|
1492
|
+
const LINK_CODE_INSERT_ATTEMPTS = 5;
|
|
1493
|
+
const PG_UNIQUE_VIOLATION = "23505";
|
|
1494
|
+
function sleep(ms) {
|
|
1495
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1496
|
+
}
|
|
1497
|
+
// randomInt is a CSPRNG and uniform over the range. Math.random() would do
|
|
1498
|
+
// neither, and a link code is a short-lived credential for claiming this
|
|
1499
|
+
// device's memories — a guessable one is a hijack.
|
|
1500
|
+
function generateLinkCode() {
|
|
1501
|
+
return String(randomInt(0, 1_000_000)).padStart(6, "0");
|
|
1502
|
+
}
|
|
1503
|
+
async function cmdLink() {
|
|
1504
|
+
const deviceId = getDeviceId();
|
|
1505
|
+
// A 6-digit space collides rarely, but only unclaimed codes occupy it and
|
|
1506
|
+
// the primary key makes a collision a hard error rather than a silent
|
|
1507
|
+
// overwrite — so retry with a fresh code instead of failing the command.
|
|
1508
|
+
let code = null;
|
|
1509
|
+
for (let attempt = 0; attempt < LINK_CODE_INSERT_ATTEMPTS; attempt++) {
|
|
1510
|
+
const candidate = generateLinkCode();
|
|
1511
|
+
const { error } = await supabase
|
|
1512
|
+
.from("link_codes")
|
|
1513
|
+
.insert({ code: candidate, device_id: deviceId });
|
|
1514
|
+
if (!error) {
|
|
1515
|
+
code = candidate;
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
if (error.code !== PG_UNIQUE_VIOLATION) {
|
|
1519
|
+
fail(`Failed to create a link code: ${error.message}`);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
if (!code) {
|
|
1523
|
+
fail("Could not get an unused link code. Try again in a minute.");
|
|
1524
|
+
}
|
|
1525
|
+
console.log(`Go to ${VAULT_URL}/link and enter code: ${code}`);
|
|
1526
|
+
const deadline = Date.now() + LINK_CODE_TTL_MS;
|
|
1527
|
+
while (Date.now() < deadline) {
|
|
1528
|
+
await sleep(LINK_POLL_INTERVAL_MS);
|
|
1529
|
+
const { data, error } = await supabase
|
|
1530
|
+
.from("link_codes")
|
|
1531
|
+
.select("claimed_at")
|
|
1532
|
+
.eq("code", code)
|
|
1533
|
+
.maybeSingle();
|
|
1534
|
+
// A transient read failure shouldn't abandon a link the user may have
|
|
1535
|
+
// already completed in the browser — keep polling until the code expires.
|
|
1536
|
+
if (error)
|
|
1537
|
+
continue;
|
|
1538
|
+
// The row is gone: either housekeeping swept it or it was never visible
|
|
1539
|
+
// to this device. Either way there's nothing left to claim.
|
|
1540
|
+
if (!data)
|
|
1541
|
+
break;
|
|
1542
|
+
if (data.claimed_at) {
|
|
1543
|
+
console.log("Device linked to your GitHub account.");
|
|
1544
|
+
// Best-effort cleanup — the code is spent either way, and the API
|
|
1545
|
+
// rejects claimed codes, so a failure here is harmless.
|
|
1546
|
+
await supabase.from("link_codes").delete().eq("code", code);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
console.log("Link code expired. Run cm link again.");
|
|
1551
|
+
}
|
|
1475
1552
|
// --- cm context ------------------------------------------------------------
|
|
1476
1553
|
const CONTEXT_TYPE_ORDER = ["decision", "constraint", "rejection", "discovery"];
|
|
1477
1554
|
const CONTEXT_TOP_N = 5;
|
|
@@ -2065,6 +2142,12 @@ async function main() {
|
|
|
2065
2142
|
await cmdInit(project, targetPath);
|
|
2066
2143
|
break;
|
|
2067
2144
|
}
|
|
2145
|
+
case "link": {
|
|
2146
|
+
if (args.length > 0)
|
|
2147
|
+
usage();
|
|
2148
|
+
await cmdLink();
|
|
2149
|
+
break;
|
|
2150
|
+
}
|
|
2068
2151
|
case "sync": {
|
|
2069
2152
|
if (args.length > 0)
|
|
2070
2153
|
usage();
|
|
@@ -2149,8 +2232,20 @@ async function main() {
|
|
|
2149
2232
|
}
|
|
2150
2233
|
// Guards the auto-run so importing this module (e.g. from a test file, to
|
|
2151
2234
|
// reach exported helpers like trySeedMemory) doesn't also execute main()
|
|
2152
|
-
// against the test runner's own argv.
|
|
2153
|
-
|
|
2235
|
+
// against the test runner's own argv. realpathSync matters here: the `cm`
|
|
2236
|
+
// bin is typically a symlink (npm link, or the global install this project
|
|
2237
|
+
// itself uses), and process.argv[1] preserves the symlink path as invoked
|
|
2238
|
+
// while import.meta.url resolves to the link's real target — comparing
|
|
2239
|
+
// them raw would never match, silently skipping main() entirely.
|
|
2240
|
+
function resolveMainModulePath(argvPath) {
|
|
2241
|
+
try {
|
|
2242
|
+
return pathToFileURL(nodeFs.realpathSync(argvPath)).href;
|
|
2243
|
+
}
|
|
2244
|
+
catch {
|
|
2245
|
+
return null;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
const isMainModule = process.argv[1] != null && import.meta.url === resolveMainModulePath(process.argv[1]);
|
|
2154
2249
|
if (isMainModule) {
|
|
2155
2250
|
main().catch((error) => {
|
|
2156
2251
|
fail(error instanceof Error ? error.message : String(error));
|
package/dist/config.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
// Where the hosted vault lives. `cm link` prints a URL under this origin for
|
|
2
|
+
// the user to open in a browser, so it has to be the deployed domain.
|
|
3
|
+
export const VAULT_URL = "https://stackmem-vault.vercel.app";
|
|
1
4
|
export const SUPABASE_URL = "https://vuplpnwwygojkwxwsnip.supabase.co";
|
|
2
5
|
export const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZ1cGxwbnd3eWdvamt3eHdzbmlwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3OTA0NjU2MjIsImV4cCI6MjEwNjA0MTYyMn0.gIIWuALWlOAzKRD3AAQVHnCNq-o4mVOHOgHJKc0fmYs";
|
package/dist/storage.js
CHANGED
|
@@ -51,3 +51,9 @@ export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
|
51
51
|
//
|
|
52
52
|
// create index memory_links_source_id_idx on memory_links(source_id);
|
|
53
53
|
// create index memory_links_target_id_idx on memory_links(target_id);
|
|
54
|
+
// `cm link` (src/cli.ts) writes to a link_codes table, and the vault pairs
|
|
55
|
+
// devices to GitHub accounts via user_devices. The vault owns that schema —
|
|
56
|
+
// run supabase/migrations/001_user_devices_and_link_codes.sql from the
|
|
57
|
+
// stackmem-vault repo. link_codes is RLS-scoped to the same 'x-device-id'
|
|
58
|
+
// header as the policies above, so a device can create and poll only its own
|
|
59
|
+
// codes; the vault reads them with the service-role key.
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
import nodeFs from "node:fs";
|
|
3
3
|
import nodeOs from "node:os";
|
|
4
4
|
import nodePath from "node:path";
|
|
5
|
+
import { randomInt } from "node:crypto";
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
7
|
import { createInterface } from "node:readline/promises";
|
|
7
8
|
import { analyzeCodebase } from "./ast-index.js";
|
|
9
|
+
import { VAULT_URL } from "./config.js";
|
|
8
10
|
import { getDeviceId } from "./device.js";
|
|
9
11
|
import {
|
|
10
12
|
applyContextBudget,
|
|
@@ -64,6 +66,7 @@ function usage(): never {
|
|
|
64
66
|
cm compress [project]
|
|
65
67
|
cm init <project> <path>
|
|
66
68
|
cm uninstall [project] [path]
|
|
69
|
+
cm link
|
|
67
70
|
cm sync
|
|
68
71
|
cm context [--project <name>] <task>
|
|
69
72
|
cm session-end [project] [--summary <text>] [--errors <a,b>] [--files <a,b>] [--approaches <a,b>]
|
|
@@ -152,6 +155,13 @@ function cmdHelp() {
|
|
|
152
155
|
Also removes the stackmem entry from ~/.claude.json's mcpServers.
|
|
153
156
|
[project] defaults to the .stackmem file at <path> if omitted.
|
|
154
157
|
|
|
158
|
+
cm link
|
|
159
|
+
Connect this device to your GitHub account in the hosted vault.
|
|
160
|
+
Prints a 6-digit code, then waits for you to enter it at
|
|
161
|
+
stackmem-vault.vercel.app/link while signed in. The code is
|
|
162
|
+
single-use and expires after 10 minutes; polling stops when it
|
|
163
|
+
does. Links are per-device, so run this once on each machine.
|
|
164
|
+
|
|
155
165
|
cm sync
|
|
156
166
|
Regenerate .cursorrules and .windsurfrules from the project's
|
|
157
167
|
current unresolved memories (decay-ranked, capped at a 2000
|
|
@@ -1689,6 +1699,88 @@ async function cmdSync(project: string) {
|
|
|
1689
1699
|
console.log("Synced memories to .cursorrules and .windsurfrules");
|
|
1690
1700
|
}
|
|
1691
1701
|
|
|
1702
|
+
// --- cm link ---------------------------------------------------------------
|
|
1703
|
+
|
|
1704
|
+
// Mirrors the link_codes.expires_at default in the migration
|
|
1705
|
+
// (supabase/migrations/001_user_devices_and_link_codes.sql in the vault repo).
|
|
1706
|
+
// If that interval changes, change this too — polling past expires_at just
|
|
1707
|
+
// burns requests against a row the API will refuse.
|
|
1708
|
+
const LINK_CODE_TTL_MS = 10 * 60 * 1000;
|
|
1709
|
+
const LINK_POLL_INTERVAL_MS = 3_000;
|
|
1710
|
+
const LINK_CODE_INSERT_ATTEMPTS = 5;
|
|
1711
|
+
const PG_UNIQUE_VIOLATION = "23505";
|
|
1712
|
+
|
|
1713
|
+
function sleep(ms: number): Promise<void> {
|
|
1714
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// randomInt is a CSPRNG and uniform over the range. Math.random() would do
|
|
1718
|
+
// neither, and a link code is a short-lived credential for claiming this
|
|
1719
|
+
// device's memories — a guessable one is a hijack.
|
|
1720
|
+
function generateLinkCode(): string {
|
|
1721
|
+
return String(randomInt(0, 1_000_000)).padStart(6, "0");
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
async function cmdLink() {
|
|
1725
|
+
const deviceId = getDeviceId();
|
|
1726
|
+
|
|
1727
|
+
// A 6-digit space collides rarely, but only unclaimed codes occupy it and
|
|
1728
|
+
// the primary key makes a collision a hard error rather than a silent
|
|
1729
|
+
// overwrite — so retry with a fresh code instead of failing the command.
|
|
1730
|
+
let code: string | null = null;
|
|
1731
|
+
for (let attempt = 0; attempt < LINK_CODE_INSERT_ATTEMPTS; attempt++) {
|
|
1732
|
+
const candidate = generateLinkCode();
|
|
1733
|
+
const { error } = await supabase
|
|
1734
|
+
.from("link_codes")
|
|
1735
|
+
.insert({ code: candidate, device_id: deviceId });
|
|
1736
|
+
|
|
1737
|
+
if (!error) {
|
|
1738
|
+
code = candidate;
|
|
1739
|
+
break;
|
|
1740
|
+
}
|
|
1741
|
+
if (error.code !== PG_UNIQUE_VIOLATION) {
|
|
1742
|
+
fail(`Failed to create a link code: ${error.message}`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (!code) {
|
|
1747
|
+
fail("Could not get an unused link code. Try again in a minute.");
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
console.log(`Go to ${VAULT_URL}/link and enter code: ${code}`);
|
|
1751
|
+
|
|
1752
|
+
const deadline = Date.now() + LINK_CODE_TTL_MS;
|
|
1753
|
+
|
|
1754
|
+
while (Date.now() < deadline) {
|
|
1755
|
+
await sleep(LINK_POLL_INTERVAL_MS);
|
|
1756
|
+
|
|
1757
|
+
const { data, error } = await supabase
|
|
1758
|
+
.from("link_codes")
|
|
1759
|
+
.select("claimed_at")
|
|
1760
|
+
.eq("code", code)
|
|
1761
|
+
.maybeSingle();
|
|
1762
|
+
|
|
1763
|
+
// A transient read failure shouldn't abandon a link the user may have
|
|
1764
|
+
// already completed in the browser — keep polling until the code expires.
|
|
1765
|
+
if (error) continue;
|
|
1766
|
+
|
|
1767
|
+
// The row is gone: either housekeeping swept it or it was never visible
|
|
1768
|
+
// to this device. Either way there's nothing left to claim.
|
|
1769
|
+
if (!data) break;
|
|
1770
|
+
|
|
1771
|
+
if (data.claimed_at) {
|
|
1772
|
+
console.log("Device linked to your GitHub account.");
|
|
1773
|
+
|
|
1774
|
+
// Best-effort cleanup — the code is spent either way, and the API
|
|
1775
|
+
// rejects claimed codes, so a failure here is harmless.
|
|
1776
|
+
await supabase.from("link_codes").delete().eq("code", code);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
console.log("Link code expired. Run cm link again.");
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1692
1784
|
// --- cm context ------------------------------------------------------------
|
|
1693
1785
|
|
|
1694
1786
|
const CONTEXT_TYPE_ORDER: MemoryType[] = ["decision", "constraint", "rejection", "discovery"];
|
|
@@ -2338,6 +2430,11 @@ async function main() {
|
|
|
2338
2430
|
await cmdInit(project, targetPath);
|
|
2339
2431
|
break;
|
|
2340
2432
|
}
|
|
2433
|
+
case "link": {
|
|
2434
|
+
if (args.length > 0) usage();
|
|
2435
|
+
await cmdLink();
|
|
2436
|
+
break;
|
|
2437
|
+
}
|
|
2341
2438
|
case "sync": {
|
|
2342
2439
|
if (args.length > 0) usage();
|
|
2343
2440
|
const project = requireProject(null);
|
|
@@ -2418,8 +2515,21 @@ async function main() {
|
|
|
2418
2515
|
|
|
2419
2516
|
// Guards the auto-run so importing this module (e.g. from a test file, to
|
|
2420
2517
|
// reach exported helpers like trySeedMemory) doesn't also execute main()
|
|
2421
|
-
// against the test runner's own argv.
|
|
2422
|
-
|
|
2518
|
+
// against the test runner's own argv. realpathSync matters here: the `cm`
|
|
2519
|
+
// bin is typically a symlink (npm link, or the global install this project
|
|
2520
|
+
// itself uses), and process.argv[1] preserves the symlink path as invoked
|
|
2521
|
+
// while import.meta.url resolves to the link's real target — comparing
|
|
2522
|
+
// them raw would never match, silently skipping main() entirely.
|
|
2523
|
+
function resolveMainModulePath(argvPath: string): string | null {
|
|
2524
|
+
try {
|
|
2525
|
+
return pathToFileURL(nodeFs.realpathSync(argvPath)).href;
|
|
2526
|
+
} catch {
|
|
2527
|
+
return null;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
const isMainModule =
|
|
2532
|
+
process.argv[1] != null && import.meta.url === resolveMainModulePath(process.argv[1]);
|
|
2423
2533
|
if (isMainModule) {
|
|
2424
2534
|
main().catch((error) => {
|
|
2425
2535
|
fail(error instanceof Error ? error.message : String(error));
|
package/src/config.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Where the hosted vault lives. `cm link` prints a URL under this origin for
|
|
2
|
+
// the user to open in a browser, so it has to be the deployed domain.
|
|
3
|
+
export const VAULT_URL = "https://stackmem-vault.vercel.app";
|
|
4
|
+
|
|
1
5
|
export const SUPABASE_URL = "https://vuplpnwwygojkwxwsnip.supabase.co";
|
|
2
6
|
export const SUPABASE_ANON_KEY =
|
|
3
7
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZ1cGxwbnd3eWdvamt3eHdzbmlwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3OTA0NjU2MjIsImV4cCI6MjEwNjA0MTYyMn0.gIIWuALWlOAzKRD3AAQVHnCNq-o4mVOHOgHJKc0fmYs";
|
package/src/storage.ts
CHANGED
|
@@ -55,3 +55,10 @@ export const supabase: SupabaseClient = createClient(SUPABASE_URL, SUPABASE_ANON
|
|
|
55
55
|
//
|
|
56
56
|
// create index memory_links_source_id_idx on memory_links(source_id);
|
|
57
57
|
// create index memory_links_target_id_idx on memory_links(target_id);
|
|
58
|
+
|
|
59
|
+
// `cm link` (src/cli.ts) writes to a link_codes table, and the vault pairs
|
|
60
|
+
// devices to GitHub accounts via user_devices. The vault owns that schema —
|
|
61
|
+
// run supabase/migrations/001_user_devices_and_link_codes.sql from the
|
|
62
|
+
// stackmem-vault repo. link_codes is RLS-scoped to the same 'x-device-id'
|
|
63
|
+
// header as the policies above, so a device can create and poll only its own
|
|
64
|
+
// codes; the vault reads them with the service-role key.
|