sensemaking 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.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -0
  3. package/bin/cli.js +5 -0
  4. package/dist/cjs/cli.d.cts +1 -0
  5. package/dist/cjs/cli.d.ts +1 -0
  6. package/dist/cjs/cli.js +422 -0
  7. package/dist/cjs/cli.js.map +1 -0
  8. package/dist/cjs/config.d.cts +18 -0
  9. package/dist/cjs/config.d.ts +18 -0
  10. package/dist/cjs/config.js +147 -0
  11. package/dist/cjs/config.js.map +1 -0
  12. package/dist/cjs/db.d.cts +20 -0
  13. package/dist/cjs/db.d.ts +20 -0
  14. package/dist/cjs/db.js +326 -0
  15. package/dist/cjs/db.js.map +1 -0
  16. package/dist/cjs/errors.d.cts +5 -0
  17. package/dist/cjs/errors.d.ts +5 -0
  18. package/dist/cjs/errors.js +134 -0
  19. package/dist/cjs/errors.js.map +1 -0
  20. package/dist/cjs/index.d.cts +12 -0
  21. package/dist/cjs/index.d.ts +12 -0
  22. package/dist/cjs/index.js +76 -0
  23. package/dist/cjs/index.js.map +1 -0
  24. package/dist/cjs/output.d.cts +2 -0
  25. package/dist/cjs/output.d.ts +2 -0
  26. package/dist/cjs/output.js +90 -0
  27. package/dist/cjs/output.js.map +1 -0
  28. package/dist/cjs/package.json +1 -0
  29. package/dist/cjs/scan.d.cts +18 -0
  30. package/dist/cjs/scan.d.ts +18 -0
  31. package/dist/cjs/scan.js +113 -0
  32. package/dist/cjs/scan.js.map +1 -0
  33. package/dist/cjs/watch.d.cts +19 -0
  34. package/dist/cjs/watch.d.ts +19 -0
  35. package/dist/cjs/watch.js +237 -0
  36. package/dist/cjs/watch.js.map +1 -0
  37. package/dist/esm/cli.d.ts +1 -0
  38. package/dist/esm/cli.js +171 -0
  39. package/dist/esm/cli.js.map +1 -0
  40. package/dist/esm/config.d.ts +18 -0
  41. package/dist/esm/config.js +78 -0
  42. package/dist/esm/config.js.map +1 -0
  43. package/dist/esm/db.d.ts +20 -0
  44. package/dist/esm/db.js +176 -0
  45. package/dist/esm/db.js.map +1 -0
  46. package/dist/esm/errors.d.ts +5 -0
  47. package/dist/esm/errors.js +10 -0
  48. package/dist/esm/errors.js.map +1 -0
  49. package/dist/esm/index.d.ts +12 -0
  50. package/dist/esm/index.js +9 -0
  51. package/dist/esm/index.js.map +1 -0
  52. package/dist/esm/output.d.ts +2 -0
  53. package/dist/esm/output.js +25 -0
  54. package/dist/esm/output.js.map +1 -0
  55. package/dist/esm/package.json +1 -0
  56. package/dist/esm/scan.d.ts +18 -0
  57. package/dist/esm/scan.js +69 -0
  58. package/dist/esm/scan.js.map +1 -0
  59. package/dist/esm/watch.d.ts +19 -0
  60. package/dist/esm/watch.js +93 -0
  61. package/dist/esm/watch.js.map +1 -0
  62. package/package.json +63 -0
  63. package/schema.json +38 -0
  64. package/skills/sense/SKILL.md +95 -0
package/dist/esm/db.js ADDED
@@ -0,0 +1,176 @@
1
+ import { mkdirSync, rmSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { STATE_DIR } from './config.js';
5
+ import { listFiles, parseFile } from './scan.js';
6
+ // Rows -> SQLite only: schema/ALTER, the reconcile diff + transaction,
7
+ // pragmas, the meta table, has() registration. Filesystem reading and
8
+ // frontmatter parsing live in scan.ts; this module never touches
9
+ // node:fs/gray-matter directly and never prints (throws are for callers).
10
+ export const DB_FILENAME = 'cache.db';
11
+ export const SCHEMA_VERSION = '1';
12
+ // Quote a SQL identifier, escaping embedded double quotes so an unusual
13
+ // frontmatter key (however unlikely) can't break out of the identifier.
14
+ function quoteIdent(name) {
15
+ return `"${name.split('"').join('""')}"`;
16
+ }
17
+ // The one custom SQL function in the whole tool: has(field, value).
18
+ // - JSON-array field (stored as JSON text, e.g. `["a","b"]`) -> membership
19
+ // - string field -> substring
20
+ // - NULL -> false
21
+ function registerFunctions(db) {
22
+ db.function('has', {
23
+ deterministic: true,
24
+ varargs: false
25
+ }, (field, value)=>{
26
+ if (field === null || field === undefined) return 0;
27
+ const needle = String(value);
28
+ if (typeof field === 'string') {
29
+ // Try JSON array first (arrays are stored as JSON text).
30
+ if (field.startsWith('[')) {
31
+ try {
32
+ const parsed = JSON.parse(field);
33
+ if (Array.isArray(parsed)) {
34
+ return parsed.some((item)=>String(item) === needle) ? 1 : 0;
35
+ }
36
+ } catch {
37
+ // fall through to substring match
38
+ }
39
+ }
40
+ return field.includes(needle) ? 1 : 0;
41
+ }
42
+ // Numbers, etc: coerce to string and substring-match.
43
+ return String(field).includes(needle) ? 1 : 0;
44
+ });
45
+ }
46
+ function getColumns(db) {
47
+ const rows = db.prepare('PRAGMA table_info(docs)').all();
48
+ return new Set(rows.map((r)=>r.name));
49
+ }
50
+ function ensureSchema(db) {
51
+ db.exec(`CREATE TABLE IF NOT EXISTS docs ("path" TEXT PRIMARY KEY, "_mtime" REAL, "_size" INTEGER)`);
52
+ db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');
53
+ if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);
54
+ }
55
+ export function getMeta(db, key) {
56
+ const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key);
57
+ return row ? row.value : null;
58
+ }
59
+ export function setMeta(db, key, value) {
60
+ if (value === null) {
61
+ db.prepare('DELETE FROM meta WHERE key = ?').run(key);
62
+ return;
63
+ }
64
+ db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);
65
+ }
66
+ export function docCount(db) {
67
+ const row = db.prepare('SELECT COUNT(*) AS n FROM docs').get();
68
+ return row.n;
69
+ }
70
+ // Reconcile the `docs` table against the filesystem: glob current files via
71
+ // scan.ts, reparse stale (mtime/size changed) + new files in one
72
+ // transaction, DELETE vanished paths, ALTER TABLE ADD COLUMN for newly
73
+ // discovered frontmatter keys. Returns the number of files (re)parsed and
74
+ // any warnings scan.ts collected (e.g. reserved-key collisions).
75
+ export function reconcile(db, cfg, baseDir) {
76
+ const files = listFiles(cfg, baseDir);
77
+ const currentSet = new Set(files.map((f)=>f.relPath));
78
+ const existingRows = db.prepare(`SELECT "path", "_mtime", "_size" FROM docs`).all();
79
+ const existing = new Map(existingRows.map((r)=>[
80
+ r.path,
81
+ r
82
+ ]));
83
+ const vanished = existingRows.filter((r)=>!currentSet.has(r.path)).map((r)=>r.path);
84
+ const toReparse = files.filter((f)=>{
85
+ const row = existing.get(f.relPath);
86
+ return !row || row._mtime !== f.mtimeMs || row._size !== f.size;
87
+ });
88
+ if (vanished.length === 0 && toReparse.length === 0) return {
89
+ parsed: 0,
90
+ warnings: []
91
+ };
92
+ const seenColumns = getColumns(db);
93
+ const newColumns = [];
94
+ const parsedDocs = [];
95
+ const warnings = [];
96
+ for (const file of toReparse){
97
+ const { doc, warnings: fileWarnings } = parseFile(file);
98
+ warnings.push(...fileWarnings);
99
+ for (const key of Object.keys(doc.data)){
100
+ if (!seenColumns.has(key)) {
101
+ seenColumns.add(key);
102
+ newColumns.push(key);
103
+ }
104
+ }
105
+ parsedDocs.push(doc);
106
+ }
107
+ // seenColumns already contains the reserved columns (they're real columns
108
+ // of `docs`), so spreading it alone avoids duplicate names in the INSERT.
109
+ const allColumns = [
110
+ ...seenColumns
111
+ ];
112
+ const insertSql = `INSERT OR REPLACE INTO docs (${allColumns.map(quoteIdent).join(', ')}) VALUES (${allColumns.map(()=>'?').join(', ')})`;
113
+ db.exec('BEGIN');
114
+ try {
115
+ for (const col of newColumns)db.exec(`ALTER TABLE docs ADD COLUMN ${quoteIdent(col)}`);
116
+ if (vanished.length > 0) {
117
+ const del = db.prepare(`DELETE FROM docs WHERE "path" = ?`);
118
+ for (const path of vanished)del.run(path);
119
+ }
120
+ if (parsedDocs.length > 0) {
121
+ const insert = db.prepare(insertSql);
122
+ for (const doc of parsedDocs){
123
+ const values = allColumns.map((col)=>{
124
+ var _doc_data_col;
125
+ if (col === 'path') return doc.relPath;
126
+ if (col === '_mtime') return doc.mtimeMs;
127
+ if (col === '_size') return doc.size;
128
+ return (_doc_data_col = doc.data[col]) !== null && _doc_data_col !== void 0 ? _doc_data_col : null;
129
+ });
130
+ insert.run(...values);
131
+ }
132
+ }
133
+ db.exec('COMMIT');
134
+ } catch (err) {
135
+ db.exec('ROLLBACK');
136
+ throw err;
137
+ }
138
+ return {
139
+ parsed: parsedDocs.length,
140
+ warnings
141
+ };
142
+ }
143
+ // open(resolved config): open (or create) the on-disk SQLite cache at
144
+ // `<baseDir>/.sense/cache.db`, reconcile against the filesystem, and return
145
+ // a live handle. The DB is a warm start, never a source of truth -- the
146
+ // `.md` files remain truth. Takes only an already-resolved config; discovery
147
+ // and version-gating are config.ts's job, not this one's.
148
+ export function open(cfg) {
149
+ const stateDir = join(cfg.baseDir, STATE_DIR);
150
+ mkdirSync(stateDir, {
151
+ recursive: true
152
+ });
153
+ const dbPath = join(stateDir, DB_FILENAME);
154
+ const db = new DatabaseSync(dbPath);
155
+ db.exec('PRAGMA journal_mode = WAL');
156
+ db.exec('PRAGMA busy_timeout = 5000');
157
+ registerFunctions(db);
158
+ ensureSchema(db);
159
+ const { parsed, warnings } = reconcile(db, cfg, cfg.baseDir);
160
+ return {
161
+ db,
162
+ cfg,
163
+ dbPath,
164
+ parsed,
165
+ warnings
166
+ };
167
+ }
168
+ // Delete the `.sense/` state dir entirely and reconcile fresh -- the manual
169
+ // reset for lingering columns or a doubted cache.
170
+ export function rebuild(cfg) {
171
+ rmSync(join(cfg.baseDir, STATE_DIR), {
172
+ recursive: true,
173
+ force: true
174
+ });
175
+ return open(cfg);
176
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/db.ts"],"sourcesContent":["import { mkdirSync, rmSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { Config, ResolvedConfig } from './config.ts';\nimport { STATE_DIR } from './config.ts';\nimport type { ParsedDoc } from './scan.ts';\nimport { listFiles, parseFile } from './scan.ts';\n\n// Rows -> SQLite only: schema/ALTER, the reconcile diff + transaction,\n// pragmas, the meta table, has() registration. Filesystem reading and\n// frontmatter parsing live in scan.ts; this module never touches\n// node:fs/gray-matter directly and never prints (throws are for callers).\n\nexport const DB_FILENAME = 'cache.db';\nexport const SCHEMA_VERSION = '1';\n\nexport interface OpenResult {\n db: DatabaseSync;\n cfg: ResolvedConfig;\n dbPath: string;\n // Number of files (re)parsed by this open's reconcile -- 0 on a fully\n // warm cache. Tests use this as reconcile instrumentation.\n parsed: number;\n warnings: string[];\n}\n\n// Quote a SQL identifier, escaping embedded double quotes so an unusual\n// frontmatter key (however unlikely) can't break out of the identifier.\nfunction quoteIdent(name: string): string {\n return `\"${name.split('\"').join('\"\"')}\"`;\n}\n\n// The one custom SQL function in the whole tool: has(field, value).\n// - JSON-array field (stored as JSON text, e.g. `[\"a\",\"b\"]`) -> membership\n// - string field -> substring\n// - NULL -> false\nfunction registerFunctions(db: DatabaseSync): void {\n db.function('has', { deterministic: true, varargs: false }, (field: unknown, value: unknown): number => {\n if (field === null || field === undefined) return 0;\n\n const needle = String(value);\n\n if (typeof field === 'string') {\n // Try JSON array first (arrays are stored as JSON text).\n if (field.startsWith('[')) {\n try {\n const parsed = JSON.parse(field);\n if (Array.isArray(parsed)) {\n return parsed.some((item) => String(item) === needle) ? 1 : 0;\n }\n } catch {\n // fall through to substring match\n }\n }\n return field.includes(needle) ? 1 : 0;\n }\n\n // Numbers, etc: coerce to string and substring-match.\n return String(field).includes(needle) ? 1 : 0;\n });\n}\n\nfunction getColumns(db: DatabaseSync): Set<string> {\n const rows = db.prepare('PRAGMA table_info(docs)').all() as Array<{ name: string }>;\n return new Set(rows.map((r) => r.name));\n}\n\nfunction ensureSchema(db: DatabaseSync): void {\n db.exec(`CREATE TABLE IF NOT EXISTS docs (\"path\" TEXT PRIMARY KEY, \"_mtime\" REAL, \"_size\" INTEGER)`);\n db.exec('CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT)');\n if (getMeta(db, 'schema_version') === null) setMeta(db, 'schema_version', SCHEMA_VERSION);\n}\n\nexport function getMeta(db: DatabaseSync, key: string): string | null {\n const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key) as { value: string } | undefined;\n return row ? row.value : null;\n}\n\nexport function setMeta(db: DatabaseSync, key: string, value: string | null): void {\n if (value === null) {\n db.prepare('DELETE FROM meta WHERE key = ?').run(key);\n return;\n }\n db.prepare('INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value').run(key, value);\n}\n\nexport function docCount(db: DatabaseSync): number {\n const row = db.prepare('SELECT COUNT(*) AS n FROM docs').get() as { n: number };\n return row.n;\n}\n\n// Reconcile the `docs` table against the filesystem: glob current files via\n// scan.ts, reparse stale (mtime/size changed) + new files in one\n// transaction, DELETE vanished paths, ALTER TABLE ADD COLUMN for newly\n// discovered frontmatter keys. Returns the number of files (re)parsed and\n// any warnings scan.ts collected (e.g. reserved-key collisions).\nexport function reconcile(db: DatabaseSync, cfg: Config, baseDir: string): { parsed: number; warnings: string[] } {\n const files = listFiles(cfg, baseDir);\n const currentSet = new Set(files.map((f) => f.relPath));\n\n const existingRows = db.prepare(`SELECT \"path\", \"_mtime\", \"_size\" FROM docs`).all() as Array<{\n path: string;\n _mtime: number;\n _size: number;\n }>;\n const existing = new Map(existingRows.map((r) => [r.path, r]));\n const vanished = existingRows.filter((r) => !currentSet.has(r.path)).map((r) => r.path);\n\n const toReparse = files.filter((f) => {\n const row = existing.get(f.relPath);\n return !row || row._mtime !== f.mtimeMs || row._size !== f.size;\n });\n\n if (vanished.length === 0 && toReparse.length === 0) return { parsed: 0, warnings: [] };\n\n const seenColumns = getColumns(db);\n const newColumns: string[] = [];\n const parsedDocs: ParsedDoc[] = [];\n const warnings: string[] = [];\n\n for (const file of toReparse) {\n const { doc, warnings: fileWarnings } = parseFile(file);\n warnings.push(...fileWarnings);\n for (const key of Object.keys(doc.data)) {\n if (!seenColumns.has(key)) {\n seenColumns.add(key);\n newColumns.push(key);\n }\n }\n parsedDocs.push(doc);\n }\n\n // seenColumns already contains the reserved columns (they're real columns\n // of `docs`), so spreading it alone avoids duplicate names in the INSERT.\n const allColumns = [...seenColumns];\n const insertSql = `INSERT OR REPLACE INTO docs (${allColumns.map(quoteIdent).join(', ')}) VALUES (${allColumns.map(() => '?').join(', ')})`;\n\n db.exec('BEGIN');\n try {\n for (const col of newColumns) db.exec(`ALTER TABLE docs ADD COLUMN ${quoteIdent(col)}`);\n if (vanished.length > 0) {\n const del = db.prepare(`DELETE FROM docs WHERE \"path\" = ?`);\n for (const path of vanished) del.run(path);\n }\n if (parsedDocs.length > 0) {\n const insert = db.prepare(insertSql);\n for (const doc of parsedDocs) {\n const values = allColumns.map((col) => {\n if (col === 'path') return doc.relPath;\n if (col === '_mtime') return doc.mtimeMs;\n if (col === '_size') return doc.size;\n return doc.data[col] ?? null;\n });\n insert.run(...values);\n }\n }\n db.exec('COMMIT');\n } catch (err) {\n db.exec('ROLLBACK');\n throw err;\n }\n\n return { parsed: parsedDocs.length, warnings };\n}\n\n// open(resolved config): open (or create) the on-disk SQLite cache at\n// `<baseDir>/.sense/cache.db`, reconcile against the filesystem, and return\n// a live handle. The DB is a warm start, never a source of truth -- the\n// `.md` files remain truth. Takes only an already-resolved config; discovery\n// and version-gating are config.ts's job, not this one's.\nexport function open(cfg: ResolvedConfig): OpenResult {\n const stateDir = join(cfg.baseDir, STATE_DIR);\n mkdirSync(stateDir, { recursive: true });\n const dbPath = join(stateDir, DB_FILENAME);\n\n const db = new DatabaseSync(dbPath);\n db.exec('PRAGMA journal_mode = WAL');\n db.exec('PRAGMA busy_timeout = 5000');\n registerFunctions(db);\n\n ensureSchema(db);\n const { parsed, warnings } = reconcile(db, cfg, cfg.baseDir);\n\n return { db, cfg, dbPath, parsed, warnings };\n}\n\n// Delete the `.sense/` state dir entirely and reconcile fresh -- the manual\n// reset for lingering columns or a doubted cache.\nexport function rebuild(cfg: ResolvedConfig): OpenResult {\n rmSync(join(cfg.baseDir, STATE_DIR), { recursive: true, force: true });\n return open(cfg);\n}\n"],"names":["mkdirSync","rmSync","join","DatabaseSync","STATE_DIR","listFiles","parseFile","DB_FILENAME","SCHEMA_VERSION","quoteIdent","name","split","registerFunctions","db","function","deterministic","varargs","field","value","undefined","needle","String","startsWith","parsed","JSON","parse","Array","isArray","some","item","includes","getColumns","rows","prepare","all","Set","map","r","ensureSchema","exec","getMeta","setMeta","key","row","get","run","docCount","n","reconcile","cfg","baseDir","files","currentSet","f","relPath","existingRows","existing","Map","path","vanished","filter","has","toReparse","_mtime","mtimeMs","_size","size","length","warnings","seenColumns","newColumns","parsedDocs","file","doc","fileWarnings","push","Object","keys","data","add","allColumns","insertSql","col","del","insert","values","err","open","stateDir","recursive","dbPath","rebuild","force"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE';
2
+ export declare class SenseError extends Error {
3
+ code: SenseErrorCode;
4
+ constructor(code: SenseErrorCode, message: string);
5
+ }
@@ -0,0 +1,10 @@
1
+ // Typed errors for library modules. Library code (config.ts, scan.ts,
2
+ // db.ts, watch.ts) never calls process.exit and never prints -- it throws.
3
+ // Only cli.ts catches, prints the message, and maps it to an exit code.
4
+ export class SenseError extends Error {
5
+ constructor(code, message){
6
+ super(message);
7
+ this.name = 'SenseError';
8
+ this.code = code;
9
+ }
10
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/errors.ts"],"sourcesContent":["// Typed errors for library modules. Library code (config.ts, scan.ts,\n// db.ts, watch.ts) never calls process.exit and never prints -- it throws.\n// Only cli.ts catches, prints the message, and maps it to an exit code.\nexport type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE';\n\nexport class SenseError extends Error {\n code: SenseErrorCode;\n\n constructor(code: SenseErrorCode, message: string) {\n super(message);\n this.name = 'SenseError';\n this.code = code;\n }\n}\n"],"names":["SenseError","Error","code","message","name"],"mappings":"AAAA,sEAAsE;AACtE,2EAA2E;AAC3E,wEAAwE;AAGxE,OAAO,MAAMA,mBAAmBC;IAG9B,YAAYC,IAAoB,EAAEC,OAAe,CAAE;QACjD,KAAK,CAACA;QACN,IAAI,CAACC,IAAI,GAAG;QACZ,IAAI,CAACF,IAAI,GAAGA;IACd;AACF"}
@@ -0,0 +1,12 @@
1
+ export type { Config, ResolvedConfig } from './config.js';
2
+ export { CONFIG_FILENAME, findConfigPath, initConfig, loadConfig, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config.js';
3
+ export type { OpenResult } from './db.js';
4
+ export { DB_FILENAME, docCount, getMeta, open, rebuild, reconcile, setMeta } from './db.js';
5
+ export type { SenseErrorCode } from './errors.js';
6
+ export { SenseError } from './errors.js';
7
+ export type { Row } from './output.js';
8
+ export { printRows } from './output.js';
9
+ export type { FileStat, ParsedDoc } from './scan.js';
10
+ export { listFiles, parseFile } from './scan.js';
11
+ export type { WatchEvent, WatchOptions } from './watch.js';
12
+ export { runWatch } from './watch.js';
@@ -0,0 +1,9 @@
1
+ // Public library API. cli.ts is a consumer of this surface, not a source of
2
+ // truth for it -- anything a test or another tool needs from sense should be
3
+ // exported here rather than reached into src/ by relative path.
4
+ export { CONFIG_FILENAME, findConfigPath, initConfig, loadConfig, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config.js';
5
+ export { DB_FILENAME, docCount, getMeta, open, rebuild, reconcile, setMeta } from './db.js';
6
+ export { SenseError } from './errors.js';
7
+ export { printRows } from './output.js';
8
+ export { listFiles, parseFile } from './scan.js';
9
+ export { runWatch } from './watch.js';
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/index.ts"],"sourcesContent":["// Public library API. cli.ts is a consumer of this surface, not a source of\n// truth for it -- anything a test or another tool needs from sense should be\n// exported here rather than reached into src/ by relative path.\n\nexport type { Config, ResolvedConfig } from './config.ts';\nexport { CONFIG_FILENAME, findConfigPath, initConfig, loadConfig, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config.ts';\n\nexport type { OpenResult } from './db.ts';\nexport { DB_FILENAME, docCount, getMeta, open, rebuild, reconcile, setMeta } from './db.ts';\n\nexport type { SenseErrorCode } from './errors.ts';\nexport { SenseError } from './errors.ts';\n\nexport type { Row } from './output.ts';\nexport { printRows } from './output.ts';\n\nexport type { FileStat, ParsedDoc } from './scan.ts';\nexport { listFiles, parseFile } from './scan.ts';\n\nexport type { WatchEvent, WatchOptions } from './watch.ts';\nexport { runWatch } from './watch.ts';\n"],"names":["CONFIG_FILENAME","findConfigPath","initConfig","loadConfig","STATE_DIR","SUPPORTED_CONFIG_VERSION","DB_FILENAME","docCount","getMeta","open","rebuild","reconcile","setMeta","SenseError","printRows","listFiles","parseFile","runWatch"],"mappings":"AAAA,4EAA4E;AAC5E,6EAA6E;AAC7E,gEAAgE;AAGhE,SAASA,eAAe,EAAEC,cAAc,EAAEC,UAAU,EAAEC,UAAU,EAAEC,SAAS,EAAEC,wBAAwB,QAAQ,cAAc;AAG3H,SAASC,WAAW,EAAEC,QAAQ,EAAEC,OAAO,EAAEC,IAAI,EAAEC,OAAO,EAAEC,SAAS,EAAEC,OAAO,QAAQ,UAAU;AAG5F,SAASC,UAAU,QAAQ,cAAc;AAGzC,SAASC,SAAS,QAAQ,cAAc;AAGxC,SAASC,SAAS,EAAEC,SAAS,QAAQ,YAAY;AAGjD,SAASC,QAAQ,QAAQ,aAAa"}
@@ -0,0 +1,2 @@
1
+ export type Row = Record<string, unknown>;
2
+ export declare function printRows(rows: Row[], format: 'table' | 'json'): void;
@@ -0,0 +1,25 @@
1
+ // rows -> table (default) | json
2
+ export function printRows(rows, format) {
3
+ if (format === 'json') {
4
+ console.log(JSON.stringify(rows, null, 2));
5
+ return;
6
+ }
7
+ if (rows.length === 0) {
8
+ console.log('(0 rows)');
9
+ return;
10
+ }
11
+ const columns = Object.keys(rows[0]);
12
+ const widths = columns.map((col)=>Math.max(col.length, ...rows.map((row)=>{
13
+ var _row_col;
14
+ return String((_row_col = row[col]) !== null && _row_col !== void 0 ? _row_col : '').length;
15
+ })));
16
+ const formatRow = (values)=>values.map((value, i)=>value.padEnd(widths[i])).join(' ');
17
+ console.log(formatRow(columns));
18
+ console.log(widths.map((w)=>'-'.repeat(w)).join(' '));
19
+ for (const row of rows){
20
+ console.log(formatRow(columns.map((col)=>{
21
+ var _row_col;
22
+ return String((_row_col = row[col]) !== null && _row_col !== void 0 ? _row_col : '');
23
+ })));
24
+ }
25
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/output.ts"],"sourcesContent":["// rows -> table (default) | json\n\nexport type Row = Record<string, unknown>;\n\nexport function printRows(rows: Row[], format: 'table' | 'json'): void {\n if (format === 'json') {\n console.log(JSON.stringify(rows, null, 2));\n return;\n }\n\n if (rows.length === 0) {\n console.log('(0 rows)');\n return;\n }\n\n const columns = Object.keys(rows[0]);\n const widths = columns.map((col) => Math.max(col.length, ...rows.map((row) => String(row[col] ?? '').length)));\n\n const formatRow = (values: string[]) => values.map((value, i) => value.padEnd(widths[i])).join(' ');\n\n console.log(formatRow(columns));\n console.log(widths.map((w) => '-'.repeat(w)).join(' '));\n for (const row of rows) {\n console.log(formatRow(columns.map((col) => String(row[col] ?? ''))));\n }\n}\n"],"names":["printRows","rows","format","console","log","JSON","stringify","length","columns","Object","keys","widths","map","col","Math","max","row","String","formatRow","values","value","i","padEnd","join","w","repeat"],"mappings":"AAAA,iCAAiC;AAIjC,OAAO,SAASA,UAAUC,IAAW,EAAEC,MAAwB;IAC7D,IAAIA,WAAW,QAAQ;QACrBC,QAAQC,GAAG,CAACC,KAAKC,SAAS,CAACL,MAAM,MAAM;QACvC;IACF;IAEA,IAAIA,KAAKM,MAAM,KAAK,GAAG;QACrBJ,QAAQC,GAAG,CAAC;QACZ;IACF;IAEA,MAAMI,UAAUC,OAAOC,IAAI,CAACT,IAAI,CAAC,EAAE;IACnC,MAAMU,SAASH,QAAQI,GAAG,CAAC,CAACC,MAAQC,KAAKC,GAAG,CAACF,IAAIN,MAAM,KAAKN,KAAKW,GAAG,CAAC,CAACI;gBAAeA;mBAAPC,QAAOD,WAAAA,GAAG,CAACH,IAAI,cAARG,sBAAAA,WAAY,IAAIT,MAAM;;IAE3G,MAAMW,YAAY,CAACC,SAAqBA,OAAOP,GAAG,CAAC,CAACQ,OAAOC,IAAMD,MAAME,MAAM,CAACX,MAAM,CAACU,EAAE,GAAGE,IAAI,CAAC;IAE/FpB,QAAQC,GAAG,CAACc,UAAUV;IACtBL,QAAQC,GAAG,CAACO,OAAOC,GAAG,CAAC,CAACY,IAAM,IAAIC,MAAM,CAACD,IAAID,IAAI,CAAC;IAClD,KAAK,MAAMP,OAAOf,KAAM;QACtBE,QAAQC,GAAG,CAACc,UAAUV,QAAQI,GAAG,CAAC,CAACC;gBAAeG;mBAAPC,QAAOD,WAAAA,GAAG,CAACH,IAAI,cAARG,sBAAAA,WAAY;;IAChE;AACF"}
@@ -0,0 +1 @@
1
+ { "type": "module" }
@@ -0,0 +1,18 @@
1
+ import type { Config } from './config.js';
2
+ export interface FileStat {
3
+ relPath: string;
4
+ absPath: string;
5
+ mtimeMs: number;
6
+ size: number;
7
+ }
8
+ export declare function listFiles(cfg: Config, baseDir: string): FileStat[];
9
+ export interface ParsedDoc {
10
+ relPath: string;
11
+ mtimeMs: number;
12
+ size: number;
13
+ data: Record<string, string | number | null>;
14
+ }
15
+ export declare function parseFile(file: FileStat): {
16
+ doc: ParsedDoc;
17
+ warnings: string[];
18
+ };
@@ -0,0 +1,69 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import fastGlob from 'fast-glob';
4
+ import matter from 'gray-matter';
5
+ // Filesystem -> rows. Pure data in, data + warnings out -- no node:sqlite,
6
+ // no printing. db.ts is the only thing that knows what to do with the
7
+ // result (diffing against `docs`, writing SQL).
8
+ // Reserved `docs` columns: the real file path plus the mtime/size pair used
9
+ // to detect staleness. A frontmatter key literally named one of these would
10
+ // collide, so it's dropped (with a warning returned to the caller) rather
11
+ // than clobbering the column.
12
+ const RESERVED_COLUMNS = new Set([
13
+ 'path',
14
+ '_mtime',
15
+ '_size'
16
+ ]);
17
+ // Glob the files `cfg.scan.include` matches under `baseDir`, stat each.
18
+ // Sorted for deterministic output.
19
+ export function listFiles(cfg, baseDir) {
20
+ const relPaths = fastGlob.sync(cfg.scan.include, {
21
+ cwd: baseDir
22
+ }).sort();
23
+ return relPaths.map((relPath)=>{
24
+ const absPath = join(baseDir, relPath);
25
+ const st = statSync(absPath);
26
+ return {
27
+ relPath,
28
+ absPath,
29
+ mtimeMs: st.mtimeMs,
30
+ size: st.size
31
+ };
32
+ });
33
+ }
34
+ // Value mapping: strings/numbers as-is; booleans -> 0/1; dates -> ISO
35
+ // strings (lexicographic = chronological, no date lib); arrays/objects ->
36
+ // JSON text. `null`/`undefined` map to SQL NULL.
37
+ function mapValue(value) {
38
+ if (value === null || value === undefined) return null;
39
+ if (value instanceof Date) return value.toISOString();
40
+ if (typeof value === 'boolean') return value ? 1 : 0;
41
+ if (typeof value === 'string' || typeof value === 'number') return value;
42
+ // arrays and plain objects
43
+ return JSON.stringify(value);
44
+ }
45
+ // Read + parse one file's frontmatter, mapping values and dropping reserved
46
+ // keys. Returns the parsed doc plus any warnings (e.g. a reserved key
47
+ // collision) for the caller to surface.
48
+ export function parseFile(file) {
49
+ const raw = readFileSync(file.absPath, 'utf8');
50
+ const { data } = matter(raw);
51
+ const warnings = [];
52
+ const mapped = {};
53
+ for (const key of Object.keys(data)){
54
+ if (RESERVED_COLUMNS.has(key)) {
55
+ warnings.push(`warning: ${file.relPath} has a frontmatter key named "${key}", which is reserved; ignoring it`);
56
+ continue;
57
+ }
58
+ mapped[key] = mapValue(data[key]);
59
+ }
60
+ return {
61
+ doc: {
62
+ relPath: file.relPath,
63
+ mtimeMs: file.mtimeMs,
64
+ size: file.size,
65
+ data: mapped
66
+ },
67
+ warnings
68
+ };
69
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/scan.ts"],"sourcesContent":["import { readFileSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\nimport fastGlob from 'fast-glob';\nimport matter from 'gray-matter';\nimport type { Config } from './config.ts';\n\n// Filesystem -> rows. Pure data in, data + warnings out -- no node:sqlite,\n// no printing. db.ts is the only thing that knows what to do with the\n// result (diffing against `docs`, writing SQL).\n\n// Reserved `docs` columns: the real file path plus the mtime/size pair used\n// to detect staleness. A frontmatter key literally named one of these would\n// collide, so it's dropped (with a warning returned to the caller) rather\n// than clobbering the column.\nconst RESERVED_COLUMNS = new Set(['path', '_mtime', '_size']);\n\nexport interface FileStat {\n relPath: string;\n absPath: string;\n mtimeMs: number;\n size: number;\n}\n\n// Glob the files `cfg.scan.include` matches under `baseDir`, stat each.\n// Sorted for deterministic output.\nexport function listFiles(cfg: Config, baseDir: string): FileStat[] {\n const relPaths = fastGlob.sync(cfg.scan.include, { cwd: baseDir }).sort();\n return relPaths.map((relPath) => {\n const absPath = join(baseDir, relPath);\n const st = statSync(absPath);\n return { relPath, absPath, mtimeMs: st.mtimeMs, size: st.size };\n });\n}\n\nexport interface ParsedDoc {\n relPath: string;\n mtimeMs: number;\n size: number;\n // Already value-mapped (booleans -> 0/1, dates -> ISO, arrays/objects ->\n // JSON text), reserved keys already dropped -- ready to bind into SQL.\n data: Record<string, string | number | null>;\n}\n\n// Value mapping: strings/numbers as-is; booleans -> 0/1; dates -> ISO\n// strings (lexicographic = chronological, no date lib); arrays/objects ->\n// JSON text. `null`/`undefined` map to SQL NULL.\nfunction mapValue(value: unknown): string | number | null {\n if (value === null || value === undefined) return null;\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'boolean') return value ? 1 : 0;\n if (typeof value === 'string' || typeof value === 'number') return value;\n // arrays and plain objects\n return JSON.stringify(value);\n}\n\n// Read + parse one file's frontmatter, mapping values and dropping reserved\n// keys. Returns the parsed doc plus any warnings (e.g. a reserved key\n// collision) for the caller to surface.\nexport function parseFile(file: FileStat): { doc: ParsedDoc; warnings: string[] } {\n const raw = readFileSync(file.absPath, 'utf8');\n const { data } = matter(raw);\n\n const warnings: string[] = [];\n const mapped: Record<string, string | number | null> = {};\n\n for (const key of Object.keys(data)) {\n if (RESERVED_COLUMNS.has(key)) {\n warnings.push(`warning: ${file.relPath} has a frontmatter key named \"${key}\", which is reserved; ignoring it`);\n continue;\n }\n mapped[key] = mapValue(data[key]);\n }\n\n return {\n doc: { relPath: file.relPath, mtimeMs: file.mtimeMs, size: file.size, data: mapped },\n warnings,\n };\n}\n"],"names":["readFileSync","statSync","join","fastGlob","matter","RESERVED_COLUMNS","Set","listFiles","cfg","baseDir","relPaths","sync","scan","include","cwd","sort","map","relPath","absPath","st","mtimeMs","size","mapValue","value","undefined","Date","toISOString","JSON","stringify","parseFile","file","raw","data","warnings","mapped","key","Object","keys","has","push","doc"],"mappings":"AAAA,SAASA,YAAY,EAAEC,QAAQ,QAAQ,UAAU;AACjD,SAASC,IAAI,QAAQ,YAAY;AACjC,OAAOC,cAAc,YAAY;AACjC,OAAOC,YAAY,cAAc;AAGjC,2EAA2E;AAC3E,sEAAsE;AACtE,gDAAgD;AAEhD,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,8BAA8B;AAC9B,MAAMC,mBAAmB,IAAIC,IAAI;IAAC;IAAQ;IAAU;CAAQ;AAS5D,wEAAwE;AACxE,mCAAmC;AACnC,OAAO,SAASC,UAAUC,GAAW,EAAEC,OAAe;IACpD,MAAMC,WAAWP,SAASQ,IAAI,CAACH,IAAII,IAAI,CAACC,OAAO,EAAE;QAAEC,KAAKL;IAAQ,GAAGM,IAAI;IACvE,OAAOL,SAASM,GAAG,CAAC,CAACC;QACnB,MAAMC,UAAUhB,KAAKO,SAASQ;QAC9B,MAAME,KAAKlB,SAASiB;QACpB,OAAO;YAAED;YAASC;YAASE,SAASD,GAAGC,OAAO;YAAEC,MAAMF,GAAGE,IAAI;QAAC;IAChE;AACF;AAWA,sEAAsE;AACtE,0EAA0E;AAC1E,iDAAiD;AACjD,SAASC,SAASC,KAAc;IAC9B,IAAIA,UAAU,QAAQA,UAAUC,WAAW,OAAO;IAClD,IAAID,iBAAiBE,MAAM,OAAOF,MAAMG,WAAW;IACnD,IAAI,OAAOH,UAAU,WAAW,OAAOA,QAAQ,IAAI;IACnD,IAAI,OAAOA,UAAU,YAAY,OAAOA,UAAU,UAAU,OAAOA;IACnE,2BAA2B;IAC3B,OAAOI,KAAKC,SAAS,CAACL;AACxB;AAEA,4EAA4E;AAC5E,sEAAsE;AACtE,wCAAwC;AACxC,OAAO,SAASM,UAAUC,IAAc;IACtC,MAAMC,MAAM/B,aAAa8B,KAAKZ,OAAO,EAAE;IACvC,MAAM,EAAEc,IAAI,EAAE,GAAG5B,OAAO2B;IAExB,MAAME,WAAqB,EAAE;IAC7B,MAAMC,SAAiD,CAAC;IAExD,KAAK,MAAMC,OAAOC,OAAOC,IAAI,CAACL,MAAO;QACnC,IAAI3B,iBAAiBiC,GAAG,CAACH,MAAM;YAC7BF,SAASM,IAAI,CAAC,CAAC,SAAS,EAAET,KAAKb,OAAO,CAAC,8BAA8B,EAAEkB,IAAI,iCAAiC,CAAC;YAC7G;QACF;QACAD,MAAM,CAACC,IAAI,GAAGb,SAASU,IAAI,CAACG,IAAI;IAClC;IAEA,OAAO;QACLK,KAAK;YAAEvB,SAASa,KAAKb,OAAO;YAAEG,SAASU,KAAKV,OAAO;YAAEC,MAAMS,KAAKT,IAAI;YAAEW,MAAME;QAAO;QACnFD;IACF;AACF"}
@@ -0,0 +1,19 @@
1
+ import type { ResolvedConfig } from './config.js';
2
+ export type WatchEvent = {
3
+ type: 'started';
4
+ baseDir: string;
5
+ dbPath: string;
6
+ } | {
7
+ type: 'reconciled';
8
+ parsed: number;
9
+ total: number;
10
+ warnings: string[];
11
+ } | {
12
+ type: 'reconcile-error';
13
+ message: string;
14
+ };
15
+ export interface WatchOptions {
16
+ force?: boolean;
17
+ onEvent?: (event: WatchEvent) => void;
18
+ }
19
+ export declare function runWatch(cfg: ResolvedConfig, opts?: WatchOptions): Promise<void>;
@@ -0,0 +1,93 @@
1
+ import { watch as fsWatch } from 'node:fs';
2
+ import { STATE_DIR } from './config.js';
3
+ import { docCount, getMeta, open, reconcile, setMeta } from './db.js';
4
+ import { SenseError } from './errors.js';
5
+ // Watch is a cache pre-warmer, not a correctness mechanism: every query
6
+ // `open()` reconciles against the filesystem regardless, so a missed or
7
+ // coalesced fs event can never change a query result -- it only moves one
8
+ // file's parse from ahead-of-time to query-time. That's why any event, of
9
+ // any kind, triggers the exact same response: a debounced full reconcile.
10
+ // Per-file event handling would be complexity with no correctness payoff.
11
+ const DEBOUNCE_MS = 200;
12
+ const HEARTBEAT_INTERVAL_MS = 5000;
13
+ const STALE_HEARTBEAT_MS = 15000;
14
+ // Runs in the foreground until SIGINT/SIGTERM; never forks or daemonizes --
15
+ // process lifecycle belongs to the OS (launchd/systemd/terminal). Resolves
16
+ // on clean shutdown; throws SenseError("WATCH_ACTIVE", ...) if another
17
+ // watcher's heartbeat is still fresh and `--force` wasn't given.
18
+ export async function runWatch(cfg, opts = {}) {
19
+ var _opts_onEvent;
20
+ const onEvent = (_opts_onEvent = opts.onEvent) !== null && _opts_onEvent !== void 0 ? _opts_onEvent : ()=>{};
21
+ const { db, dbPath, warnings: initialWarnings, parsed: initialParsed } = open(cfg);
22
+ const baseDir = cfg.baseDir;
23
+ const existingHeartbeat = getMeta(db, 'watch_heartbeat');
24
+ if (existingHeartbeat && !opts.force) {
25
+ const age = Date.now() - Date.parse(existingHeartbeat);
26
+ if (age >= 0 && age < STALE_HEARTBEAT_MS) {
27
+ db.close();
28
+ throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);
29
+ }
30
+ }
31
+ onEvent({
32
+ type: 'started',
33
+ baseDir,
34
+ dbPath
35
+ });
36
+ if (initialWarnings.length > 0 || initialParsed > 0) {
37
+ onEvent({
38
+ type: 'reconciled',
39
+ parsed: initialParsed,
40
+ total: docCount(db),
41
+ warnings: initialWarnings
42
+ });
43
+ }
44
+ const touchHeartbeat = ()=>{
45
+ setMeta(db, 'watch_heartbeat', new Date().toISOString());
46
+ setMeta(db, 'watch_pid', String(process.pid));
47
+ };
48
+ touchHeartbeat();
49
+ let debounceTimer = null;
50
+ const scheduleReconcile = ()=>{
51
+ if (debounceTimer) clearTimeout(debounceTimer);
52
+ debounceTimer = setTimeout(()=>{
53
+ debounceTimer = null;
54
+ try {
55
+ const { parsed, warnings } = reconcile(db, cfg, baseDir);
56
+ onEvent({
57
+ type: 'reconciled',
58
+ parsed,
59
+ total: docCount(db),
60
+ warnings
61
+ });
62
+ } catch (err) {
63
+ onEvent({
64
+ type: 'reconcile-error',
65
+ message: err.message
66
+ });
67
+ }
68
+ }, DEBOUNCE_MS);
69
+ };
70
+ // Ignore events from our own state dir: the heartbeat writes cache.db
71
+ // every few seconds, and without this filter each write would schedule a
72
+ // (harmless but pointless) reconcile forever — a perpetual self-trigger.
73
+ const watcher = fsWatch(baseDir, {
74
+ recursive: true
75
+ }, (_event, filename)=>{
76
+ if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;
77
+ scheduleReconcile();
78
+ });
79
+ const heartbeatTimer = setInterval(touchHeartbeat, HEARTBEAT_INTERVAL_MS);
80
+ return new Promise((resolveShutdown)=>{
81
+ const cleanExit = ()=>{
82
+ clearInterval(heartbeatTimer);
83
+ if (debounceTimer) clearTimeout(debounceTimer);
84
+ watcher.close();
85
+ setMeta(db, 'watch_heartbeat', null);
86
+ setMeta(db, 'watch_pid', null);
87
+ db.close();
88
+ resolveShutdown();
89
+ };
90
+ process.once('SIGINT', cleanExit);
91
+ process.once('SIGTERM', cleanExit);
92
+ });
93
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/watch.ts"],"sourcesContent":["import { watch as fsWatch } from 'node:fs';\nimport type { ResolvedConfig } from './config.ts';\nimport { STATE_DIR } from './config.ts';\nimport { docCount, getMeta, open, reconcile, setMeta } from './db.ts';\nimport { SenseError } from './errors.ts';\n\n// Watch is a cache pre-warmer, not a correctness mechanism: every query\n// `open()` reconciles against the filesystem regardless, so a missed or\n// coalesced fs event can never change a query result -- it only moves one\n// file's parse from ahead-of-time to query-time. That's why any event, of\n// any kind, triggers the exact same response: a debounced full reconcile.\n// Per-file event handling would be complexity with no correctness payoff.\nconst DEBOUNCE_MS = 200;\nconst HEARTBEAT_INTERVAL_MS = 5000;\nconst STALE_HEARTBEAT_MS = 15000;\n\n// This module never prints -- it emits events via `onEvent` and throws on\n// the one exit-worthy condition (a live watcher already present). cli.ts is\n// the only place that touches console/process.exit.\nexport type WatchEvent = { type: 'started'; baseDir: string; dbPath: string } | { type: 'reconciled'; parsed: number; total: number; warnings: string[] } | { type: 'reconcile-error'; message: string };\n\nexport interface WatchOptions {\n force?: boolean;\n onEvent?: (event: WatchEvent) => void;\n}\n\n// Runs in the foreground until SIGINT/SIGTERM; never forks or daemonizes --\n// process lifecycle belongs to the OS (launchd/systemd/terminal). Resolves\n// on clean shutdown; throws SenseError(\"WATCH_ACTIVE\", ...) if another\n// watcher's heartbeat is still fresh and `--force` wasn't given.\nexport async function runWatch(cfg: ResolvedConfig, opts: WatchOptions = {}): Promise<void> {\n const onEvent = opts.onEvent ?? (() => {});\n const { db, dbPath, warnings: initialWarnings, parsed: initialParsed } = open(cfg);\n const baseDir = cfg.baseDir;\n\n const existingHeartbeat = getMeta(db, 'watch_heartbeat');\n if (existingHeartbeat && !opts.force) {\n const age = Date.now() - Date.parse(existingHeartbeat);\n if (age >= 0 && age < STALE_HEARTBEAT_MS) {\n db.close();\n throw new SenseError('WATCH_ACTIVE', `another watcher appears active (heartbeat ${Math.round(age / 1000)}s ago); use --force to override`);\n }\n }\n\n onEvent({ type: 'started', baseDir, dbPath });\n if (initialWarnings.length > 0 || initialParsed > 0) {\n onEvent({ type: 'reconciled', parsed: initialParsed, total: docCount(db), warnings: initialWarnings });\n }\n\n const touchHeartbeat = () => {\n setMeta(db, 'watch_heartbeat', new Date().toISOString());\n setMeta(db, 'watch_pid', String(process.pid));\n };\n touchHeartbeat();\n\n let debounceTimer: NodeJS.Timeout | null = null;\n const scheduleReconcile = () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(() => {\n debounceTimer = null;\n try {\n const { parsed, warnings } = reconcile(db, cfg, baseDir);\n onEvent({ type: 'reconciled', parsed, total: docCount(db), warnings });\n } catch (err) {\n onEvent({ type: 'reconcile-error', message: (err as Error).message });\n }\n }, DEBOUNCE_MS);\n };\n\n // Ignore events from our own state dir: the heartbeat writes cache.db\n // every few seconds, and without this filter each write would schedule a\n // (harmless but pointless) reconcile forever — a perpetual self-trigger.\n const watcher = fsWatch(baseDir, { recursive: true }, (_event, filename) => {\n if (typeof filename === 'string' && filename.startsWith(STATE_DIR)) return;\n scheduleReconcile();\n });\n const heartbeatTimer = setInterval(touchHeartbeat, HEARTBEAT_INTERVAL_MS);\n\n return new Promise<void>((resolveShutdown) => {\n const cleanExit = () => {\n clearInterval(heartbeatTimer);\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n setMeta(db, 'watch_heartbeat', null);\n setMeta(db, 'watch_pid', null);\n db.close();\n resolveShutdown();\n };\n process.once('SIGINT', cleanExit);\n process.once('SIGTERM', cleanExit);\n });\n}\n"],"names":["watch","fsWatch","STATE_DIR","docCount","getMeta","open","reconcile","setMeta","SenseError","DEBOUNCE_MS","HEARTBEAT_INTERVAL_MS","STALE_HEARTBEAT_MS","runWatch","cfg","opts","onEvent","db","dbPath","warnings","initialWarnings","parsed","initialParsed","baseDir","existingHeartbeat","force","age","Date","now","parse","close","Math","round","type","length","total","touchHeartbeat","toISOString","String","process","pid","debounceTimer","scheduleReconcile","clearTimeout","setTimeout","err","message","watcher","recursive","_event","filename","startsWith","heartbeatTimer","setInterval","Promise","resolveShutdown","cleanExit","clearInterval","once"],"mappings":"AAAA,SAASA,SAASC,OAAO,QAAQ,UAAU;AAE3C,SAASC,SAAS,QAAQ,cAAc;AACxC,SAASC,QAAQ,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,OAAO,QAAQ,UAAU;AACtE,SAASC,UAAU,QAAQ,cAAc;AAEzC,wEAAwE;AACxE,wEAAwE;AACxE,0EAA0E;AAC1E,0EAA0E;AAC1E,0EAA0E;AAC1E,0EAA0E;AAC1E,MAAMC,cAAc;AACpB,MAAMC,wBAAwB;AAC9B,MAAMC,qBAAqB;AAY3B,4EAA4E;AAC5E,2EAA2E;AAC3E,uEAAuE;AACvE,iEAAiE;AACjE,OAAO,eAAeC,SAASC,GAAmB,EAAEC,OAAqB,CAAC,CAAC;QACzDA;IAAhB,MAAMC,WAAUD,gBAAAA,KAAKC,OAAO,cAAZD,2BAAAA,gBAAiB,KAAO;IACxC,MAAM,EAAEE,EAAE,EAAEC,MAAM,EAAEC,UAAUC,eAAe,EAAEC,QAAQC,aAAa,EAAE,GAAGhB,KAAKQ;IAC9E,MAAMS,UAAUT,IAAIS,OAAO;IAE3B,MAAMC,oBAAoBnB,QAAQY,IAAI;IACtC,IAAIO,qBAAqB,CAACT,KAAKU,KAAK,EAAE;QACpC,MAAMC,MAAMC,KAAKC,GAAG,KAAKD,KAAKE,KAAK,CAACL;QACpC,IAAIE,OAAO,KAAKA,MAAMd,oBAAoB;YACxCK,GAAGa,KAAK;YACR,MAAM,IAAIrB,WAAW,gBAAgB,CAAC,0CAA0C,EAAEsB,KAAKC,KAAK,CAACN,MAAM,MAAM,+BAA+B,CAAC;QAC3I;IACF;IAEAV,QAAQ;QAAEiB,MAAM;QAAWV;QAASL;IAAO;IAC3C,IAAIE,gBAAgBc,MAAM,GAAG,KAAKZ,gBAAgB,GAAG;QACnDN,QAAQ;YAAEiB,MAAM;YAAcZ,QAAQC;YAAea,OAAO/B,SAASa;YAAKE,UAAUC;QAAgB;IACtG;IAEA,MAAMgB,iBAAiB;QACrB5B,QAAQS,IAAI,mBAAmB,IAAIU,OAAOU,WAAW;QACrD7B,QAAQS,IAAI,aAAaqB,OAAOC,QAAQC,GAAG;IAC7C;IACAJ;IAEA,IAAIK,gBAAuC;IAC3C,MAAMC,oBAAoB;QACxB,IAAID,eAAeE,aAAaF;QAChCA,gBAAgBG,WAAW;YACzBH,gBAAgB;YAChB,IAAI;gBACF,MAAM,EAAEpB,MAAM,EAAEF,QAAQ,EAAE,GAAGZ,UAAUU,IAAIH,KAAKS;gBAChDP,QAAQ;oBAAEiB,MAAM;oBAAcZ;oBAAQc,OAAO/B,SAASa;oBAAKE;gBAAS;YACtE,EAAE,OAAO0B,KAAK;gBACZ7B,QAAQ;oBAAEiB,MAAM;oBAAmBa,SAAS,AAACD,IAAcC,OAAO;gBAAC;YACrE;QACF,GAAGpC;IACL;IAEA,sEAAsE;IACtE,yEAAyE;IACzE,yEAAyE;IACzE,MAAMqC,UAAU7C,QAAQqB,SAAS;QAAEyB,WAAW;IAAK,GAAG,CAACC,QAAQC;QAC7D,IAAI,OAAOA,aAAa,YAAYA,SAASC,UAAU,CAAChD,YAAY;QACpEuC;IACF;IACA,MAAMU,iBAAiBC,YAAYjB,gBAAgBzB;IAEnD,OAAO,IAAI2C,QAAc,CAACC;QACxB,MAAMC,YAAY;YAChBC,cAAcL;YACd,IAAIX,eAAeE,aAAaF;YAChCM,QAAQjB,KAAK;YACbtB,QAAQS,IAAI,mBAAmB;YAC/BT,QAAQS,IAAI,aAAa;YACzBA,GAAGa,KAAK;YACRyB;QACF;QACAhB,QAAQmB,IAAI,CAAC,UAAUF;QACvBjB,QAAQmB,IAAI,CAAC,WAAWF;IAC1B;AACF"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "sensemaking",
3
+ "version": "0.1.0",
4
+ "description": "Query a knowledge base you build with an agent: filter notes by frontmatter, then search inside them",
5
+ "keywords": [
6
+ "markdown",
7
+ "frontmatter",
8
+ "sql",
9
+ "sqlite",
10
+ "query",
11
+ "obsidian",
12
+ "notes",
13
+ "cli"
14
+ ],
15
+ "homepage": "https://github.com/kmalakoff/sensemaking",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+ssh://git@github.com/kmalakoff/sensemaking.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "Kevin Malakoff <kmalakoff@gmail.com> (https://github.com/kmalakoff)",
22
+ "type": "module",
23
+ "exports": {
24
+ ".": {
25
+ "import": "./dist/esm/index.js",
26
+ "require": "./dist/cjs/index.js"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "main": "dist/cjs/index.js",
31
+ "source": "src/index.ts",
32
+ "types": "dist/cjs/index.d.ts",
33
+ "bin": {
34
+ "sense": "bin/cli.js"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "skills",
39
+ "schema.json"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsds build",
43
+ "format": "tsds format",
44
+ "prepublishOnly": "tsds validate",
45
+ "test": "tsds test:node --no-timeouts",
46
+ "test:engines": "nvu engines tsds test:node --no-timeouts",
47
+ "version": "tsds version"
48
+ },
49
+ "dependencies": {
50
+ "fast-glob": "^3.3.3",
51
+ "gray-matter": "^4.0.3"
52
+ },
53
+ "devDependencies": {
54
+ "@types/mocha": "*",
55
+ "@types/node": "*",
56
+ "node-version-use": "*",
57
+ "ts-dev-stack": "*",
58
+ "tsds-config": "*"
59
+ },
60
+ "engines": {
61
+ "node": ">=22.13"
62
+ }
63
+ }
package/schema.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "sense.config.json",
4
+ "description": "Config for sense -- SQL over the frontmatter of a markdown tree.",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["scan", "queries"],
8
+ "properties": {
9
+ "$schema": {
10
+ "type": "string",
11
+ "description": "Points editors at this schema for autocomplete and validation. Not read by sense itself. Conventionally \"https://unpkg.com/sensemaking/schema.json\"."
12
+ },
13
+ "version": {
14
+ "type": "integer",
15
+ "enum": [1],
16
+ "description": "Config format version. Bumped only on breaking changes to this file's shape. Omit to default to 1; a version newer than this sense build supports makes it exit with an error rather than misinterpret the file."
17
+ },
18
+ "scan": {
19
+ "type": "object",
20
+ "additionalProperties": false,
21
+ "required": ["include"],
22
+ "description": "Which files become rows in the `docs` table.",
23
+ "properties": {
24
+ "include": {
25
+ "type": "array",
26
+ "items": { "type": "string" },
27
+ "minItems": 1,
28
+ "description": "Glob patterns (globby syntax), resolved relative to this config file's own directory -- never the invocation cwd."
29
+ }
30
+ }
31
+ },
32
+ "queries": {
33
+ "type": "object",
34
+ "description": "Named SQL queries runnable as `sense <name> [params...]`. Each value is a SQL SELECT against the `docs` table (one row per file, one column per discovered frontmatter key, plus reserved `path`/`_mtime`/`_size`). `?` placeholders bind to CLI positional arguments in order. The custom `has(field, value)` function does array-membership on a JSON-array field, substring match on a string field, and is always false on NULL. The names `init`, `query`, `watch`, `status`, and `rebuild` are reserved subcommands — a query with one of those names is unreachable from the CLI.",
35
+ "additionalProperties": { "type": "string" }
36
+ }
37
+ }
38
+ }