sensemaking 0.1.0 → 0.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.
@@ -1 +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","DB_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","SenseError","docCount","findConfigPath","getMeta","initConfig","listFiles","loadConfig","open","parseFile","printRows","rebuild","reconcile","runWatch","setMeta"],"mappings":"AAAA,4EAA4E;AAC5E,6EAA6E;AAC7E,gEAAgE;;;;;;;;;;;;QAGvDA;eAAAA,yBAAe;;QAGfC;eAAAA,iBAAW;;QAH8CC;eAAAA,mBAAS;;QAAEC;eAAAA,kCAAwB;;QAM5FC;eAAAA,oBAAU;;QAHGC;eAAAA,cAAQ;;QAHJC;eAAAA,wBAAc;;QAGRC;eAAAA,aAAO;;QAHGC;eAAAA,oBAAU;;QAY3CC;eAAAA,iBAAS;;QAZoCC;eAAAA,oBAAU;;QAGvBC;eAAAA,UAAI;;QASzBC;eAAAA,iBAAS;;QAHpBC;eAAAA,mBAAS;;QAN6BC;eAAAA,aAAO;;QAAEC;eAAAA,eAAS;;QAYxDC;eAAAA,iBAAQ;;QAZkDC;eAAAA,aAAO;;;wBAHmC;oBAG3B;wBAGvD;wBAGD;sBAGW;uBAGZ"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/index.ts"],"sourcesContent":["// Public library API.\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","DB_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","SenseError","docCount","findConfigPath","getMeta","initConfig","listFiles","loadConfig","open","parseFile","printRows","rebuild","reconcile","runWatch","setMeta"],"mappings":"AAAA,sBAAsB;;;;;;;;;;;;QAGbA;eAAAA,yBAAe;;QAGfC;eAAAA,iBAAW;;QAH8CC;eAAAA,mBAAS;;QAAEC;eAAAA,kCAAwB;;QAM5FC;eAAAA,oBAAU;;QAHGC;eAAAA,cAAQ;;QAHJC;eAAAA,wBAAc;;QAGRC;eAAAA,aAAO;;QAHGC;eAAAA,oBAAU;;QAY3CC;eAAAA,iBAAS;;QAZoCC;eAAAA,oBAAU;;QAGvBC;eAAAA,UAAI;;QASzBC;eAAAA,iBAAS;;QAHpBC;eAAAA,mBAAS;;QAN6BC;eAAAA,aAAO;;QAAEC;eAAAA,eAAS;;QAYxDC;eAAAA,iBAAQ;;QAZkDC;eAAAA,aAAO;;;wBAHmC;oBAG3B;wBAGvD;wBAGD;sBAGW;uBAGZ"}
@@ -34,23 +34,34 @@ function _unsupported_iterable_to_array(o, minLen) {
34
34
  if (n === "Map" || n === "Set") return Array.from(n);
35
35
  if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen);
36
36
  }
37
+ // Warned about on stderr (keeps --format json stdout machine-readable), never truncated.
38
+ var OVERSIZE_BYTES = 50000;
39
+ // Flatten embedded newlines so they can't break table column alignment; JSON output is left alone.
40
+ function cell(value) {
41
+ return String(value !== null && value !== void 0 ? value : '').replace(/\s*\n\s*/g, ' ');
42
+ }
37
43
  function printRows(rows, format) {
38
- var _Math;
39
- if (format === 'json') {
40
- console.log(JSON.stringify(rows, null, 2));
41
- return;
42
- }
43
- if (rows.length === 0) {
44
- console.log('(0 rows)');
45
- return;
44
+ var rendered = render(rows, format);
45
+ console.log(rendered);
46
+ if (rendered.length > OVERSIZE_BYTES) {
47
+ console.warn("warning: result is ".concat(Math.round(rendered.length / 1000), " KB across ").concat(rows.length, " row(s); consider a LIMIT, fewer columns, or snippet() instead of whole values"));
46
48
  }
49
+ }
50
+ function render(rows, format) {
51
+ var _Math;
52
+ if (format === 'json') return JSON.stringify(rows, null, 2);
53
+ if (rows.length === 0) return '(0 rows)';
47
54
  var columns = Object.keys(rows[0]);
48
- var widths = columns.map(function(col) {
55
+ var cells = rows.map(function(row) {
56
+ return columns.map(function(col) {
57
+ return cell(row[col]);
58
+ });
59
+ });
60
+ var widths = columns.map(function(col, i) {
49
61
  return (_Math = Math).max.apply(_Math, [
50
62
  col.length
51
- ].concat(_to_consumable_array(rows.map(function(row) {
52
- var _row_col;
53
- return String((_row_col = row[col]) !== null && _row_col !== void 0 ? _row_col : '').length;
63
+ ].concat(_to_consumable_array(cells.map(function(row) {
64
+ return row[i].length;
54
65
  }))));
55
66
  });
56
67
  var formatRow = function formatRow(values) {
@@ -58,33 +69,11 @@ function printRows(rows, format) {
58
69
  return value.padEnd(widths[i]);
59
70
  }).join(' ');
60
71
  };
61
- console.log(formatRow(columns));
62
- console.log(widths.map(function(w) {
63
- return '-'.repeat(w);
64
- }).join(' '));
65
- var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
66
- try {
67
- var _loop = function() {
68
- var row = _step.value;
69
- console.log(formatRow(columns.map(function(col) {
70
- var _row_col;
71
- return String((_row_col = row[col]) !== null && _row_col !== void 0 ? _row_col : '');
72
- })));
73
- };
74
- for(var _iterator = rows[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true)_loop();
75
- } catch (err) {
76
- _didIteratorError = true;
77
- _iteratorError = err;
78
- } finally{
79
- try {
80
- if (!_iteratorNormalCompletion && _iterator.return != null) {
81
- _iterator.return();
82
- }
83
- } finally{
84
- if (_didIteratorError) {
85
- throw _iteratorError;
86
- }
87
- }
88
- }
72
+ return [
73
+ formatRow(columns),
74
+ widths.map(function(w) {
75
+ return '-'.repeat(w);
76
+ }).join(' ')
77
+ ].concat(_to_consumable_array(cells.map(formatRow))).join('\n');
89
78
  }
90
79
  /* CJS INTEROP */ if (exports.__esModule && exports.default) { try { Object.defineProperty(exports.default, '__esModule', { value: true }); for (var key in exports) { exports.default[key] = exports[key]; } } catch (_) {}; module.exports = exports.default; }
@@ -1 +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","Math","console","log","JSON","stringify","length","columns","Object","keys","widths","map","col","max","row","String","formatRow","values","value","i","padEnd","join","w","repeat"],"mappings":"AAAA,iCAAiC;;;;;+BAIjBA;;;eAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAT,SAASA,UAAUC,IAAW,EAAEC,MAAwB;QAYzBC;IAXpC,IAAID,WAAW,QAAQ;QACrBE,QAAQC,GAAG,CAACC,KAAKC,SAAS,CAACN,MAAM,MAAM;QACvC;IACF;IAEA,IAAIA,KAAKO,MAAM,KAAK,GAAG;QACrBJ,QAAQC,GAAG,CAAC;QACZ;IACF;IAEA,IAAMI,UAAUC,OAAOC,IAAI,CAACV,IAAI,CAAC,EAAE;IACnC,IAAMW,SAASH,QAAQI,GAAG,CAAC,SAACC;eAAQX,CAAAA,QAAAA,MAAKY,GAAG,OAARZ,OAAAA;YAASW,IAAIN,MAAM;SAAsD,CAAzEL,OAAqB,qBAAGF,KAAKY,GAAG,CAAC,SAACG;gBAAeA;mBAAPC,QAAOD,WAAAA,GAAG,CAACF,IAAI,cAARE,sBAAAA,WAAY,IAAIR,MAAM;;;IAE3G,IAAMU,YAAY,mBAACC;eAAqBA,OAAON,GAAG,CAAC,SAACO,OAAOC;mBAAMD,MAAME,MAAM,CAACV,MAAM,CAACS,EAAE;WAAGE,IAAI,CAAC;;IAE/FnB,QAAQC,GAAG,CAACa,UAAUT;IACtBL,QAAQC,GAAG,CAACO,OAAOC,GAAG,CAAC,SAACW;eAAM,IAAIC,MAAM,CAACD;OAAID,IAAI,CAAC;QAC7C,kCAAA,2BAAA;;;YAAA,IAAMP,MAAN;YACHZ,QAAQC,GAAG,CAACa,UAAUT,QAAQI,GAAG,CAAC,SAACC;oBAAeE;uBAAPC,QAAOD,WAAAA,GAAG,CAACF,IAAI,cAARE,sBAAAA,WAAY;;QAChE;QAFA,QAAK,YAAaf,yBAAb,SAAA,6BAAA,QAAA,yBAAA;;QAAA;QAAA;;;iBAAA,6BAAA;gBAAA;;;gBAAA;sBAAA;;;;AAGP"}
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\n// Warned about on stderr (keeps --format json stdout machine-readable), never truncated.\nconst OVERSIZE_BYTES = 50_000;\n\n// Flatten embedded newlines so they can't break table column alignment; JSON output is left alone.\nfunction cell(value: unknown): string {\n return String(value ?? '').replace(/\\s*\\n\\s*/g, ' ');\n}\n\nexport function printRows(rows: Row[], format: 'table' | 'json'): void {\n const rendered = render(rows, format);\n console.log(rendered);\n if (rendered.length > OVERSIZE_BYTES) {\n console.warn(`warning: result is ${Math.round(rendered.length / 1000)} KB across ${rows.length} row(s); consider a LIMIT, fewer columns, or snippet() instead of whole values`);\n }\n}\n\nfunction render(rows: Row[], format: 'table' | 'json'): string {\n if (format === 'json') return JSON.stringify(rows, null, 2);\n if (rows.length === 0) return '(0 rows)';\n\n const columns = Object.keys(rows[0]);\n const cells = rows.map((row) => columns.map((col) => cell(row[col])));\n const widths = columns.map((col, i) => Math.max(col.length, ...cells.map((row) => row[i].length)));\n\n const formatRow = (values: string[]) => values.map((value, i) => value.padEnd(widths[i])).join(' ');\n\n return [formatRow(columns), widths.map((w) => '-'.repeat(w)).join(' '), ...cells.map(formatRow)].join('\\n');\n}\n"],"names":["printRows","OVERSIZE_BYTES","cell","value","String","replace","rows","format","rendered","render","console","log","length","warn","Math","round","JSON","stringify","columns","Object","keys","cells","map","row","col","widths","i","max","formatRow","values","padEnd","join","w","repeat"],"mappings":"AAAA,iCAAiC;;;;;+BAYjBA;;;eAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AARhB,yFAAyF;AACzF,IAAMC,iBAAiB;AAEvB,mGAAmG;AACnG,SAASC,KAAKC,KAAc;IAC1B,OAAOC,OAAOD,kBAAAA,mBAAAA,QAAS,IAAIE,OAAO,CAAC,aAAa;AAClD;AAEO,SAASL,UAAUM,IAAW,EAAEC,MAAwB;IAC7D,IAAMC,WAAWC,OAAOH,MAAMC;IAC9BG,QAAQC,GAAG,CAACH;IACZ,IAAIA,SAASI,MAAM,GAAGX,gBAAgB;QACpCS,QAAQG,IAAI,CAAC,AAAC,sBAAqEP,OAAhDQ,KAAKC,KAAK,CAACP,SAASI,MAAM,GAAG,OAAM,eAAyB,OAAZN,KAAKM,MAAM,EAAC;IACjG;AACF;AAEA,SAASH,OAAOH,IAAW,EAAEC,MAAwB;QAMZO;IALvC,IAAIP,WAAW,QAAQ,OAAOS,KAAKC,SAAS,CAACX,MAAM,MAAM;IACzD,IAAIA,KAAKM,MAAM,KAAK,GAAG,OAAO;IAE9B,IAAMM,UAAUC,OAAOC,IAAI,CAACd,IAAI,CAAC,EAAE;IACnC,IAAMe,QAAQf,KAAKgB,GAAG,CAAC,SAACC;eAAQL,QAAQI,GAAG,CAAC,SAACE;mBAAQtB,KAAKqB,GAAG,CAACC,IAAI;;;IAClE,IAAMC,SAASP,QAAQI,GAAG,CAAC,SAACE,KAAKE;eAAMZ,CAAAA,QAAAA,MAAKa,GAAG,OAARb,OAAAA;YAASU,IAAIZ,MAAM;SAAuC,CAA1DE,OAAqB,qBAAGO,MAAMC,GAAG,CAAC,SAACC;mBAAQA,GAAG,CAACG,EAAE,CAACd,MAAM;;;IAE/F,IAAMgB,YAAY,mBAACC;eAAqBA,OAAOP,GAAG,CAAC,SAACnB,OAAOuB;mBAAMvB,MAAM2B,MAAM,CAACL,MAAM,CAACC,EAAE;WAAGK,IAAI,CAAC;;IAE/F,OAAO;QAACH,UAAUV;QAAUO,OAAOH,GAAG,CAAC,SAACU;mBAAM,IAAIC,MAAM,CAACD;WAAID,IAAI,CAAC;KAA+B,CAA1F,OAAkE,qBAAGV,MAAMC,GAAG,CAACM,aAAYG,IAAI,CAAC;AACzG"}
@@ -11,6 +11,11 @@ export interface ParsedDoc {
11
11
  mtimeMs: number;
12
12
  size: number;
13
13
  data: Record<string, string | number | null>;
14
+ search: {
15
+ title: string;
16
+ summary: string;
17
+ text: string;
18
+ };
14
19
  }
15
20
  export declare function parseFile(file: FileStat): {
16
21
  doc: ParsedDoc;
@@ -11,6 +11,11 @@ export interface ParsedDoc {
11
11
  mtimeMs: number;
12
12
  size: number;
13
13
  data: Record<string, string | number | null>;
14
+ search: {
15
+ title: string;
16
+ summary: string;
17
+ text: string;
18
+ };
14
19
  }
15
20
  export declare function parseFile(file: FileStat): {
16
21
  doc: ParsedDoc;
package/dist/cjs/scan.js CHANGED
@@ -20,6 +20,7 @@ var _nodefs = require("node:fs");
20
20
  var _nodepath = require("node:path");
21
21
  var _fastglob = /*#__PURE__*/ _interop_require_default(require("fast-glob"));
22
22
  var _graymatter = /*#__PURE__*/ _interop_require_default(require("gray-matter"));
23
+ var _removemarkdown = /*#__PURE__*/ _interop_require_default(require("remove-markdown"));
23
24
  function _instanceof(left, right) {
24
25
  "@swc/helpers - instanceof";
25
26
  if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
@@ -33,18 +34,26 @@ function _interop_require_default(obj) {
33
34
  default: obj
34
35
  };
35
36
  }
36
- // Filesystem -> rows. Pure data in, data + warnings out -- no node:sqlite,
37
- // no printing. db.ts is the only thing that knows what to do with the
38
- // result (diffing against `docs`, writing SQL).
39
- // Reserved `docs` columns: the real file path plus the mtime/size pair used
40
- // to detect staleness. A frontmatter key literally named one of these would
41
- // collide, so it's dropped (with a warning returned to the caller) rather
42
- // than clobbering the column.
37
+ // Filesystem -> rows. Pure data in, data + warnings out; db.ts does the SQL.
38
+ // Reserved: colliding with these would clash with the `frontmatter` table's own columns or the `content` FTS5 table.
43
39
  var RESERVED_COLUMNS = new Set([
44
40
  'path',
45
41
  '_mtime',
46
- '_size'
42
+ '_size',
43
+ 'content'
47
44
  ]);
45
+ function normalizeText(value) {
46
+ if (value === null || value === undefined) return '';
47
+ return String(value).replace(/\s+/g, ' ').trim();
48
+ }
49
+ // Markdown is stripped at index time so snippets read as clean prose (also improves matching, e.g. `**bold**` indexes as `bold`).
50
+ // remove-markdown doesn't cover Obsidian wikilinks or table scaffolding, so those are handled first/after.
51
+ function stripText(value) {
52
+ var withoutWikilinks = value.replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g, '$2').replace(/\[\[([^\]]+)\]\]/g, '$1');
53
+ var withoutMarkdown = (0, _removemarkdown.default)(withoutWikilinks);
54
+ var withoutTables = withoutMarkdown.replace(/^\s*\|?[-\s|:]+\|\s*$/gm, '').replace(/\|/g, ' ');
55
+ return normalizeText(withoutTables);
56
+ }
48
57
  function listFiles(cfg, baseDir) {
49
58
  var relPaths = _fastglob.default.sync(cfg.scan.include, {
50
59
  cwd: baseDir
@@ -60,20 +69,16 @@ function listFiles(cfg, baseDir) {
60
69
  };
61
70
  });
62
71
  }
63
- // Value mapping: strings/numbers as-is; booleans -> 0/1; dates -> ISO
64
- // strings (lexicographic = chronological, no date lib); arrays/objects ->
65
- // JSON text. `null`/`undefined` map to SQL NULL.
66
72
  function mapValue(value) {
67
73
  if (value === null || value === undefined) return null;
68
74
  if (_instanceof(value, Date)) return value.toISOString();
69
75
  if (typeof value === 'boolean') return value ? 1 : 0;
70
76
  if (typeof value === 'string' || typeof value === 'number') return value;
71
- // arrays and plain objects
72
77
  return JSON.stringify(value);
73
78
  }
74
79
  function parseFile(file) {
75
80
  var raw = (0, _nodefs.readFileSync)(file.absPath, 'utf8');
76
- var data = (0, _graymatter.default)(raw).data;
81
+ var _matter = (0, _graymatter.default)(raw), data = _matter.data, content = _matter.content;
77
82
  var warnings = [];
78
83
  var mapped = {};
79
84
  var _iteratorNormalCompletion = true, _didIteratorError = false, _iteratorError = undefined;
@@ -105,7 +110,14 @@ function parseFile(file) {
105
110
  relPath: file.relPath,
106
111
  mtimeMs: file.mtimeMs,
107
112
  size: file.size,
108
- data: mapped
113
+ data: mapped,
114
+ search: {
115
+ // title/summary are plain YAML strings -- whitespace-collapse only;
116
+ // the prose gets the full markdown strip.
117
+ title: normalizeText(data.title),
118
+ summary: normalizeText(data.summary),
119
+ text: stripText(content)
120
+ }
109
121
  },
110
122
  warnings: warnings
111
123
  };
@@ -1 +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":["listFiles","parseFile","RESERVED_COLUMNS","Set","cfg","baseDir","relPaths","fastGlob","sync","scan","include","cwd","sort","map","relPath","absPath","join","st","statSync","mtimeMs","size","mapValue","value","undefined","Date","toISOString","JSON","stringify","file","raw","readFileSync","data","matter","warnings","mapped","Object","keys","key","has","push","doc"],"mappings":";;;;;;;;;;;QAyBgBA;eAAAA;;QAiCAC;eAAAA;;;sBA1DuB;wBAClB;+DACA;iEACF;;;;;;;;;;;;;;AAGnB,2EAA2E;AAC3E,sEAAsE;AACtE,gDAAgD;AAEhD,4EAA4E;AAC5E,4EAA4E;AAC5E,0EAA0E;AAC1E,8BAA8B;AAC9B,IAAMC,mBAAmB,IAAIC,IAAI;IAAC;IAAQ;IAAU;CAAQ;AAWrD,SAASH,UAAUI,GAAW,EAAEC,OAAe;IACpD,IAAMC,WAAWC,iBAAQ,CAACC,IAAI,CAACJ,IAAIK,IAAI,CAACC,OAAO,EAAE;QAAEC,KAAKN;IAAQ,GAAGO,IAAI;IACvE,OAAON,SAASO,GAAG,CAAC,SAACC;QACnB,IAAMC,UAAUC,IAAAA,cAAI,EAACX,SAASS;QAC9B,IAAMG,KAAKC,IAAAA,gBAAQ,EAACH;QACpB,OAAO;YAAED,SAAAA;YAASC,SAAAA;YAASI,SAASF,GAAGE,OAAO;YAAEC,MAAMH,GAAGG,IAAI;QAAC;IAChE;AACF;AAWA,sEAAsE;AACtE,0EAA0E;AAC1E,iDAAiD;AACjD,SAASC,SAASC,KAAc;IAC9B,IAAIA,UAAU,QAAQA,UAAUC,WAAW,OAAO;IAClD,IAAID,AAAK,YAALA,OAAiBE,OAAM,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;AAKO,SAASrB,UAAU2B,IAAc;IACtC,IAAMC,MAAMC,IAAAA,oBAAY,EAACF,KAAKb,OAAO,EAAE;IACvC,IAAM,AAAEgB,OAASC,IAAAA,mBAAM,EAACH,KAAhBE;IAER,IAAME,WAAqB,EAAE;IAC7B,IAAMC,SAAiD,CAAC;QAEnD,kCAAA,2BAAA;;QAAL,QAAK,YAAaC,OAAOC,IAAI,CAACL,0BAAzB,SAAA,6BAAA,QAAA,yBAAA,iCAAgC;YAAhC,IAAMM,MAAN;YACH,IAAInC,iBAAiBoC,GAAG,CAACD,MAAM;gBAC7BJ,SAASM,IAAI,CAAC,AAAC,YAAwDF,OAA7CT,KAAKd,OAAO,EAAC,kCAAoC,OAAJuB,KAAI;gBAC3E;YACF;YACAH,MAAM,CAACG,IAAI,GAAGhB,SAASU,IAAI,CAACM,IAAI;QAClC;;QANK;QAAA;;;iBAAA,6BAAA;gBAAA;;;gBAAA;sBAAA;;;;IAQL,OAAO;QACLG,KAAK;YAAE1B,SAASc,KAAKd,OAAO;YAAEK,SAASS,KAAKT,OAAO;YAAEC,MAAMQ,KAAKR,IAAI;YAAEW,MAAMG;QAAO;QACnFD,UAAAA;IACF;AACF"}
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 removeMarkdown from 'remove-markdown';\nimport type { Config } from './config.ts';\n\n// Filesystem -> rows. Pure data in, data + warnings out; db.ts does the SQL.\n\n// Reserved: colliding with these would clash with the `frontmatter` table's own columns or the `content` FTS5 table.\nconst RESERVED_COLUMNS = new Set(['path', '_mtime', '_size', 'content']);\n\nfunction normalizeText(value: unknown): string {\n if (value === null || value === undefined) return '';\n return String(value).replace(/\\s+/g, ' ').trim();\n}\n\n// Markdown is stripped at index time so snippets read as clean prose (also improves matching, e.g. `**bold**` indexes as `bold`).\n// remove-markdown doesn't cover Obsidian wikilinks or table scaffolding, so those are handled first/after.\nfunction stripText(value: string): string {\n const withoutWikilinks = value.replace(/\\[\\[([^\\]|]+)\\|([^\\]]+)\\]\\]/g, '$2').replace(/\\[\\[([^\\]]+)\\]\\]/g, '$1');\n const withoutMarkdown = removeMarkdown(withoutWikilinks);\n const withoutTables = withoutMarkdown.replace(/^\\s*\\|?[-\\s|:]+\\|\\s*$/gm, '').replace(/\\|/g, ' ');\n return normalizeText(withoutTables);\n}\n\nexport interface FileStat {\n relPath: string;\n absPath: string;\n mtimeMs: number;\n size: number;\n}\n\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 data: Record<string, string | number | null>;\n // title/summary are duplicated from frontmatter so bm25() can weight them above the body text.\n search: { title: string; summary: string; text: string };\n}\n\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 return JSON.stringify(value);\n}\n\nexport function parseFile(file: FileStat): { doc: ParsedDoc; warnings: string[] } {\n const raw = readFileSync(file.absPath, 'utf8');\n const { data, content } = 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: {\n relPath: file.relPath,\n mtimeMs: file.mtimeMs,\n size: file.size,\n data: mapped,\n search: {\n // title/summary are plain YAML strings -- whitespace-collapse only;\n // the prose gets the full markdown strip.\n title: normalizeText(data.title),\n summary: normalizeText(data.summary),\n text: stripText(content),\n },\n },\n warnings,\n };\n}\n"],"names":["listFiles","parseFile","RESERVED_COLUMNS","Set","normalizeText","value","undefined","String","replace","trim","stripText","withoutWikilinks","withoutMarkdown","removeMarkdown","withoutTables","cfg","baseDir","relPaths","fastGlob","sync","scan","include","cwd","sort","map","relPath","absPath","join","st","statSync","mtimeMs","size","mapValue","Date","toISOString","JSON","stringify","file","raw","readFileSync","matter","data","content","warnings","mapped","Object","keys","key","has","push","doc","search","title","summary","text"],"mappings":";;;;;;;;;;;QAiCgBA;eAAAA;;QA0BAC;eAAAA;;;sBA3DuB;wBAClB;+DACA;iEACF;qEACQ;;;;;;;;;;;;;;AAG3B,6EAA6E;AAE7E,qHAAqH;AACrH,IAAMC,mBAAmB,IAAIC,IAAI;IAAC;IAAQ;IAAU;IAAS;CAAU;AAEvE,SAASC,cAAcC,KAAc;IACnC,IAAIA,UAAU,QAAQA,UAAUC,WAAW,OAAO;IAClD,OAAOC,OAAOF,OAAOG,OAAO,CAAC,QAAQ,KAAKC,IAAI;AAChD;AAEA,kIAAkI;AAClI,2GAA2G;AAC3G,SAASC,UAAUL,KAAa;IAC9B,IAAMM,mBAAmBN,MAAMG,OAAO,CAAC,gCAAgC,MAAMA,OAAO,CAAC,qBAAqB;IAC1G,IAAMI,kBAAkBC,IAAAA,uBAAc,EAACF;IACvC,IAAMG,gBAAgBF,gBAAgBJ,OAAO,CAAC,2BAA2B,IAAIA,OAAO,CAAC,OAAO;IAC5F,OAAOJ,cAAcU;AACvB;AASO,SAASd,UAAUe,GAAW,EAAEC,OAAe;IACpD,IAAMC,WAAWC,iBAAQ,CAACC,IAAI,CAACJ,IAAIK,IAAI,CAACC,OAAO,EAAE;QAAEC,KAAKN;IAAQ,GAAGO,IAAI;IACvE,OAAON,SAASO,GAAG,CAAC,SAACC;QACnB,IAAMC,UAAUC,IAAAA,cAAI,EAACX,SAASS;QAC9B,IAAMG,KAAKC,IAAAA,gBAAQ,EAACH;QACpB,OAAO;YAAED,SAAAA;YAASC,SAAAA;YAASI,SAASF,GAAGE,OAAO;YAAEC,MAAMH,GAAGG,IAAI;QAAC;IAChE;AACF;AAWA,SAASC,SAAS3B,KAAc;IAC9B,IAAIA,UAAU,QAAQA,UAAUC,WAAW,OAAO;IAClD,IAAID,AAAK,YAALA,OAAiB4B,OAAM,OAAO5B,MAAM6B,WAAW;IACnD,IAAI,OAAO7B,UAAU,WAAW,OAAOA,QAAQ,IAAI;IACnD,IAAI,OAAOA,UAAU,YAAY,OAAOA,UAAU,UAAU,OAAOA;IACnE,OAAO8B,KAAKC,SAAS,CAAC/B;AACxB;AAEO,SAASJ,UAAUoC,IAAc;IACtC,IAAMC,MAAMC,IAAAA,oBAAY,EAACF,KAAKX,OAAO,EAAE;IACvC,IAA0Bc,UAAAA,IAAAA,mBAAM,EAACF,MAAzBG,OAAkBD,QAAlBC,MAAMC,UAAYF,QAAZE;IAEd,IAAMC,WAAqB,EAAE;IAC7B,IAAMC,SAAiD,CAAC;QAEnD,kCAAA,2BAAA;;QAAL,QAAK,YAAaC,OAAOC,IAAI,CAACL,0BAAzB,SAAA,6BAAA,QAAA,yBAAA,iCAAgC;YAAhC,IAAMM,MAAN;YACH,IAAI7C,iBAAiB8C,GAAG,CAACD,MAAM;gBAC7BJ,SAASM,IAAI,CAAC,AAAC,YAAwDF,OAA7CV,KAAKZ,OAAO,EAAC,kCAAoC,OAAJsB,KAAI;gBAC3E;YACF;YACAH,MAAM,CAACG,IAAI,GAAGf,SAASS,IAAI,CAACM,IAAI;QAClC;;QANK;QAAA;;;iBAAA,6BAAA;gBAAA;;;gBAAA;sBAAA;;;;IAQL,OAAO;QACLG,KAAK;YACHzB,SAASY,KAAKZ,OAAO;YACrBK,SAASO,KAAKP,OAAO;YACrBC,MAAMM,KAAKN,IAAI;YACfU,MAAMG;YACNO,QAAQ;gBACN,oEAAoE;gBACpE,0CAA0C;gBAC1CC,OAAOhD,cAAcqC,KAAKW,KAAK;gBAC/BC,SAASjD,cAAcqC,KAAKY,OAAO;gBACnCC,MAAM5C,UAAUgC;YAClB;QACF;QACAC,UAAAA;IACF;AACF"}
package/dist/cjs/watch.js CHANGED
@@ -140,12 +140,7 @@ function _ts_generator(thisArg, body) {
140
140
  };
141
141
  }
142
142
  }
143
- // Watch is a cache pre-warmer, not a correctness mechanism: every query
144
- // `open()` reconciles against the filesystem regardless, so a missed or
145
- // coalesced fs event can never change a query result -- it only moves one
146
- // file's parse from ahead-of-time to query-time. That's why any event, of
147
- // any kind, triggers the exact same response: a debounced full reconcile.
148
- // Per-file event handling would be complexity with no correctness payoff.
143
+ // Watch is a cache pre-warmer, not a correctness mechanism: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.
149
144
  var DEBOUNCE_MS = 200;
150
145
  var HEARTBEAT_INTERVAL_MS = 5000;
151
146
  var STALE_HEARTBEAT_MS = 15000;
@@ -205,9 +200,7 @@ function runWatch(_0) {
205
200
  }
206
201
  }, DEBOUNCE_MS);
207
202
  };
208
- // Ignore events from our own state dir: the heartbeat writes cache.db
209
- // every few seconds, and without this filter each write would schedule a
210
- // (harmless but pointless) reconcile forever — a perpetual self-trigger.
203
+ // Ignore our own state dir, or the heartbeat write would retrigger itself forever.
211
204
  watcher = (0, _nodefs.watch)(baseDir, {
212
205
  recursive: true
213
206
  }, function(_event, filename) {
@@ -1 +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":["runWatch","DEBOUNCE_MS","HEARTBEAT_INTERVAL_MS","STALE_HEARTBEAT_MS","cfg","opts","onEvent","open","db","dbPath","initialWarnings","initialParsed","baseDir","existingHeartbeat","age","touchHeartbeat","debounceTimer","scheduleReconcile","watcher","heartbeatTimer","warnings","parsed","getMeta","force","Date","now","parse","close","SenseError","Math","round","type","length","total","docCount","setMeta","toISOString","String","process","pid","clearTimeout","setTimeout","reconcile","err","message","fsWatch","recursive","_event","filename","startsWith","STATE_DIR","setInterval","Promise","resolveShutdown","cleanExit","clearInterval","once"],"mappings":"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"}
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: open() always reconciles anyway, so any fs event just triggers a debounced full reconcile.\nconst DEBOUNCE_MS = 200;\nconst HEARTBEAT_INTERVAL_MS = 5000;\nconst STALE_HEARTBEAT_MS = 15000;\n\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. Throws WATCH_ACTIVE if another 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 our own state dir, or the heartbeat write would retrigger itself forever.\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":["runWatch","DEBOUNCE_MS","HEARTBEAT_INTERVAL_MS","STALE_HEARTBEAT_MS","cfg","opts","onEvent","open","db","dbPath","initialWarnings","initialParsed","baseDir","existingHeartbeat","age","touchHeartbeat","debounceTimer","scheduleReconcile","watcher","heartbeatTimer","warnings","parsed","getMeta","force","Date","now","parse","close","SenseError","Math","round","type","length","total","docCount","setMeta","toISOString","String","process","pid","clearTimeout","setTimeout","reconcile","err","message","fsWatch","recursive","_event","filename","startsWith","STATE_DIR","setInterval","Promise","resolveShutdown","cleanExit","clearInterval","once"],"mappings":";;;;+BAmBsBA;;;eAAAA;;;sBAnBW;wBAEP;oBACkC;wBACjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAE3B,uJAAuJ;AACvJ,IAAMC,cAAc;AACpB,IAAMC,wBAAwB;AAC9B,IAAMC,qBAAqB;AAUpB,SAAeH;wCAASI,GAAmB;YAAEC,MAClCA,eAAVC,SACmEC,OAAjEC,IAAIC,QAAkBC,iBAAyBC,eACjDC,SAEAC,mBAEEC,KAYFC,gBAMFC,eACEC,mBAcAC,SAIAC;;;YA5C4Cd,OAAAA,oEAAqB,CAAC;YAClEC,WAAUD,gBAAAA,KAAKC,OAAO,cAAZD,2BAAAA,gBAAiB,YAAO;YACiCE,QAAAA,IAAAA,UAAI,EAACH,MAAtEI,KAAiED,MAAjEC,IAAIC,SAA6DF,MAA7DE,QAAkBC,kBAA2CH,MAArDa,UAAmCT,gBAAkBJ,MAA1Bc;YACzCT,UAAUR,IAAIQ,OAAO;YAErBC,oBAAoBS,IAAAA,aAAO,EAACd,IAAI;YACtC,IAAIK,qBAAqB,CAACR,KAAKkB,KAAK,EAAE;gBAC9BT,MAAMU,KAAKC,GAAG,KAAKD,KAAKE,KAAK,CAACb;gBACpC,IAAIC,OAAO,KAAKA,MAAMX,oBAAoB;oBACxCK,GAAGmB,KAAK;oBACR,MAAM,IAAIC,oBAAU,CAAC,gBAAgB,AAAC,6CAAmE,OAAvBC,KAAKC,KAAK,CAAChB,MAAM,OAAM;gBAC3G;YACF;YAEAR,QAAQ;gBAAEyB,MAAM;gBAAWnB,SAAAA;gBAASH,QAAAA;YAAO;YAC3C,IAAIC,gBAAgBsB,MAAM,GAAG,KAAKrB,gBAAgB,GAAG;gBACnDL,QAAQ;oBAAEyB,MAAM;oBAAcV,QAAQV;oBAAesB,OAAOC,IAAAA,cAAQ,EAAC1B;oBAAKY,UAAUV;gBAAgB;YACtG;YAEMK,iBAAiB;gBACrBoB,IAAAA,aAAO,EAAC3B,IAAI,mBAAmB,IAAIgB,OAAOY,WAAW;gBACrDD,IAAAA,aAAO,EAAC3B,IAAI,aAAa6B,OAAOC,QAAQC,GAAG;YAC7C;YACAxB;YAEIC,gBAAuC;YACrCC,oBAAoB;gBACxB,IAAID,eAAewB,aAAaxB;gBAChCA,gBAAgByB,WAAW;oBACzBzB,gBAAgB;oBAChB,IAAI;wBACF,IAA6B0B,aAAAA,IAAAA,eAAS,EAAClC,IAAIJ,KAAKQ,UAAxCS,SAAqBqB,WAArBrB,QAAQD,WAAasB,WAAbtB;wBAChBd,QAAQ;4BAAEyB,MAAM;4BAAcV,QAAAA;4BAAQY,OAAOC,IAAAA,cAAQ,EAAC1B;4BAAKY,UAAAA;wBAAS;oBACtE,EAAE,OAAOuB,KAAK;wBACZrC,QAAQ;4BAAEyB,MAAM;4BAAmBa,SAAS,AAACD,IAAcC,OAAO;wBAAC;oBACrE;gBACF,GAAG3C;YACL;YAEA,mFAAmF;YAC7EiB,UAAU2B,IAAAA,aAAO,EAACjC,SAAS;gBAAEkC,WAAW;YAAK,GAAG,SAACC,QAAQC;gBAC7D,IAAI,OAAOA,aAAa,YAAYA,SAASC,UAAU,CAACC,mBAAS,GAAG;gBACpEjC;YACF;YACME,iBAAiBgC,YAAYpC,gBAAgBb;YAEnD;;gBAAO,IAAIkD,QAAc,SAACC;oBACxB,IAAMC,YAAY;wBAChBC,cAAcpC;wBACd,IAAIH,eAAewB,aAAaxB;wBAChCE,QAAQS,KAAK;wBACbQ,IAAAA,aAAO,EAAC3B,IAAI,mBAAmB;wBAC/B2B,IAAAA,aAAO,EAAC3B,IAAI,aAAa;wBACzBA,GAAGmB,KAAK;wBACR0B;oBACF;oBACAf,QAAQkB,IAAI,CAAC,UAAUF;oBACvBhB,QAAQkB,IAAI,CAAC,WAAWF;gBAC1B;;;IACF"}
package/dist/esm/cli.js CHANGED
@@ -43,10 +43,7 @@ function parseCliArgs(argv, name) {
43
43
  function printWarnings(warnings) {
44
44
  for (const w of warnings)console.warn(w);
45
45
  }
46
- // Shared tail of both the named-query and ad-hoc `query` paths. An unbound
47
- // `?` silently binds NULL and returns misleading empty results — fail
48
- // loudly instead. (Naive count; queries putting '?' in string literals
49
- // would miscount, which none of ours do.)
46
+ // An unbound `?` silently binds NULL, so mismatched param counts fail loudly instead.
50
47
  function runSql(cfg, sql, params, format, label) {
51
48
  var _sql_match;
52
49
  const placeholderCount = ((_sql_match = sql.match(/\?/g)) !== null && _sql_match !== void 0 ? _sql_match : []).length;
@@ -75,26 +72,20 @@ function logWatchEvent(event) {
75
72
  }
76
73
  console.error(`sense watch: reconcile error: ${event.message}`);
77
74
  }
78
- // Everything below that can throw (bad JSON, a config `version` newer than
79
- // this build supports, another watcher's fresh heartbeat, a SQLite error)
80
- // is caught by the top-level handler below and reported as exit 1 with the
81
- // error's message verbatim. Usage errors (missing/unknown query name, wrong
82
- // parameter count) exit(2) directly instead of throwing.
75
+ // Thrown errors -> exit 1 with the message verbatim; usage errors exit(2) directly.
83
76
  export default async function cli(argv, name) {
84
77
  const { values, positionals } = parseCliArgs(argv, name);
85
78
  if (values.help) {
86
79
  console.log(usage(name));
87
80
  process.exit(0);
88
81
  }
89
- // cli.ts's only config knowledge is the --config flag; discovery, parsing,
90
- // and version-gating all live in config.ts.
91
82
  const resolveConfig = ()=>loadConfig(values.config);
92
83
  try {
93
84
  const [first, ...rest] = positionals;
94
85
  if (first === 'init') {
95
86
  const configPath = initConfig(process.cwd());
96
87
  console.log(`created ${configPath}`);
97
- console.log('edit the queries to fit your tree, then: sense --list');
88
+ console.log('query away: sense query "SELECT path FROM frontmatter LIMIT 10"');
98
89
  process.exit(0);
99
90
  }
100
91
  if (first === 'watch') {
@@ -135,8 +126,6 @@ export default async function cli(argv, name) {
135
126
  process.exit(0);
136
127
  }
137
128
  const format = values.format === 'json' ? 'json' : 'table';
138
- // Ad-hoc SQL without touching the config -- for one-off questions;
139
- // save a query into sense.config.json only when it'll be reused.
140
129
  if (first === 'query') {
141
130
  const [sql, ...params] = rest;
142
131
  if (!sql) {
@@ -163,8 +152,6 @@ export default async function cli(argv, name) {
163
152
  }
164
153
  runSql(cfg, sql, params, format, `query "${name_}"`);
165
154
  } catch (err) {
166
- // SQLite's own error message, a bad config, an unsupported config
167
- // version, or an already-active watcher — printed verbatim.
168
155
  console.error(err.message);
169
156
  process.exit(1);
170
157
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/cli.ts"],"sourcesContent":["import { parseArgs } from 'node:util';\nimport type { ResolvedConfig } from './config.ts';\nimport { initConfig, loadConfig } from './config.ts';\nimport { docCount, getMeta, open, rebuild } from './db.ts';\nimport type { Row } from './output.ts';\nimport { printRows } from './output.ts';\nimport type { WatchEvent } from './watch.ts';\nimport { runWatch } from './watch.ts';\n\nfunction usage(name: string): string {\n return `usage: ${name} <name> [params...] [--format table|json] [--config path]\\n` + ` ${name} query \"<sql>\" [params...]\\n` + ` ${name} --list\\n` + ` ${name} init\\n` + ` ${name} watch [--force]\\n` + ` ${name} status\\n` + ` ${name} rebuild`;\n}\n\nfunction parseCliArgs(argv: string[], name: string) {\n try {\n return parseArgs({\n args: argv,\n options: {\n format: { type: 'string', default: 'table' },\n config: { type: 'string' },\n list: { type: 'boolean', default: false },\n force: { type: 'boolean', default: false },\n help: { type: 'boolean', default: false, short: 'h' },\n },\n allowPositionals: true,\n });\n } catch (err) {\n console.error((err as Error).message);\n console.error(usage(name));\n process.exit(2);\n }\n}\n\nfunction printWarnings(warnings: string[]): void {\n for (const w of warnings) console.warn(w);\n}\n\n// Shared tail of both the named-query and ad-hoc `query` paths. An unbound\n// `?` silently binds NULL and returns misleading empty results — fail\n// loudly instead. (Naive count; queries putting '?' in string literals\n// would miscount, which none of ours do.)\nfunction runSql(cfg: ResolvedConfig, sql: string, params: string[], format: 'table' | 'json', label: string): void {\n const placeholderCount = (sql.match(/\\?/g) ?? []).length;\n if (params.length !== placeholderCount) {\n console.error(`${label} expects ${placeholderCount} parameter(s), got ${params.length}`);\n process.exit(2);\n }\n const { db, warnings } = open(cfg);\n printWarnings(warnings);\n const rows = db.prepare(sql).all(...params) as Row[];\n printRows(rows, format);\n db.close();\n}\n\nfunction logWatchEvent(event: WatchEvent): void {\n if (event.type === 'started') {\n console.log(`sense watch: watching ${event.baseDir}`);\n console.log(`sense watch: db ${event.dbPath}`);\n return;\n }\n if (event.type === 'reconciled') {\n printWarnings(event.warnings);\n if (event.parsed > 0) {\n console.log(`sense watch: reconciled, ${event.parsed} file(s) reparsed (${event.total} total)`);\n }\n return;\n }\n console.error(`sense watch: reconcile error: ${event.message}`);\n}\n\n// Everything below that can throw (bad JSON, a config `version` newer than\n// this build supports, another watcher's fresh heartbeat, a SQLite error)\n// is caught by the top-level handler below and reported as exit 1 with the\n// error's message verbatim. Usage errors (missing/unknown query name, wrong\n// parameter count) exit(2) directly instead of throwing.\nexport default async function cli(argv: string[], name: string): Promise<void> {\n const { values, positionals } = parseCliArgs(argv, name);\n\n if (values.help) {\n console.log(usage(name));\n process.exit(0);\n }\n\n // cli.ts's only config knowledge is the --config flag; discovery, parsing,\n // and version-gating all live in config.ts.\n const resolveConfig = () => loadConfig(values.config);\n\n try {\n const [first, ...rest] = positionals;\n\n if (first === 'init') {\n const configPath = initConfig(process.cwd());\n console.log(`created ${configPath}`);\n console.log('edit the queries to fit your tree, then: sense --list');\n process.exit(0);\n }\n\n if (first === 'watch') {\n const cfg = resolveConfig();\n await runWatch(cfg, { force: values.force, onEvent: logWatchEvent });\n process.exit(0);\n }\n\n if (first === 'status') {\n const cfg = resolveConfig();\n const { db, dbPath, warnings } = open(cfg);\n printWarnings(warnings);\n console.log(`db: ${dbPath}`);\n console.log(`docs: ${docCount(db)}`);\n const heartbeat = getMeta(db, 'watch_heartbeat');\n if (heartbeat) {\n const ageSec = Math.round((Date.now() - Date.parse(heartbeat)) / 1000);\n console.log(`watcher: last heartbeat ${ageSec}s ago`);\n } else {\n console.log('watcher: no watcher');\n }\n db.close();\n process.exit(0);\n }\n\n if (first === 'rebuild') {\n const cfg = resolveConfig();\n const result = rebuild(cfg);\n printWarnings(result.warnings);\n console.log(`rebuilt: ${docCount(result.db)} docs`);\n result.db.close();\n process.exit(0);\n }\n\n if (values.list) {\n const cfg = resolveConfig();\n for (const queryName of Object.keys(cfg.queries).sort()) console.log(queryName);\n process.exit(0);\n }\n\n const format = values.format === 'json' ? 'json' : 'table';\n\n // Ad-hoc SQL without touching the config -- for one-off questions;\n // save a query into sense.config.json only when it'll be reused.\n if (first === 'query') {\n const [sql, ...params] = rest;\n if (!sql) {\n console.error(`usage: ${name} query \"<sql>\" [params...]`);\n process.exit(2);\n }\n runSql(resolveConfig(), sql, params, format, 'ad-hoc query');\n return;\n }\n\n const [name_, ...params] = [first, ...rest];\n\n if (!name_) {\n console.error(usage(name));\n process.exit(2);\n }\n\n const cfg = resolveConfig();\n\n const sql = cfg.queries[name_];\n if (!sql) {\n console.error(`unknown query: \"${name_}\"`);\n console.error(`valid queries: ${Object.keys(cfg.queries).sort().join(', ')}`);\n process.exit(2);\n }\n\n runSql(cfg, sql, params, format, `query \"${name_}\"`);\n } catch (err) {\n // SQLite's own error message, a bad config, an unsupported config\n // version, or an already-active watcher — printed verbatim.\n console.error((err as Error).message);\n process.exit(1);\n }\n}\n"],"names":["parseArgs","initConfig","loadConfig","docCount","getMeta","open","rebuild","printRows","runWatch","usage","name","parseCliArgs","argv","args","options","format","type","default","config","list","force","help","short","allowPositionals","err","console","error","message","process","exit","printWarnings","warnings","w","warn","runSql","cfg","sql","params","label","placeholderCount","match","length","db","rows","prepare","all","close","logWatchEvent","event","log","baseDir","dbPath","parsed","total","cli","values","positionals","resolveConfig","first","rest","configPath","cwd","onEvent","heartbeat","ageSec","Math","round","Date","now","parse","result","queryName","Object","keys","queries","sort","name_","join"],"mappings":"AAAA,SAASA,SAAS,QAAQ,YAAY;AAEtC,SAASC,UAAU,EAAEC,UAAU,QAAQ,cAAc;AACrD,SAASC,QAAQ,EAAEC,OAAO,EAAEC,IAAI,EAAEC,OAAO,QAAQ,UAAU;AAE3D,SAASC,SAAS,QAAQ,cAAc;AAExC,SAASC,QAAQ,QAAQ,aAAa;AAEtC,SAASC,MAAMC,IAAY;IACzB,OAAO,CAAC,OAAO,EAAEA,KAAK,2DAA2D,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,4BAA4B,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,SAAS,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,OAAO,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,kBAAkB,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,SAAS,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,QAAQ,CAAC;AACrR;AAEA,SAASC,aAAaC,IAAc,EAAEF,IAAY;IAChD,IAAI;QACF,OAAOV,UAAU;YACfa,MAAMD;YACNE,SAAS;gBACPC,QAAQ;oBAAEC,MAAM;oBAAUC,SAAS;gBAAQ;gBAC3CC,QAAQ;oBAAEF,MAAM;gBAAS;gBACzBG,MAAM;oBAAEH,MAAM;oBAAWC,SAAS;gBAAM;gBACxCG,OAAO;oBAAEJ,MAAM;oBAAWC,SAAS;gBAAM;gBACzCI,MAAM;oBAAEL,MAAM;oBAAWC,SAAS;oBAAOK,OAAO;gBAAI;YACtD;YACAC,kBAAkB;QACpB;IACF,EAAE,OAAOC,KAAK;QACZC,QAAQC,KAAK,CAAC,AAACF,IAAcG,OAAO;QACpCF,QAAQC,KAAK,CAACjB,MAAMC;QACpBkB,QAAQC,IAAI,CAAC;IACf;AACF;AAEA,SAASC,cAAcC,QAAkB;IACvC,KAAK,MAAMC,KAAKD,SAAUN,QAAQQ,IAAI,CAACD;AACzC;AAEA,2EAA2E;AAC3E,sEAAsE;AACtE,uEAAuE;AACvE,0CAA0C;AAC1C,SAASE,OAAOC,GAAmB,EAAEC,GAAW,EAAEC,MAAgB,EAAEtB,MAAwB,EAAEuB,KAAa;QAC/EF;IAA1B,MAAMG,mBAAmB,EAACH,aAAAA,IAAII,KAAK,CAAC,oBAAVJ,wBAAAA,aAAoB,EAAE,EAAEK,MAAM;IACxD,IAAIJ,OAAOI,MAAM,KAAKF,kBAAkB;QACtCd,QAAQC,KAAK,CAAC,GAAGY,MAAM,SAAS,EAAEC,iBAAiB,mBAAmB,EAAEF,OAAOI,MAAM,EAAE;QACvFb,QAAQC,IAAI,CAAC;IACf;IACA,MAAM,EAAEa,EAAE,EAAEX,QAAQ,EAAE,GAAG1B,KAAK8B;IAC9BL,cAAcC;IACd,MAAMY,OAAOD,GAAGE,OAAO,CAACR,KAAKS,GAAG,IAAIR;IACpC9B,UAAUoC,MAAM5B;IAChB2B,GAAGI,KAAK;AACV;AAEA,SAASC,cAAcC,KAAiB;IACtC,IAAIA,MAAMhC,IAAI,KAAK,WAAW;QAC5BS,QAAQwB,GAAG,CAAC,CAAC,sBAAsB,EAAED,MAAME,OAAO,EAAE;QACpDzB,QAAQwB,GAAG,CAAC,CAAC,gBAAgB,EAAED,MAAMG,MAAM,EAAE;QAC7C;IACF;IACA,IAAIH,MAAMhC,IAAI,KAAK,cAAc;QAC/Bc,cAAckB,MAAMjB,QAAQ;QAC5B,IAAIiB,MAAMI,MAAM,GAAG,GAAG;YACpB3B,QAAQwB,GAAG,CAAC,CAAC,yBAAyB,EAAED,MAAMI,MAAM,CAAC,mBAAmB,EAAEJ,MAAMK,KAAK,CAAC,OAAO,CAAC;QAChG;QACA;IACF;IACA5B,QAAQC,KAAK,CAAC,CAAC,8BAA8B,EAAEsB,MAAMrB,OAAO,EAAE;AAChE;AAEA,2EAA2E;AAC3E,0EAA0E;AAC1E,2EAA2E;AAC3E,4EAA4E;AAC5E,yDAAyD;AACzD,eAAe,eAAe2B,IAAI1C,IAAc,EAAEF,IAAY;IAC5D,MAAM,EAAE6C,MAAM,EAAEC,WAAW,EAAE,GAAG7C,aAAaC,MAAMF;IAEnD,IAAI6C,OAAOlC,IAAI,EAAE;QACfI,QAAQwB,GAAG,CAACxC,MAAMC;QAClBkB,QAAQC,IAAI,CAAC;IACf;IAEA,2EAA2E;IAC3E,4CAA4C;IAC5C,MAAM4B,gBAAgB,IAAMvD,WAAWqD,OAAOrC,MAAM;IAEpD,IAAI;QACF,MAAM,CAACwC,OAAO,GAAGC,KAAK,GAAGH;QAEzB,IAAIE,UAAU,QAAQ;YACpB,MAAME,aAAa3D,WAAW2B,QAAQiC,GAAG;YACzCpC,QAAQwB,GAAG,CAAC,CAAC,QAAQ,EAAEW,YAAY;YACnCnC,QAAQwB,GAAG,CAAC;YACZrB,QAAQC,IAAI,CAAC;QACf;QAEA,IAAI6B,UAAU,SAAS;YACrB,MAAMvB,MAAMsB;YACZ,MAAMjD,SAAS2B,KAAK;gBAAEf,OAAOmC,OAAOnC,KAAK;gBAAE0C,SAASf;YAAc;YAClEnB,QAAQC,IAAI,CAAC;QACf;QAEA,IAAI6B,UAAU,UAAU;YACtB,MAAMvB,MAAMsB;YACZ,MAAM,EAAEf,EAAE,EAAES,MAAM,EAAEpB,QAAQ,EAAE,GAAG1B,KAAK8B;YACtCL,cAAcC;YACdN,QAAQwB,GAAG,CAAC,CAAC,IAAI,EAAEE,QAAQ;YAC3B1B,QAAQwB,GAAG,CAAC,CAAC,MAAM,EAAE9C,SAASuC,KAAK;YACnC,MAAMqB,YAAY3D,QAAQsC,IAAI;YAC9B,IAAIqB,WAAW;gBACb,MAAMC,SAASC,KAAKC,KAAK,CAAC,AAACC,CAAAA,KAAKC,GAAG,KAAKD,KAAKE,KAAK,CAACN,UAAS,IAAK;gBACjEtC,QAAQwB,GAAG,CAAC,CAAC,wBAAwB,EAAEe,OAAO,KAAK,CAAC;YACtD,OAAO;gBACLvC,QAAQwB,GAAG,CAAC;YACd;YACAP,GAAGI,KAAK;YACRlB,QAAQC,IAAI,CAAC;QACf;QAEA,IAAI6B,UAAU,WAAW;YACvB,MAAMvB,MAAMsB;YACZ,MAAMa,SAAShE,QAAQ6B;YACvBL,cAAcwC,OAAOvC,QAAQ;YAC7BN,QAAQwB,GAAG,CAAC,CAAC,SAAS,EAAE9C,SAASmE,OAAO5B,EAAE,EAAE,KAAK,CAAC;YAClD4B,OAAO5B,EAAE,CAACI,KAAK;YACflB,QAAQC,IAAI,CAAC;QACf;QAEA,IAAI0B,OAAOpC,IAAI,EAAE;YACf,MAAMgB,MAAMsB;YACZ,KAAK,MAAMc,aAAaC,OAAOC,IAAI,CAACtC,IAAIuC,OAAO,EAAEC,IAAI,GAAIlD,QAAQwB,GAAG,CAACsB;YACrE3C,QAAQC,IAAI,CAAC;QACf;QAEA,MAAMd,SAASwC,OAAOxC,MAAM,KAAK,SAAS,SAAS;QAEnD,mEAAmE;QACnE,iEAAiE;QACjE,IAAI2C,UAAU,SAAS;YACrB,MAAM,CAACtB,KAAK,GAAGC,OAAO,GAAGsB;YACzB,IAAI,CAACvB,KAAK;gBACRX,QAAQC,KAAK,CAAC,CAAC,OAAO,EAAEhB,KAAK,0BAA0B,CAAC;gBACxDkB,QAAQC,IAAI,CAAC;YACf;YACAK,OAAOuB,iBAAiBrB,KAAKC,QAAQtB,QAAQ;YAC7C;QACF;QAEA,MAAM,CAAC6D,OAAO,GAAGvC,OAAO,GAAG;YAACqB;eAAUC;SAAK;QAE3C,IAAI,CAACiB,OAAO;YACVnD,QAAQC,KAAK,CAACjB,MAAMC;YACpBkB,QAAQC,IAAI,CAAC;QACf;QAEA,MAAMM,MAAMsB;QAEZ,MAAMrB,MAAMD,IAAIuC,OAAO,CAACE,MAAM;QAC9B,IAAI,CAACxC,KAAK;YACRX,QAAQC,KAAK,CAAC,CAAC,gBAAgB,EAAEkD,MAAM,CAAC,CAAC;YACzCnD,QAAQC,KAAK,CAAC,CAAC,eAAe,EAAE8C,OAAOC,IAAI,CAACtC,IAAIuC,OAAO,EAAEC,IAAI,GAAGE,IAAI,CAAC,OAAO;YAC5EjD,QAAQC,IAAI,CAAC;QACf;QAEAK,OAAOC,KAAKC,KAAKC,QAAQtB,QAAQ,CAAC,OAAO,EAAE6D,MAAM,CAAC,CAAC;IACrD,EAAE,OAAOpD,KAAK;QACZ,kEAAkE;QAClE,4DAA4D;QAC5DC,QAAQC,KAAK,CAAC,AAACF,IAAcG,OAAO;QACpCC,QAAQC,IAAI,CAAC;IACf;AACF"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/cli.ts"],"sourcesContent":["import { parseArgs } from 'node:util';\nimport type { ResolvedConfig } from './config.ts';\nimport { initConfig, loadConfig } from './config.ts';\nimport { docCount, getMeta, open, rebuild } from './db.ts';\nimport type { Row } from './output.ts';\nimport { printRows } from './output.ts';\nimport type { WatchEvent } from './watch.ts';\nimport { runWatch } from './watch.ts';\n\nfunction usage(name: string): string {\n return `usage: ${name} <name> [params...] [--format table|json] [--config path]\\n` + ` ${name} query \"<sql>\" [params...]\\n` + ` ${name} --list\\n` + ` ${name} init\\n` + ` ${name} watch [--force]\\n` + ` ${name} status\\n` + ` ${name} rebuild`;\n}\n\nfunction parseCliArgs(argv: string[], name: string) {\n try {\n return parseArgs({\n args: argv,\n options: {\n format: { type: 'string', default: 'table' },\n config: { type: 'string' },\n list: { type: 'boolean', default: false },\n force: { type: 'boolean', default: false },\n help: { type: 'boolean', default: false, short: 'h' },\n },\n allowPositionals: true,\n });\n } catch (err) {\n console.error((err as Error).message);\n console.error(usage(name));\n process.exit(2);\n }\n}\n\nfunction printWarnings(warnings: string[]): void {\n for (const w of warnings) console.warn(w);\n}\n\n// An unbound `?` silently binds NULL, so mismatched param counts fail loudly instead.\nfunction runSql(cfg: ResolvedConfig, sql: string, params: string[], format: 'table' | 'json', label: string): void {\n const placeholderCount = (sql.match(/\\?/g) ?? []).length;\n if (params.length !== placeholderCount) {\n console.error(`${label} expects ${placeholderCount} parameter(s), got ${params.length}`);\n process.exit(2);\n }\n const { db, warnings } = open(cfg);\n printWarnings(warnings);\n const rows = db.prepare(sql).all(...params) as Row[];\n printRows(rows, format);\n db.close();\n}\n\nfunction logWatchEvent(event: WatchEvent): void {\n if (event.type === 'started') {\n console.log(`sense watch: watching ${event.baseDir}`);\n console.log(`sense watch: db ${event.dbPath}`);\n return;\n }\n if (event.type === 'reconciled') {\n printWarnings(event.warnings);\n if (event.parsed > 0) {\n console.log(`sense watch: reconciled, ${event.parsed} file(s) reparsed (${event.total} total)`);\n }\n return;\n }\n console.error(`sense watch: reconcile error: ${event.message}`);\n}\n\n// Thrown errors -> exit 1 with the message verbatim; usage errors exit(2) directly.\nexport default async function cli(argv: string[], name: string): Promise<void> {\n const { values, positionals } = parseCliArgs(argv, name);\n\n if (values.help) {\n console.log(usage(name));\n process.exit(0);\n }\n\n const resolveConfig = () => loadConfig(values.config);\n\n try {\n const [first, ...rest] = positionals;\n\n if (first === 'init') {\n const configPath = initConfig(process.cwd());\n console.log(`created ${configPath}`);\n console.log('query away: sense query \"SELECT path FROM frontmatter LIMIT 10\"');\n process.exit(0);\n }\n\n if (first === 'watch') {\n const cfg = resolveConfig();\n await runWatch(cfg, { force: values.force, onEvent: logWatchEvent });\n process.exit(0);\n }\n\n if (first === 'status') {\n const cfg = resolveConfig();\n const { db, dbPath, warnings } = open(cfg);\n printWarnings(warnings);\n console.log(`db: ${dbPath}`);\n console.log(`docs: ${docCount(db)}`);\n const heartbeat = getMeta(db, 'watch_heartbeat');\n if (heartbeat) {\n const ageSec = Math.round((Date.now() - Date.parse(heartbeat)) / 1000);\n console.log(`watcher: last heartbeat ${ageSec}s ago`);\n } else {\n console.log('watcher: no watcher');\n }\n db.close();\n process.exit(0);\n }\n\n if (first === 'rebuild') {\n const cfg = resolveConfig();\n const result = rebuild(cfg);\n printWarnings(result.warnings);\n console.log(`rebuilt: ${docCount(result.db)} docs`);\n result.db.close();\n process.exit(0);\n }\n\n if (values.list) {\n const cfg = resolveConfig();\n for (const queryName of Object.keys(cfg.queries).sort()) console.log(queryName);\n process.exit(0);\n }\n\n const format = values.format === 'json' ? 'json' : 'table';\n\n if (first === 'query') {\n const [sql, ...params] = rest;\n if (!sql) {\n console.error(`usage: ${name} query \"<sql>\" [params...]`);\n process.exit(2);\n }\n runSql(resolveConfig(), sql, params, format, 'ad-hoc query');\n return;\n }\n\n const [name_, ...params] = [first, ...rest];\n\n if (!name_) {\n console.error(usage(name));\n process.exit(2);\n }\n\n const cfg = resolveConfig();\n\n const sql = cfg.queries[name_];\n if (!sql) {\n console.error(`unknown query: \"${name_}\"`);\n console.error(`valid queries: ${Object.keys(cfg.queries).sort().join(', ')}`);\n process.exit(2);\n }\n\n runSql(cfg, sql, params, format, `query \"${name_}\"`);\n } catch (err) {\n console.error((err as Error).message);\n process.exit(1);\n }\n}\n"],"names":["parseArgs","initConfig","loadConfig","docCount","getMeta","open","rebuild","printRows","runWatch","usage","name","parseCliArgs","argv","args","options","format","type","default","config","list","force","help","short","allowPositionals","err","console","error","message","process","exit","printWarnings","warnings","w","warn","runSql","cfg","sql","params","label","placeholderCount","match","length","db","rows","prepare","all","close","logWatchEvent","event","log","baseDir","dbPath","parsed","total","cli","values","positionals","resolveConfig","first","rest","configPath","cwd","onEvent","heartbeat","ageSec","Math","round","Date","now","parse","result","queryName","Object","keys","queries","sort","name_","join"],"mappings":"AAAA,SAASA,SAAS,QAAQ,YAAY;AAEtC,SAASC,UAAU,EAAEC,UAAU,QAAQ,cAAc;AACrD,SAASC,QAAQ,EAAEC,OAAO,EAAEC,IAAI,EAAEC,OAAO,QAAQ,UAAU;AAE3D,SAASC,SAAS,QAAQ,cAAc;AAExC,SAASC,QAAQ,QAAQ,aAAa;AAEtC,SAASC,MAAMC,IAAY;IACzB,OAAO,CAAC,OAAO,EAAEA,KAAK,2DAA2D,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,4BAA4B,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,SAAS,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,OAAO,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,kBAAkB,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,SAAS,CAAC,GAAG,CAAC,OAAO,EAAEA,KAAK,QAAQ,CAAC;AACrR;AAEA,SAASC,aAAaC,IAAc,EAAEF,IAAY;IAChD,IAAI;QACF,OAAOV,UAAU;YACfa,MAAMD;YACNE,SAAS;gBACPC,QAAQ;oBAAEC,MAAM;oBAAUC,SAAS;gBAAQ;gBAC3CC,QAAQ;oBAAEF,MAAM;gBAAS;gBACzBG,MAAM;oBAAEH,MAAM;oBAAWC,SAAS;gBAAM;gBACxCG,OAAO;oBAAEJ,MAAM;oBAAWC,SAAS;gBAAM;gBACzCI,MAAM;oBAAEL,MAAM;oBAAWC,SAAS;oBAAOK,OAAO;gBAAI;YACtD;YACAC,kBAAkB;QACpB;IACF,EAAE,OAAOC,KAAK;QACZC,QAAQC,KAAK,CAAC,AAACF,IAAcG,OAAO;QACpCF,QAAQC,KAAK,CAACjB,MAAMC;QACpBkB,QAAQC,IAAI,CAAC;IACf;AACF;AAEA,SAASC,cAAcC,QAAkB;IACvC,KAAK,MAAMC,KAAKD,SAAUN,QAAQQ,IAAI,CAACD;AACzC;AAEA,sFAAsF;AACtF,SAASE,OAAOC,GAAmB,EAAEC,GAAW,EAAEC,MAAgB,EAAEtB,MAAwB,EAAEuB,KAAa;QAC/EF;IAA1B,MAAMG,mBAAmB,EAACH,aAAAA,IAAII,KAAK,CAAC,oBAAVJ,wBAAAA,aAAoB,EAAE,EAAEK,MAAM;IACxD,IAAIJ,OAAOI,MAAM,KAAKF,kBAAkB;QACtCd,QAAQC,KAAK,CAAC,GAAGY,MAAM,SAAS,EAAEC,iBAAiB,mBAAmB,EAAEF,OAAOI,MAAM,EAAE;QACvFb,QAAQC,IAAI,CAAC;IACf;IACA,MAAM,EAAEa,EAAE,EAAEX,QAAQ,EAAE,GAAG1B,KAAK8B;IAC9BL,cAAcC;IACd,MAAMY,OAAOD,GAAGE,OAAO,CAACR,KAAKS,GAAG,IAAIR;IACpC9B,UAAUoC,MAAM5B;IAChB2B,GAAGI,KAAK;AACV;AAEA,SAASC,cAAcC,KAAiB;IACtC,IAAIA,MAAMhC,IAAI,KAAK,WAAW;QAC5BS,QAAQwB,GAAG,CAAC,CAAC,sBAAsB,EAAED,MAAME,OAAO,EAAE;QACpDzB,QAAQwB,GAAG,CAAC,CAAC,gBAAgB,EAAED,MAAMG,MAAM,EAAE;QAC7C;IACF;IACA,IAAIH,MAAMhC,IAAI,KAAK,cAAc;QAC/Bc,cAAckB,MAAMjB,QAAQ;QAC5B,IAAIiB,MAAMI,MAAM,GAAG,GAAG;YACpB3B,QAAQwB,GAAG,CAAC,CAAC,yBAAyB,EAAED,MAAMI,MAAM,CAAC,mBAAmB,EAAEJ,MAAMK,KAAK,CAAC,OAAO,CAAC;QAChG;QACA;IACF;IACA5B,QAAQC,KAAK,CAAC,CAAC,8BAA8B,EAAEsB,MAAMrB,OAAO,EAAE;AAChE;AAEA,oFAAoF;AACpF,eAAe,eAAe2B,IAAI1C,IAAc,EAAEF,IAAY;IAC5D,MAAM,EAAE6C,MAAM,EAAEC,WAAW,EAAE,GAAG7C,aAAaC,MAAMF;IAEnD,IAAI6C,OAAOlC,IAAI,EAAE;QACfI,QAAQwB,GAAG,CAACxC,MAAMC;QAClBkB,QAAQC,IAAI,CAAC;IACf;IAEA,MAAM4B,gBAAgB,IAAMvD,WAAWqD,OAAOrC,MAAM;IAEpD,IAAI;QACF,MAAM,CAACwC,OAAO,GAAGC,KAAK,GAAGH;QAEzB,IAAIE,UAAU,QAAQ;YACpB,MAAME,aAAa3D,WAAW2B,QAAQiC,GAAG;YACzCpC,QAAQwB,GAAG,CAAC,CAAC,QAAQ,EAAEW,YAAY;YACnCnC,QAAQwB,GAAG,CAAC;YACZrB,QAAQC,IAAI,CAAC;QACf;QAEA,IAAI6B,UAAU,SAAS;YACrB,MAAMvB,MAAMsB;YACZ,MAAMjD,SAAS2B,KAAK;gBAAEf,OAAOmC,OAAOnC,KAAK;gBAAE0C,SAASf;YAAc;YAClEnB,QAAQC,IAAI,CAAC;QACf;QAEA,IAAI6B,UAAU,UAAU;YACtB,MAAMvB,MAAMsB;YACZ,MAAM,EAAEf,EAAE,EAAES,MAAM,EAAEpB,QAAQ,EAAE,GAAG1B,KAAK8B;YACtCL,cAAcC;YACdN,QAAQwB,GAAG,CAAC,CAAC,IAAI,EAAEE,QAAQ;YAC3B1B,QAAQwB,GAAG,CAAC,CAAC,MAAM,EAAE9C,SAASuC,KAAK;YACnC,MAAMqB,YAAY3D,QAAQsC,IAAI;YAC9B,IAAIqB,WAAW;gBACb,MAAMC,SAASC,KAAKC,KAAK,CAAC,AAACC,CAAAA,KAAKC,GAAG,KAAKD,KAAKE,KAAK,CAACN,UAAS,IAAK;gBACjEtC,QAAQwB,GAAG,CAAC,CAAC,wBAAwB,EAAEe,OAAO,KAAK,CAAC;YACtD,OAAO;gBACLvC,QAAQwB,GAAG,CAAC;YACd;YACAP,GAAGI,KAAK;YACRlB,QAAQC,IAAI,CAAC;QACf;QAEA,IAAI6B,UAAU,WAAW;YACvB,MAAMvB,MAAMsB;YACZ,MAAMa,SAAShE,QAAQ6B;YACvBL,cAAcwC,OAAOvC,QAAQ;YAC7BN,QAAQwB,GAAG,CAAC,CAAC,SAAS,EAAE9C,SAASmE,OAAO5B,EAAE,EAAE,KAAK,CAAC;YAClD4B,OAAO5B,EAAE,CAACI,KAAK;YACflB,QAAQC,IAAI,CAAC;QACf;QAEA,IAAI0B,OAAOpC,IAAI,EAAE;YACf,MAAMgB,MAAMsB;YACZ,KAAK,MAAMc,aAAaC,OAAOC,IAAI,CAACtC,IAAIuC,OAAO,EAAEC,IAAI,GAAIlD,QAAQwB,GAAG,CAACsB;YACrE3C,QAAQC,IAAI,CAAC;QACf;QAEA,MAAMd,SAASwC,OAAOxC,MAAM,KAAK,SAAS,SAAS;QAEnD,IAAI2C,UAAU,SAAS;YACrB,MAAM,CAACtB,KAAK,GAAGC,OAAO,GAAGsB;YACzB,IAAI,CAACvB,KAAK;gBACRX,QAAQC,KAAK,CAAC,CAAC,OAAO,EAAEhB,KAAK,0BAA0B,CAAC;gBACxDkB,QAAQC,IAAI,CAAC;YACf;YACAK,OAAOuB,iBAAiBrB,KAAKC,QAAQtB,QAAQ;YAC7C;QACF;QAEA,MAAM,CAAC6D,OAAO,GAAGvC,OAAO,GAAG;YAACqB;eAAUC;SAAK;QAE3C,IAAI,CAACiB,OAAO;YACVnD,QAAQC,KAAK,CAACjB,MAAMC;YACpBkB,QAAQC,IAAI,CAAC;QACf;QAEA,MAAMM,MAAMsB;QAEZ,MAAMrB,MAAMD,IAAIuC,OAAO,CAACE,MAAM;QAC9B,IAAI,CAACxC,KAAK;YACRX,QAAQC,KAAK,CAAC,CAAC,gBAAgB,EAAEkD,MAAM,CAAC,CAAC;YACzCnD,QAAQC,KAAK,CAAC,CAAC,eAAe,EAAE8C,OAAOC,IAAI,CAACtC,IAAIuC,OAAO,EAAEC,IAAI,GAAGE,IAAI,CAAC,OAAO;YAC5EjD,QAAQC,IAAI,CAAC;QACf;QAEAK,OAAOC,KAAKC,KAAKC,QAAQtB,QAAQ,CAAC,OAAO,EAAE6D,MAAM,CAAC,CAAC;IACrD,EAAE,OAAOpD,KAAK;QACZC,QAAQC,KAAK,CAAC,AAACF,IAAcG,OAAO;QACpCC,QAAQC,IAAI,CAAC;IACf;AACF"}
@@ -1,17 +1,11 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join, resolve } from 'node:path';
3
3
  import { SenseError } from './errors.js';
4
- // All config concerns live here: the file name, discovery, parsing, and
5
- // version gating. db.ts consumes only the result -- it does no discovery
6
- // and no version logic of its own.
7
4
  export const CONFIG_FILENAME = 'sense.config.json';
8
5
  export const STATE_DIR = '.sense';
9
- // The highest `sense.config.json` `version` this build understands. Bumped
10
- // only when the config *format* changes in a breaking way -- unrelated to
11
- // the package's own semver.
6
+ // Highest sense.config.json `version` this build understands.
12
7
  export const SUPPORTED_CONFIG_VERSION = 1;
13
- // `sense init`: write a starter config into `dir` — the example doubles as
14
- // the documentation of the format. Refuses to overwrite an existing one.
8
+ // Refuses to overwrite an existing config.
15
9
  export function initConfig(dir) {
16
10
  const configPath = join(dir, CONFIG_FILENAME);
17
11
  if (existsSync(configPath)) {
@@ -25,15 +19,11 @@ export function initConfig(dir) {
25
19
  '**/*.md'
26
20
  ]
27
21
  },
28
- queries: {
29
- all: 'SELECT path, title FROM docs ORDER BY path',
30
- 'by-tag': 'SELECT path, title FROM docs WHERE has(tags, ?) ORDER BY path'
31
- }
22
+ queries: {}
32
23
  };
33
24
  writeFileSync(configPath, `${JSON.stringify(starter, null, 2)}\n`);
34
25
  return configPath;
35
26
  }
36
- // Find sense.config.json by walking up from startDir, git-style.
37
27
  export function findConfigPath(startDir) {
38
28
  let dir = resolve(startDir);
39
29
  for(;;){
@@ -44,10 +34,6 @@ export function findConfigPath(startDir) {
44
34
  dir = parent;
45
35
  }
46
36
  }
47
- // Resolve + load sense.config.json: `explicitPath` bypasses walk-up
48
- // discovery (as `--config` does on the CLI); otherwise discovery starts at
49
- // process cwd. Throws on a missing config, invalid JSON, or a `version`
50
- // newer than this build supports -- callers surface the message and exit 1.
51
37
  export function loadConfig(explicitPath) {
52
38
  var _cfg_version;
53
39
  let configPath;
@@ -63,9 +49,6 @@ export function loadConfig(explicitPath) {
63
49
  }
64
50
  const raw = readFileSync(configPath, 'utf8');
65
51
  const cfg = JSON.parse(raw);
66
- // Missing `version` is treated as 1. A version newer than this build
67
- // supports is a hard error -- misinterpreting a future config format
68
- // silently would be worse than refusing to run.
69
52
  const version = (_cfg_version = cfg.version) !== null && _cfg_version !== void 0 ? _cfg_version : 1;
70
53
  if (version > SUPPORTED_CONFIG_VERSION) {
71
54
  throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from './errors.ts';\n\n// All config concerns live here: the file name, discovery, parsing, and\n// version gating. db.ts consumes only the result -- it does no discovery\n// and no version logic of its own.\n\nexport const CONFIG_FILENAME = 'sense.config.json';\nexport const STATE_DIR = '.sense';\n\n// The highest `sense.config.json` `version` this build understands. Bumped\n// only when the config *format* changes in a breaking way -- unrelated to\n// the package's own semver.\nexport const SUPPORTED_CONFIG_VERSION = 1;\n\n// sense.config.json's on-disk shape.\nexport interface Config {\n // Editor-only pointer to schema.json; never read by sense.\n $schema?: string;\n // Config format version. Omitted = 1.\n version?: number;\n scan: { include: string[] };\n queries: Record<string, string>;\n}\n\n// A Config resolved to where it lives on disk (or, for tests, a baseDir\n// supplied directly with no backing file). `scan.include` globs and the\n// `.sense/` state dir both resolve relative to `baseDir`, never process cwd.\nexport interface ResolvedConfig extends Config {\n baseDir: string;\n configPath: string | null;\n}\n\n// `sense init`: write a starter config into `dir` — the example doubles as\n// the documentation of the format. Refuses to overwrite an existing one.\nexport function initConfig(dir: string): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n const starter: Config = {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: 1,\n scan: { include: ['**/*.md'] },\n queries: {\n all: 'SELECT path, title FROM docs ORDER BY path',\n 'by-tag': 'SELECT path, title FROM docs WHERE has(tags, ?) ORDER BY path',\n },\n };\n writeFileSync(configPath, `${JSON.stringify(starter, null, 2)}\\n`);\n return configPath;\n}\n\n// Find sense.config.json by walking up from startDir, git-style.\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n// Resolve + load sense.config.json: `explicitPath` bypasses walk-up\n// discovery (as `--config` does on the CLI); otherwise discovery starts at\n// process cwd. Throws on a missing config, invalid JSON, or a `version`\n// newer than this build supports -- callers surface the message and exit 1.\nexport function loadConfig(explicitPath?: string): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const cfg = JSON.parse(raw) as Config;\n\n // Missing `version` is treated as 1. A version newer than this build\n // supports is a hard error -- misinterpreting a future config format\n // silently would be worse than refusing to run.\n const version = cfg.version ?? 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n return { ...cfg, baseDir: dirname(configPath), configPath };\n}\n"],"names":["existsSync","readFileSync","writeFileSync","dirname","join","resolve","SenseError","CONFIG_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","initConfig","dir","configPath","starter","$schema","version","scan","include","queries","all","JSON","stringify","findConfigPath","startDir","candidate","parent","loadConfig","explicitPath","cfg","process","cwd","found","raw","parse","baseDir"],"mappings":"AAAA,SAASA,UAAU,EAAEC,YAAY,EAAEC,aAAa,QAAQ,UAAU;AAClE,SAASC,OAAO,EAAEC,IAAI,EAAEC,OAAO,QAAQ,YAAY;AACnD,SAASC,UAAU,QAAQ,cAAc;AAEzC,wEAAwE;AACxE,yEAAyE;AACzE,mCAAmC;AAEnC,OAAO,MAAMC,kBAAkB,oBAAoB;AACnD,OAAO,MAAMC,YAAY,SAAS;AAElC,2EAA2E;AAC3E,0EAA0E;AAC1E,4BAA4B;AAC5B,OAAO,MAAMC,2BAA2B,EAAE;AAoB1C,2EAA2E;AAC3E,yEAAyE;AACzE,OAAO,SAASC,WAAWC,GAAW;IACpC,MAAMC,aAAaR,KAAKO,KAAKJ;IAC7B,IAAIP,WAAWY,aAAa;QAC1B,MAAM,IAAIN,WAAW,iBAAiB,GAAGC,gBAAgB,mBAAmB,EAAEI,KAAK;IACrF;IACA,MAAME,UAAkB;QACtBC,SAAS;QACTC,SAAS;QACTC,MAAM;YAAEC,SAAS;gBAAC;aAAU;QAAC;QAC7BC,SAAS;YACPC,KAAK;YACL,UAAU;QACZ;IACF;IACAjB,cAAcU,YAAY,GAAGQ,KAAKC,SAAS,CAACR,SAAS,MAAM,GAAG,EAAE,CAAC;IACjE,OAAOD;AACT;AAEA,iEAAiE;AACjE,OAAO,SAASU,eAAeC,QAAgB;IAC7C,IAAIZ,MAAMN,QAAQkB;IAClB,OAAS;QACP,MAAMC,YAAYpB,KAAKO,KAAKJ;QAC5B,IAAIP,WAAWwB,YAAY,OAAOA;QAClC,MAAMC,SAAStB,QAAQQ;QACvB,IAAIc,WAAWd,KAAK,OAAO;QAC3BA,MAAMc;IACR;AACF;AAEA,oEAAoE;AACpE,2EAA2E;AAC3E,wEAAwE;AACxE,4EAA4E;AAC5E,OAAO,SAASC,WAAWC,YAAqB;QAmB9BC;IAlBhB,IAAIhB;IACJ,IAAIe,cAAc;QAChBf,aAAaP,QAAQwB,QAAQC,GAAG,IAAIH;QACpC,IAAI,CAAC3B,WAAWY,aAAa,MAAM,IAAIN,WAAW,oBAAoB,CAAC,kBAAkB,EAAEM,YAAY;IACzG,OAAO;QACL,MAAMmB,QAAQT,eAAeO,QAAQC,GAAG;QACxC,IAAI,CAACC,OAAO;YACV,MAAM,IAAIzB,WAAW,oBAAoB,CAAC,eAAe,EAAEC,gBAAgB,IAAI,EAAEsB,QAAQC,GAAG,GAAG,wBAAwB,CAAC;QAC1H;QACAlB,aAAamB;IACf;IAEA,MAAMC,MAAM/B,aAAaW,YAAY;IACrC,MAAMgB,MAAMR,KAAKa,KAAK,CAACD;IAEvB,qEAAqE;IACrE,qEAAqE;IACrE,gDAAgD;IAChD,MAAMjB,WAAUa,eAAAA,IAAIb,OAAO,cAAXa,0BAAAA,eAAe;IAC/B,IAAIb,UAAUN,0BAA0B;QACtC,MAAM,IAAIH,WAAW,8BAA8B,CAAC,eAAe,EAAES,QAAQ,uBAAuB,CAAC;IACvG;IAEA,OAAO;QAAE,GAAGa,GAAG;QAAEM,SAAS/B,QAAQS;QAAaA;IAAW;AAC5D"}
1
+ {"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/config.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { SenseError } from './errors.ts';\n\nexport const CONFIG_FILENAME = 'sense.config.json';\nexport const STATE_DIR = '.sense';\n\n// Highest sense.config.json `version` this build understands.\nexport const SUPPORTED_CONFIG_VERSION = 1;\n\nexport interface Config {\n // Editor-only pointer to schema.json; never read by sense.\n $schema?: string;\n version?: number;\n scan: { include: string[] };\n queries: Record<string, string>;\n}\n\nexport interface ResolvedConfig extends Config {\n baseDir: string;\n configPath: string | null;\n}\n\n// Refuses to overwrite an existing config.\nexport function initConfig(dir: string): string {\n const configPath = join(dir, CONFIG_FILENAME);\n if (existsSync(configPath)) {\n throw new SenseError('CONFIG_EXISTS', `${CONFIG_FILENAME} already exists in ${dir}`);\n }\n const starter: Config = {\n $schema: 'https://unpkg.com/sensemaking/schema.json',\n version: 1,\n scan: { include: ['**/*.md'] },\n queries: {},\n };\n writeFileSync(configPath, `${JSON.stringify(starter, null, 2)}\\n`);\n return configPath;\n}\n\nexport function findConfigPath(startDir: string): string | null {\n let dir = resolve(startDir);\n for (;;) {\n const candidate = join(dir, CONFIG_FILENAME);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nexport function loadConfig(explicitPath?: string): ResolvedConfig {\n let configPath: string;\n if (explicitPath) {\n configPath = resolve(process.cwd(), explicitPath);\n if (!existsSync(configPath)) throw new SenseError('CONFIG_NOT_FOUND', `config not found: ${configPath}`);\n } else {\n const found = findConfigPath(process.cwd());\n if (!found) {\n throw new SenseError('CONFIG_NOT_FOUND', `could not find ${CONFIG_FILENAME} in ${process.cwd()} or any parent directory`);\n }\n configPath = found;\n }\n\n const raw = readFileSync(configPath, 'utf8');\n const cfg = JSON.parse(raw) as Config;\n\n const version = cfg.version ?? 1;\n if (version > SUPPORTED_CONFIG_VERSION) {\n throw new SenseError('CONFIG_VERSION_UNSUPPORTED', `config version ${version} requires a newer sense`);\n }\n\n return { ...cfg, baseDir: dirname(configPath), configPath };\n}\n"],"names":["existsSync","readFileSync","writeFileSync","dirname","join","resolve","SenseError","CONFIG_FILENAME","STATE_DIR","SUPPORTED_CONFIG_VERSION","initConfig","dir","configPath","starter","$schema","version","scan","include","queries","JSON","stringify","findConfigPath","startDir","candidate","parent","loadConfig","explicitPath","cfg","process","cwd","found","raw","parse","baseDir"],"mappings":"AAAA,SAASA,UAAU,EAAEC,YAAY,EAAEC,aAAa,QAAQ,UAAU;AAClE,SAASC,OAAO,EAAEC,IAAI,EAAEC,OAAO,QAAQ,YAAY;AACnD,SAASC,UAAU,QAAQ,cAAc;AAEzC,OAAO,MAAMC,kBAAkB,oBAAoB;AACnD,OAAO,MAAMC,YAAY,SAAS;AAElC,8DAA8D;AAC9D,OAAO,MAAMC,2BAA2B,EAAE;AAe1C,2CAA2C;AAC3C,OAAO,SAASC,WAAWC,GAAW;IACpC,MAAMC,aAAaR,KAAKO,KAAKJ;IAC7B,IAAIP,WAAWY,aAAa;QAC1B,MAAM,IAAIN,WAAW,iBAAiB,GAAGC,gBAAgB,mBAAmB,EAAEI,KAAK;IACrF;IACA,MAAME,UAAkB;QACtBC,SAAS;QACTC,SAAS;QACTC,MAAM;YAAEC,SAAS;gBAAC;aAAU;QAAC;QAC7BC,SAAS,CAAC;IACZ;IACAhB,cAAcU,YAAY,GAAGO,KAAKC,SAAS,CAACP,SAAS,MAAM,GAAG,EAAE,CAAC;IACjE,OAAOD;AACT;AAEA,OAAO,SAASS,eAAeC,QAAgB;IAC7C,IAAIX,MAAMN,QAAQiB;IAClB,OAAS;QACP,MAAMC,YAAYnB,KAAKO,KAAKJ;QAC5B,IAAIP,WAAWuB,YAAY,OAAOA;QAClC,MAAMC,SAASrB,QAAQQ;QACvB,IAAIa,WAAWb,KAAK,OAAO;QAC3BA,MAAMa;IACR;AACF;AAEA,OAAO,SAASC,WAAWC,YAAqB;QAgB9BC;IAfhB,IAAIf;IACJ,IAAIc,cAAc;QAChBd,aAAaP,QAAQuB,QAAQC,GAAG,IAAIH;QACpC,IAAI,CAAC1B,WAAWY,aAAa,MAAM,IAAIN,WAAW,oBAAoB,CAAC,kBAAkB,EAAEM,YAAY;IACzG,OAAO;QACL,MAAMkB,QAAQT,eAAeO,QAAQC,GAAG;QACxC,IAAI,CAACC,OAAO;YACV,MAAM,IAAIxB,WAAW,oBAAoB,CAAC,eAAe,EAAEC,gBAAgB,IAAI,EAAEqB,QAAQC,GAAG,GAAG,wBAAwB,CAAC;QAC1H;QACAjB,aAAakB;IACf;IAEA,MAAMC,MAAM9B,aAAaW,YAAY;IACrC,MAAMe,MAAMR,KAAKa,KAAK,CAACD;IAEvB,MAAMhB,WAAUY,eAAAA,IAAIZ,OAAO,cAAXY,0BAAAA,eAAe;IAC/B,IAAIZ,UAAUN,0BAA0B;QACtC,MAAM,IAAIH,WAAW,8BAA8B,CAAC,eAAe,EAAES,QAAQ,uBAAuB,CAAC;IACvG;IAEA,OAAO;QAAE,GAAGY,GAAG;QAAEM,SAAS9B,QAAQS;QAAaA;IAAW;AAC5D"}
package/dist/esm/db.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import type { Config, ResolvedConfig } from './config.js';
3
3
  export declare const DB_FILENAME = "cache.db";
4
- export declare const SCHEMA_VERSION = "1";
4
+ export declare const SCHEMA_VERSION = "2";
5
5
  export interface OpenResult {
6
6
  db: DatabaseSync;
7
7
  cfg: ResolvedConfig;