Skip to content

Commit 867738c

Browse files
author
StackMemory Bot (CLI)
committed
feat(sdk): add @stackmemoryai/sdk TypeScript package
Self-contained SDK at packages/sdk/ with typed facade over: - ContentCache (SHA-256 dedup, token savings tracking) - SkillPackRegistry (install/search/list, FTS5, pack.yaml parser) - ProvenanceStore (TraceEvent spec, lineage, supersession) - scoreConfidence() (decision detection, weighted signals) Usage: new StackMemory({ dataDir }) → sm.cache / sm.packs / sm.provenance 16 tests passing. Zero type errors. Ready for npm publish.
1 parent 3d0761d commit 867738c

13 files changed

Lines changed: 3667 additions & 0 deletions

packages/sdk/package-lock.json

Lines changed: 2119 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sdk/package.json

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"name": "@stackmemoryai/sdk",
3+
"version": "0.1.0",
4+
"description": "TypeScript SDK for StackMemory — content cache, skill packs, and provenance tracking",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"type": "module",
8+
"files": [
9+
"dist"
10+
],
11+
"exports": {
12+
".": {
13+
"types": "./dist/index.d.ts",
14+
"import": "./dist/index.js"
15+
}
16+
},
17+
"scripts": {
18+
"build": "tsc",
19+
"test": "vitest run",
20+
"test:watch": "vitest",
21+
"lint": "eslint src/"
22+
},
23+
"dependencies": {
24+
"better-sqlite3": "^11.8.1",
25+
"js-yaml": "^4.1.0",
26+
"zod": "^3.24.2"
27+
},
28+
"devDependencies": {
29+
"@types/better-sqlite3": "^7.6.8",
30+
"@types/js-yaml": "^4.0.9",
31+
"@types/node": "^22.13.10",
32+
"typescript": "^5.8.2",
33+
"vitest": "^3.0.9"
34+
},
35+
"keywords": [
36+
"stackmemory",
37+
"mcp",
38+
"skill-packs",
39+
"provenance",
40+
"token-cache",
41+
"ai",
42+
"llm"
43+
],
44+
"license": "MIT"
45+
}
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { describe, it, expect, afterEach } from 'vitest';
2+
import { StackMemory } from '../stackmemory.js';
3+
import { scoreConfidence } from '../confidence-scorer.js';
4+
import { estimateTokens, hashContent } from '../token-estimator.js';
5+
import * as fs from 'fs';
6+
import * as path from 'path';
7+
import * as os from 'os';
8+
9+
function tmpDir(): string {
10+
return fs.mkdtempSync(path.join(os.tmpdir(), 'sm-sdk-test-'));
11+
}
12+
13+
describe('StackMemory SDK', () => {
14+
let sm: StackMemory;
15+
let dir: string;
16+
17+
afterEach(() => {
18+
sm?.close();
19+
if (dir) fs.rmSync(dir, { recursive: true, force: true });
20+
});
21+
22+
it('initializes with defaults', () => {
23+
dir = tmpDir();
24+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
25+
expect(sm.dataDir).toBe(dir);
26+
expect(sm.cache).toBeDefined();
27+
expect(sm.packs).toBeDefined();
28+
expect(sm.provenance).toBeDefined();
29+
});
30+
31+
describe('cache', () => {
32+
it('put + lookup roundtrip', () => {
33+
dir = tmpDir();
34+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
35+
36+
sm.cache.put('hello world', 'test');
37+
const result = sm.cache.lookup('hello world');
38+
expect(result.hit).toBe(true);
39+
expect(result.tokensSaved).toBeGreaterThan(0);
40+
});
41+
42+
it('miss on unknown content', () => {
43+
dir = tmpDir();
44+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
45+
46+
const result = sm.cache.lookup('never seen before');
47+
expect(result.hit).toBe(false);
48+
expect(result.tokensSaved).toBe(0);
49+
});
50+
51+
it('stats aggregate correctly', () => {
52+
dir = tmpDir();
53+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
54+
55+
sm.cache.put('content A', 'src-a');
56+
sm.cache.put('content B', 'src-b');
57+
sm.cache.lookup('content A');
58+
59+
const stats = sm.cache.getStats();
60+
expect(stats.totalEntries).toBe(2);
61+
expect(stats.totalTokensCached).toBeGreaterThan(0);
62+
});
63+
});
64+
65+
describe('packs', () => {
66+
it('install + get + list', () => {
67+
dir = tmpDir();
68+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
69+
70+
sm.packs.install({
71+
manifest: {
72+
name: 'test/pack',
73+
version: '1.0.0',
74+
description: 'Test pack',
75+
author: 'test',
76+
license: 'MIT',
77+
},
78+
instructions: 'Do the thing.',
79+
});
80+
81+
const pack = sm.packs.get('test/pack');
82+
expect(pack).toBeDefined();
83+
expect(pack!.manifest.version).toBe('1.0.0');
84+
expect(pack!.instructions).toBe('Do the thing.');
85+
86+
const all = sm.packs.list();
87+
expect(all.length).toBe(1);
88+
});
89+
90+
it('search by keyword', () => {
91+
dir = tmpDir();
92+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
93+
94+
sm.packs.install({
95+
manifest: {
96+
name: 'coding/react',
97+
version: '1.0.0',
98+
description: 'React conventions and patterns',
99+
author: 'test',
100+
license: 'MIT',
101+
},
102+
instructions: 'Use functional components.',
103+
});
104+
105+
const results = sm.packs.search('react');
106+
expect(results.length).toBe(1);
107+
expect(results[0]!.manifest.name).toBe('coding/react');
108+
});
109+
110+
it('uninstall removes pack', () => {
111+
dir = tmpDir();
112+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
113+
114+
sm.packs.install({
115+
manifest: {
116+
name: 'tmp/pack',
117+
version: '0.1.0',
118+
description: 'Temporary',
119+
author: 'test',
120+
license: 'MIT',
121+
},
122+
instructions: undefined,
123+
});
124+
125+
expect(sm.packs.uninstall('tmp/pack')).toBe(true);
126+
expect(sm.packs.get('tmp/pack')).toBeUndefined();
127+
});
128+
});
129+
130+
describe('provenance', () => {
131+
it('record + get trace event', () => {
132+
dir = tmpDir();
133+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
134+
135+
sm.provenance.record({
136+
timestamp: new Date().toISOString(),
137+
sessionId: 'sess-1',
138+
traceId: 'trace-1',
139+
tenantId: 'tenant-1',
140+
actor: { host: 'claude-code', agent: 'test', user: 'dev' },
141+
operation: 'query',
142+
inputs: { q: 'test' },
143+
outputs: { result: 'ok' },
144+
tokensIn: 100,
145+
tokensOut: 50,
146+
costUsd: 0.001,
147+
provenance: {
148+
sources: [
149+
{
150+
system: 'test',
151+
externalId: 'ext-1',
152+
fetchedAt: new Date().toISOString(),
153+
},
154+
],
155+
derivation: [],
156+
confidence: 0.85,
157+
},
158+
});
159+
160+
const event = sm.provenance.get('trace-1');
161+
expect(event).toBeDefined();
162+
expect(event!.operation).toBe('query');
163+
expect(event!.provenance.confidence).toBe(0.85);
164+
});
165+
166+
it('query by session', () => {
167+
dir = tmpDir();
168+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
169+
170+
for (let i = 0; i < 3; i++) {
171+
sm.provenance.record({
172+
timestamp: new Date().toISOString(),
173+
sessionId: i < 2 ? 'sess-A' : 'sess-B',
174+
traceId: `t-${i}`,
175+
tenantId: 'tenant-1',
176+
actor: { host: 'test', agent: 'test', user: 'test' },
177+
operation: 'op',
178+
inputs: null,
179+
outputs: null,
180+
tokensIn: 0,
181+
tokensOut: 0,
182+
costUsd: 0,
183+
provenance: { sources: [], derivation: [], confidence: 0 },
184+
});
185+
}
186+
187+
const results = sm.provenance.query({ sessionId: 'sess-A' });
188+
expect(results.length).toBe(2);
189+
});
190+
191+
it('lineage follows parent chain', () => {
192+
dir = tmpDir();
193+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
194+
195+
const base = {
196+
timestamp: new Date().toISOString(),
197+
tenantId: 'T',
198+
sessionId: 'S',
199+
actor: { host: 'h', agent: 'a', user: 'u' },
200+
operation: 'op',
201+
inputs: null,
202+
outputs: null,
203+
tokensIn: 0,
204+
tokensOut: 0,
205+
costUsd: 0,
206+
provenance: { sources: [], derivation: [], confidence: 0 },
207+
};
208+
209+
sm.provenance.record({ ...base, traceId: 'root' });
210+
sm.provenance.record({
211+
...base,
212+
traceId: 'child',
213+
parentTraceId: 'root',
214+
});
215+
sm.provenance.record({
216+
...base,
217+
traceId: 'grandchild',
218+
parentTraceId: 'child',
219+
});
220+
221+
const lineage = sm.provenance.getLineage('grandchild');
222+
expect(lineage.length).toBe(3);
223+
expect(lineage[0]!.traceId).toBe('root');
224+
expect(lineage[2]!.traceId).toBe('grandchild');
225+
});
226+
227+
it('stats aggregate correctly', () => {
228+
dir = tmpDir();
229+
sm = new StackMemory({ dataDir: dir, logLevel: 'silent' });
230+
231+
sm.provenance.record({
232+
timestamp: new Date().toISOString(),
233+
sessionId: 'S',
234+
traceId: 'T',
235+
tenantId: 'tenant-1',
236+
actor: { host: 'h', agent: 'a', user: 'u' },
237+
operation: 'op',
238+
inputs: null,
239+
outputs: null,
240+
tokensIn: 100,
241+
tokensOut: 200,
242+
costUsd: 0.5,
243+
provenance: { sources: [], derivation: [], confidence: 0.9 },
244+
});
245+
246+
const stats = sm.provenance.getStats();
247+
expect(stats.totalEvents).toBe(1);
248+
expect(stats.totalTokensIn).toBe(100);
249+
expect(stats.totalTokensOut).toBe(200);
250+
expect(stats.totalCostUsd).toBe(0.5);
251+
});
252+
});
253+
254+
describe('scoreConfidence', () => {
255+
it('scores strong decisions high', () => {
256+
const result = scoreConfidence(
257+
'we decided to use TypeScript. the plan is to migrate by Friday.'
258+
);
259+
expect(result.confidence).toBeGreaterThanOrEqual(0.4);
260+
expect(result.classification).not.toBe('discard');
261+
});
262+
263+
it('scores single trigger phrase', () => {
264+
const result = scoreConfidence('we decided to use TypeScript');
265+
expect(result.confidence).toBe(0.3);
266+
});
267+
268+
it('scores questions low', () => {
269+
const result = scoreConfidence('should we use TypeScript?');
270+
expect(result.confidence).toBeLessThan(0.3);
271+
expect(result.classification).toBe('discard');
272+
});
273+
});
274+
275+
describe('pure functions', () => {
276+
it('estimateTokens approximates', () => {
277+
expect(estimateTokens('hello')).toBe(2);
278+
expect(estimateTokens('')).toBe(0);
279+
});
280+
281+
it('hashContent is deterministic', () => {
282+
const a = hashContent('test');
283+
const b = hashContent('test');
284+
expect(a).toBe(b);
285+
expect(a.length).toBe(64);
286+
});
287+
});
288+
});

0 commit comments

Comments
 (0)