sandstream-kit 1.0.1 → 1.2.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.
- package/README.md +18 -0
- package/dist/check-web-search.js +37 -6
- package/dist/check-web-search.js.map +1 -1
- package/dist/cli.js +520 -2
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/database.d.ts +2 -2
- package/dist/database.js +9 -14
- package/dist/database.js.map +1 -1
- package/dist/lock.js +5 -3
- package/dist/lock.js.map +1 -1
- package/dist/memory/backup 2.d.ts +6 -0
- package/dist/memory/backup 2.js +80 -0
- package/dist/memory/backup 2.js.map +1 -0
- package/dist/memory/backup.d.ts +6 -0
- package/dist/memory/backup.js +80 -0
- package/dist/memory/backup.js.map +1 -0
- package/dist/memory/codex.d.ts +5 -0
- package/dist/memory/codex.js +150 -0
- package/dist/memory/codex.js.map +1 -0
- package/dist/memory/continue.d.ts +5 -0
- package/dist/memory/continue.js +116 -0
- package/dist/memory/continue.js.map +1 -0
- package/dist/memory/db 2.d.ts +40 -0
- package/dist/memory/db 2.js +233 -0
- package/dist/memory/db 2.js.map +1 -0
- package/dist/memory/db.d.ts +51 -0
- package/dist/memory/db.js +275 -0
- package/dist/memory/db.js.map +1 -0
- package/dist/memory/gemini.d.ts +5 -0
- package/dist/memory/gemini.js +176 -0
- package/dist/memory/gemini.js.map +1 -0
- package/dist/memory/hook 2.d.ts +6 -0
- package/dist/memory/hook 2.js +51 -0
- package/dist/memory/hook 2.js.map +1 -0
- package/dist/memory/hook.d.ts +15 -0
- package/dist/memory/hook.js +84 -0
- package/dist/memory/hook.js.map +1 -0
- package/dist/memory/hook.test 2.d.ts +1 -0
- package/dist/memory/hook.test 2.js +35 -0
- package/dist/memory/hook.test 2.js.map +1 -0
- package/dist/memory/install 2.d.ts +8 -0
- package/dist/memory/install 2.js +72 -0
- package/dist/memory/install 2.js.map +1 -0
- package/dist/memory/install.d.ts +8 -0
- package/dist/memory/install.js +73 -0
- package/dist/memory/install.js.map +1 -0
- package/dist/memory/install.test 2.d.ts +1 -0
- package/dist/memory/install.test 2.js +59 -0
- package/dist/memory/install.test 2.js.map +1 -0
- package/dist/memory/merge.d.ts +18 -0
- package/dist/memory/merge.js +109 -0
- package/dist/memory/merge.js.map +1 -0
- package/dist/memory/pal 2.d.ts +47 -0
- package/dist/memory/pal 2.js +154 -0
- package/dist/memory/pal 2.js.map +1 -0
- package/dist/memory/pal.d.ts +47 -0
- package/dist/memory/pal.js +154 -0
- package/dist/memory/pal.js.map +1 -0
- package/dist/memory/parser.d.ts +34 -0
- package/dist/memory/parser.js +195 -0
- package/dist/memory/parser.js.map +1 -0
- package/dist/memory/project 2.d.ts +1 -0
- package/dist/memory/project 2.js +24 -0
- package/dist/memory/project 2.js.map +1 -0
- package/dist/memory/project.d.ts +1 -0
- package/dist/memory/project.js +24 -0
- package/dist/memory/project.js.map +1 -0
- package/dist/memory/scan 2.d.ts +15 -0
- package/dist/memory/scan 2.js +94 -0
- package/dist/memory/scan 2.js.map +1 -0
- package/dist/memory/scan.d.ts +15 -0
- package/dist/memory/scan.js +94 -0
- package/dist/memory/scan.js.map +1 -0
- package/dist/memory/scan.test 2.d.ts +1 -0
- package/dist/memory/scan.test 2.js +55 -0
- package/dist/memory/scan.test 2.js.map +1 -0
- package/dist/memory/shared 2.d.ts +33 -0
- package/dist/memory/shared 2.js +120 -0
- package/dist/memory/shared 2.js.map +1 -0
- package/dist/memory/shared.d.ts +33 -0
- package/dist/memory/shared.js +120 -0
- package/dist/memory/shared.js.map +1 -0
- package/dist/memory/threads 2.d.ts +37 -0
- package/dist/memory/threads 2.js +50 -0
- package/dist/memory/threads 2.js.map +1 -0
- package/dist/memory/threads.d.ts +37 -0
- package/dist/memory/threads.js +50 -0
- package/dist/memory/threads.js.map +1 -0
- package/dist/memory/threads.test 2.d.ts +1 -0
- package/dist/memory/threads.test 2.js +66 -0
- package/dist/memory/threads.test 2.js.map +1 -0
- package/dist/memory/types 2.d.ts +52 -0
- package/dist/memory/types 2.js +5 -0
- package/dist/memory/types 2.js.map +1 -0
- package/dist/memory/types.d.ts +52 -0
- package/dist/memory/types.js +5 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/status.d.ts +9 -0
- package/dist/status.js +119 -0
- package/dist/status.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kit memory — secret scan over the store.
|
|
3
|
+
*
|
|
4
|
+
* The memory DB is secret-dense (it indexes raw transcripts). gitleaks and most
|
|
5
|
+
* scanners only see text files, not SQLite cell contents — so this scans the text
|
|
6
|
+
* columns directly, reusing kit's SECRET_PATTERNS via findSecrets (DRY). Findings
|
|
7
|
+
* are MASKED (label + short preview), never the raw secret.
|
|
8
|
+
*
|
|
9
|
+
* Findings are DEDUPED by (label, preview) with an occurrence count, split by
|
|
10
|
+
* CONFIDENCE so the genuinely dangerous keys (sk_live, AIzaSy, AKIA, ghp_, …) are
|
|
11
|
+
* not buried under the over-eager `KEY=value` heuristic, and ATTRIBUTED to the
|
|
12
|
+
* project(s) they leaked in (via each row's cwd) so you know which provider account
|
|
13
|
+
* to rotate. Only high-confidence findings make `kit memory scan` exit non-zero.
|
|
14
|
+
*/
|
|
15
|
+
import { basename } from "node:path";
|
|
16
|
+
import { findSecrets } from "../utils/redactSecrets.js";
|
|
17
|
+
const TARGETS = [
|
|
18
|
+
{
|
|
19
|
+
table: "messages",
|
|
20
|
+
idCol: "id",
|
|
21
|
+
columns: ["content"],
|
|
22
|
+
select: "SELECT id, content, cwd AS __project FROM messages",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
table: "tool_uses",
|
|
26
|
+
idCol: "id",
|
|
27
|
+
columns: ["tool_input"],
|
|
28
|
+
select: "SELECT tool_uses.id AS id, tool_uses.tool_input AS tool_input, m.cwd AS __project " +
|
|
29
|
+
"FROM tool_uses LEFT JOIN messages m ON m.uuid = tool_uses.message_uuid",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
table: "pending_actions",
|
|
33
|
+
idCol: "id",
|
|
34
|
+
columns: ["title", "detail", "verify_cmd"],
|
|
35
|
+
select: "SELECT id, title, detail, verify_cmd, scope AS __project FROM pending_actions",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
table: "saved_threads",
|
|
39
|
+
idCol: "name",
|
|
40
|
+
columns: ["summary"],
|
|
41
|
+
select: "SELECT name, summary, project_path AS __project FROM saved_threads",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
// Heuristic labels are pattern-based guesses (KEY=value, tfstate blobs) that
|
|
45
|
+
// frequently match benign env vars / file paths. Everything else is a structured,
|
|
46
|
+
// high-confidence credential pattern.
|
|
47
|
+
const HEURISTIC_LABELS = new Set(["kv-secret", "tfstate-value"]);
|
|
48
|
+
function projectName(raw) {
|
|
49
|
+
if (typeof raw !== "string" || !raw)
|
|
50
|
+
return null;
|
|
51
|
+
return raw.includes("/") ? basename(raw) : raw;
|
|
52
|
+
}
|
|
53
|
+
/** Scan every text cell for stored secrets. Deduped, confidence-tiered, project-attributed. */
|
|
54
|
+
export function scanDbForSecrets(db) {
|
|
55
|
+
const byKey = new Map();
|
|
56
|
+
for (const target of TARGETS) {
|
|
57
|
+
const rows = db.prepare(target.select).all();
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
const proj = projectName(row.__project);
|
|
60
|
+
for (const col of target.columns) {
|
|
61
|
+
const val = row[col];
|
|
62
|
+
if (typeof val !== "string" || !val)
|
|
63
|
+
continue;
|
|
64
|
+
for (const f of findSecrets(val)) {
|
|
65
|
+
const key = `${f.label} ${f.preview}`;
|
|
66
|
+
let entry = byKey.get(key);
|
|
67
|
+
if (!entry) {
|
|
68
|
+
entry = {
|
|
69
|
+
label: f.label,
|
|
70
|
+
preview: f.preview,
|
|
71
|
+
confidence: HEURISTIC_LABELS.has(f.label) ? "heuristic" : "high",
|
|
72
|
+
count: 0,
|
|
73
|
+
sample: `${target.table}#${row[target.idCol]}.${col}`,
|
|
74
|
+
projects: [],
|
|
75
|
+
_projects: new Set(),
|
|
76
|
+
};
|
|
77
|
+
byKey.set(key, entry);
|
|
78
|
+
}
|
|
79
|
+
entry.count++;
|
|
80
|
+
if (proj)
|
|
81
|
+
entry._projects.add(proj);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...byKey.values()]
|
|
87
|
+
.map(({ _projects, ...f }) => ({ ...f, projects: [..._projects].sort() }))
|
|
88
|
+
.sort((a, b) => {
|
|
89
|
+
if (a.confidence !== b.confidence)
|
|
90
|
+
return a.confidence === "high" ? -1 : 1;
|
|
91
|
+
return b.count - a.count;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../../src/memory/scan.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { openMemoryDb, upsertSession, insertMessage } from "./db.js";
|
|
4
|
+
import { scanDbForSecrets } from "./scan.js";
|
|
5
|
+
describe("memory secret-scan", () => {
|
|
6
|
+
it("flags a stored secret (masked, high-confidence) and locates it", () => {
|
|
7
|
+
const db = openMemoryDb(":memory:");
|
|
8
|
+
upsertSession(db, { sessionId: "s1", harness: "claude-code" });
|
|
9
|
+
const fake = "sk_live_" + "A".repeat(24); // synthetic, non-real
|
|
10
|
+
insertMessage(db, { uuid: "u1", sessionId: "s1", type: "user", content: `the key is ${fake}` });
|
|
11
|
+
insertMessage(db, { uuid: "u2", sessionId: "s1", type: "user", content: "totally clean message" });
|
|
12
|
+
const findings = scanDbForSecrets(db);
|
|
13
|
+
assert.equal(findings.length, 1);
|
|
14
|
+
assert.equal(findings[0]?.label, "stripe-key");
|
|
15
|
+
assert.equal(findings[0]?.confidence, "high");
|
|
16
|
+
assert.equal(findings[0]?.count, 1);
|
|
17
|
+
assert.match(findings[0]?.sample ?? "", /^messages#\d+\.content$/);
|
|
18
|
+
assert.ok(!findings[0]?.preview.includes("A".repeat(24)), "preview is masked, not the raw secret");
|
|
19
|
+
db.close();
|
|
20
|
+
});
|
|
21
|
+
it("dedupes the same secret across rows with an occurrence count", () => {
|
|
22
|
+
const db = openMemoryDb(":memory:");
|
|
23
|
+
upsertSession(db, { sessionId: "s1", harness: "claude-code" });
|
|
24
|
+
const fake = "sk_live_" + "B".repeat(24);
|
|
25
|
+
insertMessage(db, { uuid: "u1", sessionId: "s1", type: "user", content: `key ${fake}` });
|
|
26
|
+
insertMessage(db, { uuid: "u2", sessionId: "s1", type: "user", content: `again ${fake}` });
|
|
27
|
+
const findings = scanDbForSecrets(db);
|
|
28
|
+
assert.equal(findings.length, 1, "one unique finding, not two");
|
|
29
|
+
assert.equal(findings[0]?.count, 2);
|
|
30
|
+
db.close();
|
|
31
|
+
});
|
|
32
|
+
it("attributes a finding to the project it leaked in (via cwd)", () => {
|
|
33
|
+
const db = openMemoryDb(":memory:");
|
|
34
|
+
upsertSession(db, { sessionId: "s1", harness: "claude-code" });
|
|
35
|
+
const fake = "sk_live_" + "C".repeat(24);
|
|
36
|
+
insertMessage(db, {
|
|
37
|
+
uuid: "u1",
|
|
38
|
+
sessionId: "s1",
|
|
39
|
+
type: "user",
|
|
40
|
+
content: `key ${fake}`,
|
|
41
|
+
cwd: "/Users/me/dev/app-a",
|
|
42
|
+
});
|
|
43
|
+
const findings = scanDbForSecrets(db);
|
|
44
|
+
assert.deepEqual(findings[0]?.projects, ["app-a"]);
|
|
45
|
+
db.close();
|
|
46
|
+
});
|
|
47
|
+
it("returns nothing for a clean db", () => {
|
|
48
|
+
const db = openMemoryDb(":memory:");
|
|
49
|
+
upsertSession(db, { sessionId: "s1", harness: "claude-code" });
|
|
50
|
+
insertMessage(db, { uuid: "u1", sessionId: "s1", type: "user", content: "no secrets here" });
|
|
51
|
+
assert.deepEqual(scanDbForSecrets(db), []);
|
|
52
|
+
db.close();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=scan.test%202.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.test 2.js","sourceRoot":"","sources":["../../src/memory/scan.test 2.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type SharedKind = "decision" | "convention" | "how-built" | "status" | "security" | "note";
|
|
2
|
+
export interface SharedEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
area: string;
|
|
5
|
+
kind: SharedKind;
|
|
6
|
+
title: string;
|
|
7
|
+
body: string;
|
|
8
|
+
refs: string[];
|
|
9
|
+
author: string;
|
|
10
|
+
ts: string;
|
|
11
|
+
source_ref?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ShareInput {
|
|
14
|
+
area: string;
|
|
15
|
+
kind: SharedKind;
|
|
16
|
+
title: string;
|
|
17
|
+
body: string;
|
|
18
|
+
refs?: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function getSharedPath(root: string): string;
|
|
21
|
+
export declare function readShared(root: string): SharedEntry[];
|
|
22
|
+
/**
|
|
23
|
+
* Promote one entry into the shared store. Fail-closed: refuses (throws) if any
|
|
24
|
+
* text field contains a secret. Only allow-listed fields are persisted — no raw
|
|
25
|
+
* tool output / env dumps can sneak in. Author + source_ref give provenance.
|
|
26
|
+
*/
|
|
27
|
+
export declare function shareEntry(root: string, input: ShareInput, now: string): SharedEntry;
|
|
28
|
+
export declare function listAreas(root: string): {
|
|
29
|
+
area: string;
|
|
30
|
+
count: number;
|
|
31
|
+
}[];
|
|
32
|
+
export declare function queryArea(root: string, area: string): SharedEntry[];
|
|
33
|
+
export declare function searchShared(root: string, query: string): SharedEntry[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kit memory — shared project / responsibility-area memory (the curated tier).
|
|
3
|
+
*
|
|
4
|
+
* This is CONTEXT, not raw memory: durable, curated, intentional knowledge that
|
|
5
|
+
* is safe to share with the team and travels with the repo. Treated LIKE CODE:
|
|
6
|
+
* - committed TEXT (.kit/shared/memory.jsonl) → diffable, PR-reviewable, gitleaks-scannable;
|
|
7
|
+
* - deny-by-default — nothing is auto-shared, you promote entries explicitly;
|
|
8
|
+
* - allow-listed schema — only safe fields (no raw dumps);
|
|
9
|
+
* - fail-closed secret-scan on write (reuses kit's SECRET_PATTERNS);
|
|
10
|
+
* - provenance + receipts (author + source_ref) so colleagues can trust it.
|
|
11
|
+
*
|
|
12
|
+
* Organized by `area` (e.g. "stripe", "whatsapp", "plugins") so a growing system
|
|
13
|
+
* stays navigable: "how did we build X, what's next, is it secure?" = that area's
|
|
14
|
+
* entries (with receipts). Entries are few (curated) → plain JSONL + JS query; no
|
|
15
|
+
* second database. Querying never calls a model.
|
|
16
|
+
*/
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { existsSync, readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { join, dirname } from "node:path";
|
|
21
|
+
import { findSecrets } from "../utils/redactSecrets.js";
|
|
22
|
+
export function getSharedPath(root) {
|
|
23
|
+
return join(root, ".kit", "shared", "memory.jsonl");
|
|
24
|
+
}
|
|
25
|
+
function gitAuthor(root) {
|
|
26
|
+
const read = (key) => {
|
|
27
|
+
try {
|
|
28
|
+
return execFileSync("git", ["config", key], {
|
|
29
|
+
cwd: root,
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
32
|
+
}).trim();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const name = read("user.name");
|
|
39
|
+
const email = read("user.email");
|
|
40
|
+
if (name && email)
|
|
41
|
+
return `${name} <${email}>`;
|
|
42
|
+
return name || email || "unknown";
|
|
43
|
+
}
|
|
44
|
+
function gitHead(root) {
|
|
45
|
+
try {
|
|
46
|
+
const sha = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
|
|
47
|
+
cwd: root,
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
50
|
+
}).trim();
|
|
51
|
+
return sha || undefined;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function readShared(root) {
|
|
58
|
+
const path = getSharedPath(root);
|
|
59
|
+
if (!existsSync(path))
|
|
60
|
+
return [];
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
63
|
+
const t = line.trim();
|
|
64
|
+
if (!t)
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
out.push(JSON.parse(t));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// skip malformed lines, keep the rest readable
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Promote one entry into the shared store. Fail-closed: refuses (throws) if any
|
|
77
|
+
* text field contains a secret. Only allow-listed fields are persisted — no raw
|
|
78
|
+
* tool output / env dumps can sneak in. Author + source_ref give provenance.
|
|
79
|
+
*/
|
|
80
|
+
export function shareEntry(root, input, now) {
|
|
81
|
+
const refs = input.refs ?? [];
|
|
82
|
+
const scanned = [input.title, input.body, ...refs].join("\n");
|
|
83
|
+
const found = findSecrets(scanned);
|
|
84
|
+
if (found.length) {
|
|
85
|
+
throw new Error(`refused: entry contains ${found.length} secret(s) (${found
|
|
86
|
+
.map((f) => f.label)
|
|
87
|
+
.join(", ")}) — shared memory must be secret-clean`);
|
|
88
|
+
}
|
|
89
|
+
const entry = {
|
|
90
|
+
id: randomBytes(3).toString("hex"),
|
|
91
|
+
area: input.area,
|
|
92
|
+
kind: input.kind,
|
|
93
|
+
title: input.title,
|
|
94
|
+
body: input.body,
|
|
95
|
+
refs,
|
|
96
|
+
author: gitAuthor(root),
|
|
97
|
+
ts: now,
|
|
98
|
+
source_ref: gitHead(root),
|
|
99
|
+
};
|
|
100
|
+
const path = getSharedPath(root);
|
|
101
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
102
|
+
appendFileSync(path, JSON.stringify(entry) + "\n");
|
|
103
|
+
return entry;
|
|
104
|
+
}
|
|
105
|
+
export function listAreas(root) {
|
|
106
|
+
const counts = new Map();
|
|
107
|
+
for (const e of readShared(root))
|
|
108
|
+
counts.set(e.area, (counts.get(e.area) ?? 0) + 1);
|
|
109
|
+
return [...counts.entries()]
|
|
110
|
+
.map(([area, count]) => ({ area, count }))
|
|
111
|
+
.sort((a, b) => a.area.localeCompare(b.area));
|
|
112
|
+
}
|
|
113
|
+
export function queryArea(root, area) {
|
|
114
|
+
return readShared(root).filter((e) => e.area === area);
|
|
115
|
+
}
|
|
116
|
+
export function searchShared(root, query) {
|
|
117
|
+
const q = query.toLowerCase();
|
|
118
|
+
return readShared(root).filter((e) => e.title.toLowerCase().includes(q) || e.body.toLowerCase().includes(q));
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=shared%202.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared 2.js","sourceRoot":"","sources":["../../src/memory/shared 2.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type SharedKind = "decision" | "convention" | "how-built" | "status" | "security" | "note";
|
|
2
|
+
export interface SharedEntry {
|
|
3
|
+
id: string;
|
|
4
|
+
area: string;
|
|
5
|
+
kind: SharedKind;
|
|
6
|
+
title: string;
|
|
7
|
+
body: string;
|
|
8
|
+
refs: string[];
|
|
9
|
+
author: string;
|
|
10
|
+
ts: string;
|
|
11
|
+
source_ref?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ShareInput {
|
|
14
|
+
area: string;
|
|
15
|
+
kind: SharedKind;
|
|
16
|
+
title: string;
|
|
17
|
+
body: string;
|
|
18
|
+
refs?: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function getSharedPath(root: string): string;
|
|
21
|
+
export declare function readShared(root: string): SharedEntry[];
|
|
22
|
+
/**
|
|
23
|
+
* Promote one entry into the shared store. Fail-closed: refuses (throws) if any
|
|
24
|
+
* text field contains a secret. Only allow-listed fields are persisted — no raw
|
|
25
|
+
* tool output / env dumps can sneak in. Author + source_ref give provenance.
|
|
26
|
+
*/
|
|
27
|
+
export declare function shareEntry(root: string, input: ShareInput, now: string): SharedEntry;
|
|
28
|
+
export declare function listAreas(root: string): {
|
|
29
|
+
area: string;
|
|
30
|
+
count: number;
|
|
31
|
+
}[];
|
|
32
|
+
export declare function queryArea(root: string, area: string): SharedEntry[];
|
|
33
|
+
export declare function searchShared(root: string, query: string): SharedEntry[];
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kit memory — shared project / responsibility-area memory (the curated tier).
|
|
3
|
+
*
|
|
4
|
+
* This is CONTEXT, not raw memory: durable, curated, intentional knowledge that
|
|
5
|
+
* is safe to share with the team and travels with the repo. Treated LIKE CODE:
|
|
6
|
+
* - committed TEXT (.kit/shared/memory.jsonl) → diffable, PR-reviewable, gitleaks-scannable;
|
|
7
|
+
* - deny-by-default — nothing is auto-shared, you promote entries explicitly;
|
|
8
|
+
* - allow-listed schema — only safe fields (no raw dumps);
|
|
9
|
+
* - fail-closed secret-scan on write (reuses kit's SECRET_PATTERNS);
|
|
10
|
+
* - provenance + receipts (author + source_ref) so colleagues can trust it.
|
|
11
|
+
*
|
|
12
|
+
* Organized by `area` (e.g. "stripe", "whatsapp", "plugins") so a growing system
|
|
13
|
+
* stays navigable: "how did we build X, what's next, is it secure?" = that area's
|
|
14
|
+
* entries (with receipts). Entries are few (curated) → plain JSONL + JS query; no
|
|
15
|
+
* second database. Querying never calls a model.
|
|
16
|
+
*/
|
|
17
|
+
import { randomBytes } from "node:crypto";
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { existsSync, readFileSync, appendFileSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { join, dirname } from "node:path";
|
|
21
|
+
import { findSecrets } from "../utils/redactSecrets.js";
|
|
22
|
+
export function getSharedPath(root) {
|
|
23
|
+
return join(root, ".kit", "shared", "memory.jsonl");
|
|
24
|
+
}
|
|
25
|
+
function gitAuthor(root) {
|
|
26
|
+
const read = (key) => {
|
|
27
|
+
try {
|
|
28
|
+
return execFileSync("git", ["config", key], {
|
|
29
|
+
cwd: root,
|
|
30
|
+
encoding: "utf8",
|
|
31
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
32
|
+
}).trim();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const name = read("user.name");
|
|
39
|
+
const email = read("user.email");
|
|
40
|
+
if (name && email)
|
|
41
|
+
return `${name} <${email}>`;
|
|
42
|
+
return name || email || "unknown";
|
|
43
|
+
}
|
|
44
|
+
function gitHead(root) {
|
|
45
|
+
try {
|
|
46
|
+
const sha = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
|
|
47
|
+
cwd: root,
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
50
|
+
}).trim();
|
|
51
|
+
return sha || undefined;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export function readShared(root) {
|
|
58
|
+
const path = getSharedPath(root);
|
|
59
|
+
if (!existsSync(path))
|
|
60
|
+
return [];
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
63
|
+
const t = line.trim();
|
|
64
|
+
if (!t)
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
out.push(JSON.parse(t));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// skip malformed lines, keep the rest readable
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Promote one entry into the shared store. Fail-closed: refuses (throws) if any
|
|
77
|
+
* text field contains a secret. Only allow-listed fields are persisted — no raw
|
|
78
|
+
* tool output / env dumps can sneak in. Author + source_ref give provenance.
|
|
79
|
+
*/
|
|
80
|
+
export function shareEntry(root, input, now) {
|
|
81
|
+
const refs = input.refs ?? [];
|
|
82
|
+
const scanned = [input.title, input.body, ...refs].join("\n");
|
|
83
|
+
const found = findSecrets(scanned);
|
|
84
|
+
if (found.length) {
|
|
85
|
+
throw new Error(`refused: entry contains ${found.length} secret(s) (${found
|
|
86
|
+
.map((f) => f.label)
|
|
87
|
+
.join(", ")}) — shared memory must be secret-clean`);
|
|
88
|
+
}
|
|
89
|
+
const entry = {
|
|
90
|
+
id: randomBytes(3).toString("hex"),
|
|
91
|
+
area: input.area,
|
|
92
|
+
kind: input.kind,
|
|
93
|
+
title: input.title,
|
|
94
|
+
body: input.body,
|
|
95
|
+
refs,
|
|
96
|
+
author: gitAuthor(root),
|
|
97
|
+
ts: now,
|
|
98
|
+
source_ref: gitHead(root),
|
|
99
|
+
};
|
|
100
|
+
const path = getSharedPath(root);
|
|
101
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
102
|
+
appendFileSync(path, JSON.stringify(entry) + "\n");
|
|
103
|
+
return entry;
|
|
104
|
+
}
|
|
105
|
+
export function listAreas(root) {
|
|
106
|
+
const counts = new Map();
|
|
107
|
+
for (const e of readShared(root))
|
|
108
|
+
counts.set(e.area, (counts.get(e.area) ?? 0) + 1);
|
|
109
|
+
return [...counts.entries()]
|
|
110
|
+
.map(([area, count]) => ({ area, count }))
|
|
111
|
+
.sort((a, b) => a.area.localeCompare(b.area));
|
|
112
|
+
}
|
|
113
|
+
export function queryArea(root, area) {
|
|
114
|
+
return readShared(root).filter((e) => e.area === area);
|
|
115
|
+
}
|
|
116
|
+
export function searchShared(root, query) {
|
|
117
|
+
const q = query.toLowerCase();
|
|
118
|
+
return readShared(root).filter((e) => e.title.toLowerCase().includes(q) || e.body.toLowerCase().includes(q));
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/memory/shared.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kit memory — named copilots (saved threads).
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's resume list labels sessions by the first message you happened to
|
|
5
|
+
* type, so the thread you want is unfindable. Here you bookmark the threads worth
|
|
6
|
+
* keeping under a real name ("a fleet of named copilots"); a small curated list
|
|
7
|
+
* replaces the scrap heap. Scoped per project by default (the personal store holds
|
|
8
|
+
* all projects; this just filters). Pure read/write — no model calls.
|
|
9
|
+
*/
|
|
10
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
11
|
+
export interface SavedThread {
|
|
12
|
+
name: string;
|
|
13
|
+
session_id: string;
|
|
14
|
+
summary: string | null;
|
|
15
|
+
project_path: string | null;
|
|
16
|
+
saved_at: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface SaveThreadInput {
|
|
19
|
+
name: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
summary?: string;
|
|
22
|
+
projectPath?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function saveThread(db: DatabaseSync, input: SaveThreadInput): void;
|
|
25
|
+
export declare function listThreads(db: DatabaseSync, opts?: {
|
|
26
|
+
projectPath?: string;
|
|
27
|
+
}): SavedThread[];
|
|
28
|
+
export declare function getThread(db: DatabaseSync, name: string): SavedThread | undefined;
|
|
29
|
+
export declare function removeThread(db: DatabaseSync, name: string): boolean;
|
|
30
|
+
/** Most recent session that touched this project (by message timestamp). */
|
|
31
|
+
export declare function latestSessionId(db: DatabaseSync, opts?: {
|
|
32
|
+
projectPath?: string;
|
|
33
|
+
}): string | undefined;
|
|
34
|
+
/** Resolve a thread by name, or by 1-based index into the (scoped) list. */
|
|
35
|
+
export declare function resolveThread(db: DatabaseSync, ref: string, opts?: {
|
|
36
|
+
projectPath?: string;
|
|
37
|
+
}): SavedThread | undefined;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function saveThread(db, input) {
|
|
2
|
+
db.prepare(`INSERT INTO saved_threads (name, session_id, summary, project_path, saved_at)
|
|
3
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
4
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
5
|
+
session_id = excluded.session_id,
|
|
6
|
+
summary = COALESCE(excluded.summary, saved_threads.summary),
|
|
7
|
+
project_path = COALESCE(excluded.project_path, saved_threads.project_path),
|
|
8
|
+
saved_at = datetime('now')`).run(input.name, input.sessionId, input.summary ?? null, input.projectPath ?? null);
|
|
9
|
+
}
|
|
10
|
+
export function listThreads(db, opts = {}) {
|
|
11
|
+
if (opts.projectPath) {
|
|
12
|
+
return db
|
|
13
|
+
.prepare("SELECT * FROM saved_threads WHERE project_path = ? ORDER BY saved_at DESC")
|
|
14
|
+
.all(opts.projectPath);
|
|
15
|
+
}
|
|
16
|
+
return db
|
|
17
|
+
.prepare("SELECT * FROM saved_threads ORDER BY saved_at DESC")
|
|
18
|
+
.all();
|
|
19
|
+
}
|
|
20
|
+
export function getThread(db, name) {
|
|
21
|
+
return db.prepare("SELECT * FROM saved_threads WHERE name = ?").get(name);
|
|
22
|
+
}
|
|
23
|
+
export function removeThread(db, name) {
|
|
24
|
+
return Number(db.prepare("DELETE FROM saved_threads WHERE name = ?").run(name).changes) > 0;
|
|
25
|
+
}
|
|
26
|
+
/** Most recent session that touched this project (by message timestamp). */
|
|
27
|
+
export function latestSessionId(db, opts = {}) {
|
|
28
|
+
let row;
|
|
29
|
+
if (opts.projectPath) {
|
|
30
|
+
row = db
|
|
31
|
+
.prepare(`SELECT session_id FROM messages
|
|
32
|
+
WHERE cwd = ? OR cwd LIKE ?
|
|
33
|
+
ORDER BY timestamp DESC LIMIT 1`)
|
|
34
|
+
.get(opts.projectPath, `${opts.projectPath}/%`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
row = db
|
|
38
|
+
.prepare("SELECT session_id FROM messages ORDER BY timestamp DESC LIMIT 1")
|
|
39
|
+
.get();
|
|
40
|
+
}
|
|
41
|
+
return row?.session_id;
|
|
42
|
+
}
|
|
43
|
+
/** Resolve a thread by name, or by 1-based index into the (scoped) list. */
|
|
44
|
+
export function resolveThread(db, ref, opts = {}) {
|
|
45
|
+
if (/^\d+$/.test(ref)) {
|
|
46
|
+
return listThreads(db, opts)[Number(ref) - 1];
|
|
47
|
+
}
|
|
48
|
+
return getThread(db, ref);
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=threads%202.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threads 2.js","sourceRoot":"","sources":["../../src/memory/threads 2.ts"],"names":[],"mappings":"AA0BA,MAAM,UAAU,UAAU,CAAC,EAAgB,EAAE,KAAsB;IACjE,EAAE,CAAC,OAAO,CACR;;;;;;kCAM8B,CAC/B,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,IAAI,IAAI,EAAE,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC;AACvF,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,EAAgB,EAChB,OAAiC,EAAE;IAEnC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,OAAO,EAAE;aACN,OAAO,CAAC,2EAA2E,CAAC;aACpF,GAAG,CAAC,IAAI,CAAC,WAAW,CAA6B,CAAC;IACvD,CAAC;IACD,OAAO,EAAE;SACN,OAAO,CAAC,oDAAoD,CAAC;SAC7D,GAAG,EAA8B,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EAAgB,EAAE,IAAY;IACtD,OAAO,EAAE,CAAC,OAAO,CAAC,4CAA4C,CAAC,CAAC,GAAG,CAAC,IAAI,CAE3D,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,EAAgB,EAAE,IAAY;IACzD,OAAO,MAAM,CAAC,EAAE,CAAC,OAAO,CAAC,0CAA0C,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAC9F,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAC7B,EAAgB,EAChB,OAAiC,EAAE;IAEnC,IAAI,GAAuC,CAAC;IAC5C,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;QACrB,GAAG,GAAG,EAAE;aACL,OAAO,CACN;;yCAEiC,CAClC;aACA,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,WAAW,IAAI,CAAuC,CAAC;IAC1F,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,EAAE;aACL,OAAO,CAAC,iEAAiE,CAAC;aAC1E,GAAG,EAAwC,CAAC;IACjD,CAAC;IACD,OAAO,GAAG,EAAE,UAAU,CAAC;AACzB,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,aAAa,CAC3B,EAAgB,EAChB,GAAW,EACX,OAAiC,EAAE;IAEnC,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO,WAAW,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,SAAS,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;AAC5B,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kit memory — named copilots (saved threads).
|
|
3
|
+
*
|
|
4
|
+
* Claude Code's resume list labels sessions by the first message you happened to
|
|
5
|
+
* type, so the thread you want is unfindable. Here you bookmark the threads worth
|
|
6
|
+
* keeping under a real name ("a fleet of named copilots"); a small curated list
|
|
7
|
+
* replaces the scrap heap. Scoped per project by default (the personal store holds
|
|
8
|
+
* all projects; this just filters). Pure read/write — no model calls.
|
|
9
|
+
*/
|
|
10
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
11
|
+
export interface SavedThread {
|
|
12
|
+
name: string;
|
|
13
|
+
session_id: string;
|
|
14
|
+
summary: string | null;
|
|
15
|
+
project_path: string | null;
|
|
16
|
+
saved_at: string | null;
|
|
17
|
+
}
|
|
18
|
+
export interface SaveThreadInput {
|
|
19
|
+
name: string;
|
|
20
|
+
sessionId: string;
|
|
21
|
+
summary?: string;
|
|
22
|
+
projectPath?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function saveThread(db: DatabaseSync, input: SaveThreadInput): void;
|
|
25
|
+
export declare function listThreads(db: DatabaseSync, opts?: {
|
|
26
|
+
projectPath?: string;
|
|
27
|
+
}): SavedThread[];
|
|
28
|
+
export declare function getThread(db: DatabaseSync, name: string): SavedThread | undefined;
|
|
29
|
+
export declare function removeThread(db: DatabaseSync, name: string): boolean;
|
|
30
|
+
/** Most recent session that touched this project (by message timestamp). */
|
|
31
|
+
export declare function latestSessionId(db: DatabaseSync, opts?: {
|
|
32
|
+
projectPath?: string;
|
|
33
|
+
}): string | undefined;
|
|
34
|
+
/** Resolve a thread by name, or by 1-based index into the (scoped) list. */
|
|
35
|
+
export declare function resolveThread(db: DatabaseSync, ref: string, opts?: {
|
|
36
|
+
projectPath?: string;
|
|
37
|
+
}): SavedThread | undefined;
|