@theglitchking/semantic-memory 1.2.1 → 1.5.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 (65) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/CHANGELOG.md +144 -0
  4. package/ROADMAP.md +5 -2
  5. package/commands/healthcheck.md +13 -11
  6. package/dist/{chunk-EVAJGOCB.js → chunk-2EELGTLX.js} +49 -5
  7. package/dist/chunk-2EELGTLX.js.map +1 -0
  8. package/dist/chunk-5ZKNO6ZD.js +96 -0
  9. package/dist/chunk-5ZKNO6ZD.js.map +1 -0
  10. package/dist/chunk-7M3JZOYH.js +105 -0
  11. package/dist/chunk-7M3JZOYH.js.map +1 -0
  12. package/dist/chunk-A3KYHKB5.js +176 -0
  13. package/dist/chunk-A3KYHKB5.js.map +1 -0
  14. package/dist/{chunk-CYWPDFNI.js → chunk-CSXZ7QUU.js} +4 -203
  15. package/dist/chunk-CSXZ7QUU.js.map +1 -0
  16. package/dist/chunk-FM26NFTR.js +272 -0
  17. package/dist/chunk-FM26NFTR.js.map +1 -0
  18. package/dist/chunk-NTYYS53O.js +91 -0
  19. package/dist/chunk-NTYYS53O.js.map +1 -0
  20. package/dist/{chunk-E2Z4J4MK.js → chunk-OWZWMB4P.js} +70 -2
  21. package/dist/chunk-OWZWMB4P.js.map +1 -0
  22. package/dist/chunk-S4KW7VH4.js +203 -0
  23. package/dist/chunk-S4KW7VH4.js.map +1 -0
  24. package/dist/{chunk-HODDRXJK.js → chunk-V45NJIHI.js} +4 -2
  25. package/dist/chunk-V45NJIHI.js.map +1 -0
  26. package/dist/chunk-WSU4QIPB.js +311 -0
  27. package/dist/chunk-WSU4QIPB.js.map +1 -0
  28. package/dist/cli/index.js +265 -65
  29. package/dist/cli/index.js.map +1 -1
  30. package/dist/core/index.d.ts +39 -2
  31. package/dist/core/index.js +17 -11
  32. package/dist/decay-ELJUKVQG.js +14 -0
  33. package/dist/dossier-2UG724LB.js +39 -0
  34. package/dist/healthcheck-IMB47LZE.js +19 -0
  35. package/dist/indexer-YW4O3VVM.js +8 -0
  36. package/dist/lexicon-5PXQEMUF.js +26 -0
  37. package/dist/lexicon-5PXQEMUF.js.map +1 -0
  38. package/dist/lint-XUQ7JDYX.js +16 -0
  39. package/dist/lint-XUQ7JDYX.js.map +1 -0
  40. package/dist/mcp/server.js +478 -40
  41. package/dist/mcp/server.js.map +1 -1
  42. package/dist/profile-LXUEF7TX.js +20 -0
  43. package/dist/profile-LXUEF7TX.js.map +1 -0
  44. package/dist/{schema-F3RTY6SQ.js → schema-ATFCLC6Y.js} +2 -2
  45. package/dist/schema-ATFCLC6Y.js.map +1 -0
  46. package/dist/{search-27WYAI24.js → search-AHKEFI2B.js} +2 -2
  47. package/dist/telemetry-GB2TEHVE.js +21 -0
  48. package/dist/telemetry-GB2TEHVE.js.map +1 -0
  49. package/hooks/vault-context.js +296 -12
  50. package/package.json +2 -1
  51. package/skills/vault-first/SKILL.md +18 -0
  52. package/dist/chunk-CAN2GWUG.js +0 -88
  53. package/dist/chunk-CAN2GWUG.js.map +0 -1
  54. package/dist/chunk-CYWPDFNI.js.map +0 -1
  55. package/dist/chunk-E2Z4J4MK.js.map +0 -1
  56. package/dist/chunk-EVAJGOCB.js.map +0 -1
  57. package/dist/chunk-HODDRXJK.js.map +0 -1
  58. package/dist/healthcheck-7YOPNDXN.js +0 -14
  59. package/dist/indexer-KD6UTXZB.js +0 -8
  60. package/dist/lint-GMXQF4RW.js +0 -11
  61. /package/dist/{healthcheck-7YOPNDXN.js.map → decay-ELJUKVQG.js.map} +0 -0
  62. /package/dist/{indexer-KD6UTXZB.js.map → dossier-2UG724LB.js.map} +0 -0
  63. /package/dist/{lint-GMXQF4RW.js.map → healthcheck-IMB47LZE.js.map} +0 -0
  64. /package/dist/{schema-F3RTY6SQ.js.map → indexer-YW4O3VVM.js.map} +0 -0
  65. /package/dist/{search-27WYAI24.js.map → search-AHKEFI2B.js.map} +0 -0
@@ -14,7 +14,11 @@ version: 1
14
14
  # Provenance fields \u2014 apply to all types, enforced by lint.find_missing_provenance.
15
15
  # sources: URLs or filesystem paths the note was derived from
16
16
  # derived_from: wikilinks to other notes this one synthesizes
17
- # last_verified: ISO date this note was last confirmed true (YYYY-MM-DD)
17
+ # last_verified: ISO date this note was last confirmed true (YYYY-MM-DD).
18
+ # Set at creation and by verify_note; NOT bumped on ordinary edits.
19
+ # Drives confidence decay (see the decay: block below).
20
+ # evergreen: true \u2192 the note is pinned against decay while last_verified is
21
+ # within 365 days; after that it decays normally (re-affirm to reset).
18
22
  # status: one of: draft | active | stale | deprecated | archived
19
23
  # confidence: one of: high | medium | low
20
24
  provenance_fields:
@@ -47,6 +51,18 @@ types:
47
51
  description: External reference (paper, blog post, doc) used as provenance
48
52
  required: [title, status, source]
49
53
 
54
+ alias:
55
+ description: Learned human\u2192artifact vocabulary entry (v1.4 lexicon). Maps the human's phrasing to a concrete path/symbol.
56
+ required: [canonical]
57
+
58
+ dossier:
59
+ description: Entity-centric living note (v1.5). One per critical component \u2014 purpose, failure modes, knobs, incident log, current state. Accretes in place.
60
+ required: [title, status, entity]
61
+
62
+ profile:
63
+ description: Speaker profile (v1.5). Models how THIS human communicates \u2014 severity calibration, chronic omissions, verbosity, shorthand. One human, one profile.
64
+ required: [title, status]
65
+
50
66
  # Severity values for gotcha type.
51
67
  severity_enum: [low, medium, high, critical]
52
68
 
@@ -63,6 +79,58 @@ lint:
63
79
  schema_violations:
64
80
  # Triggers on missing required fields, unknown type, or enum mismatch.
65
81
  severity: error
82
+
83
+ # Confidence decay (v1.3). Down-weights search results by time since last_verified.
84
+ # Composes multiplicatively with load_priority \u2014 it never removes notes, only ranks
85
+ # them lower (floored). Set enabled: false for byte-identical pre-v1.3 ranking.
86
+ decay:
87
+ enabled: true
88
+ default_half_life_days: 365 # multiplier = 0.5 ^ (age_days / half_life)
89
+ per_type:
90
+ decision: 365
91
+ note: 365
92
+ gotcha: 180
93
+ source: null # null = never decays
94
+ proposal: 14
95
+ floor: 0.1 # notes never drop below this multiplier
96
+ hotness_boost:
97
+ enabled: false # extend half-life for heavily-backlinked notes (opt-in)
98
+ cap: 2.0
99
+
100
+ # Path-class ranking (v1.4). Down-weights whole regions of the vault by path glob
101
+ # (composes multiplicatively with decay + load_priority). First matching rule wins.
102
+ # The default down-weights archived notes so retired docs stop dominating results.
103
+ path_class:
104
+ enabled: true
105
+ rules:
106
+ - glob: "archive/**"
107
+ multiplier: 0.3
108
+
109
+ # Usage-feedback ranking (v1.5). Boosts notes that get CITED after retrieval (per
110
+ # the local selection log) so load-bearing knowledge floats up. Bounded by cap to
111
+ # stop a feedback runaway; composes multiplicatively with decay + path_class +
112
+ # load_priority. A no-op until the selection log has citations. The complementary
113
+ # signal (retrieved-but-never-cited "decoys") is surfaced by lint_vault, NEVER
114
+ # auto-down-ranked. Set enabled: false for byte-identical pre-v1.5 ranking.
115
+ usage_boost:
116
+ enabled: true
117
+ cap: 1.5 # max multiplier a note can earn from citations
118
+ per_citation: 0.1 # multiplier += per_citation \xD7 citation_count, then capped
119
+
120
+ # Token-frugal tool surface (v1.4). When enabled, the server registers a reduced
121
+ # tool set in modes that don't need the full surface (outage-silence \u2192 search/read
122
+ # only). DEFAULT OFF \u2014 mid-session mode switches don't re-register, so opt in and
123
+ # dogfood before relying on it.
124
+ tools:
125
+ conditional: false
126
+
127
+ # Selection-logging telemetry (v1.3.1). Records what search returned and which
128
+ # note the answer used, to a LOCAL append-only JSONL at
129
+ # .claude/.semantic-memory/selection.jsonl (gitignored). Never leaves the machine.
130
+ # Feeds the decay_candidates lint and (later) usage-feedback ranking. Set
131
+ # enabled: false to disable all logging.
132
+ telemetry:
133
+ enabled: true
66
134
  `;
67
135
 
68
136
  // src/core/schema.ts
@@ -177,4 +245,4 @@ export {
177
245
  installDefaultSchema,
178
246
  validateNote
179
247
  };
180
- //# sourceMappingURL=chunk-E2Z4J4MK.js.map
248
+ //# sourceMappingURL=chunk-OWZWMB4P.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/schema.ts","../src/core/schema-default.ts"],"sourcesContent":["import { readFile, writeFile, access } from \"node:fs/promises\";\nimport { constants } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport matter from \"gray-matter\";\nimport { DEFAULT_SCHEMA_YAML } from \"./schema-default.js\";\n\nexport interface VaultSchema {\n version: number;\n provenance_fields: string[];\n status_enum: string[];\n confidence_enum: string[];\n severity_enum: string[];\n types: Record<string, { description?: string; required: string[] }>;\n lint: {\n missing_provenance: { applies_to: string[]; severity: \"warn\" | \"error\" };\n stale: { max_age_days: number; severity: \"warn\" | \"error\" };\n schema_violations: { severity: \"warn\" | \"error\" };\n };\n}\n\nexport type Severity = \"warn\" | \"error\";\n\nexport interface LintFinding {\n path: string;\n rule: \"missing_provenance\" | \"stale\" | \"schema_violations\" | \"broken_links\" | \"code_symbols\" | \"decay_candidates\" | \"alias_conflicts\" | \"decoys\";\n severity: Severity;\n message: string;\n}\n\nexport async function loadSchema(vaultPath: string): Promise<VaultSchema> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n let raw: string;\n try {\n await access(schemaPath, constants.R_OK);\n raw = await readFile(schemaPath, \"utf-8\");\n } catch {\n raw = DEFAULT_SCHEMA_YAML;\n }\n const parsed = parseYaml(raw) as VaultSchema;\n return parsed;\n}\n\n/**\n * Bootstrap the schema file in a vault by writing the default if none exists.\n * Used by the `--install-schema` CLI flag. Returns the path that was (or would be) written.\n */\nexport async function installDefaultSchema(vaultPath: string, force = false): Promise<{ written: boolean; path: string }> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n if (!force) {\n try {\n await access(schemaPath, constants.F_OK);\n return { written: false, path: schemaPath };\n } catch {\n // not present → write it\n }\n }\n await writeFile(schemaPath, DEFAULT_SCHEMA_YAML, \"utf-8\");\n return { written: true, path: schemaPath };\n}\n\n/**\n * Validate a single note's frontmatter + content against the schema.\n * Returns findings (possibly empty). Does NOT throw — lint never throws.\n */\nexport function validateNote(\n relativePath: string,\n rawFileContent: string,\n schema: VaultSchema,\n opts: { todayIso?: string } = {}\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const { data: fm } = matter(rawFileContent);\n\n const typeName = typeof fm.type === \"string\" ? fm.type : \"note\";\n const typeDef = schema.types[typeName];\n\n if (!typeDef) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `unknown type \"${typeName}\" — allowed: ${Object.keys(schema.types).join(\", \")}`,\n });\n return findings; // can't validate required fields if type is unknown\n }\n\n // Required field check\n for (const field of typeDef.required) {\n if (fm[field] === undefined || fm[field] === null || fm[field] === \"\") {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `missing required field \"${field}\" for type \"${typeName}\"`,\n });\n }\n }\n\n // Enum checks\n if (fm.status !== undefined && !schema.status_enum.includes(String(fm.status))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `status \"${fm.status}\" not in enum [${schema.status_enum.join(\", \")}]`,\n });\n }\n if (fm.confidence !== undefined && !schema.confidence_enum.includes(String(fm.confidence))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `confidence \"${fm.confidence}\" not in enum [${schema.confidence_enum.join(\", \")}]`,\n });\n }\n if (typeName === \"gotcha\" && fm.severity !== undefined && !schema.severity_enum.includes(String(fm.severity))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `severity \"${fm.severity}\" not in enum [${schema.severity_enum.join(\", \")}]`,\n });\n }\n\n // Missing provenance\n if (schema.lint.missing_provenance.applies_to.includes(typeName)) {\n const hasSources = Array.isArray(fm.sources) ? fm.sources.length > 0 : typeof fm.sources === \"string\" && fm.sources.length > 0;\n const hasDerived = Array.isArray(fm.derived_from) ? fm.derived_from.length > 0 : typeof fm.derived_from === \"string\" && fm.derived_from.length > 0;\n if (!hasSources && !hasDerived) {\n findings.push({\n path: relativePath,\n rule: \"missing_provenance\",\n severity: schema.lint.missing_provenance.severity,\n message: `no sources or derived_from on ${typeName}`,\n });\n }\n }\n\n // Stale\n if (schema.lint.stale.max_age_days > 0 && fm.last_verified) {\n const today = opts.todayIso ?? new Date().toISOString().slice(0, 10);\n const verified = String(fm.last_verified);\n const ageDays = dayDiff(today, verified);\n if (ageDays !== null && ageDays > schema.lint.stale.max_age_days) {\n findings.push({\n path: relativePath,\n rule: \"stale\",\n severity: schema.lint.stale.severity,\n message: `last_verified ${verified} is ${ageDays} days old (limit ${schema.lint.stale.max_age_days})`,\n });\n }\n }\n\n return findings;\n}\n\nfunction dayDiff(todayIso: string, thenIso: string): number | null {\n const a = Date.parse(todayIso);\n const b = Date.parse(thenIso);\n if (Number.isNaN(a) || Number.isNaN(b)) return null;\n return Math.round((a - b) / 86400000);\n}\n","// Default vault schema shipped with semantic-sidekick.\n// Intentionally minimal — the plan is to grow from pain, not preempt it.\n// Users can override with a vault.schema.yml at the vault root.\n\nexport const DEFAULT_SCHEMA_YAML = `# semantic-sidekick vault schema (minimal starter).\n# Override by placing a vault.schema.yml at the vault root.\n\nversion: 1\n\n# Provenance fields — apply to all types, enforced by lint.find_missing_provenance.\n# sources: URLs or filesystem paths the note was derived from\n# derived_from: wikilinks to other notes this one synthesizes\n# last_verified: ISO date this note was last confirmed true (YYYY-MM-DD).\n# Set at creation and by verify_note; NOT bumped on ordinary edits.\n# Drives confidence decay (see the decay: block below).\n# evergreen: true → the note is pinned against decay while last_verified is\n# within 365 days; after that it decays normally (re-affirm to reset).\n# status: one of: draft | active | stale | deprecated | archived\n# confidence: one of: high | medium | low\nprovenance_fields:\n - sources\n - derived_from\n - last_verified\n - status\n - confidence\n\n# Allowed status values (enforced by schema_violations lint).\nstatus_enum: [draft, active, stale, deprecated, archived]\nconfidence_enum: [high, medium, low]\n\n# Note types. Each type declares its required frontmatter.\n# Any field not listed as required is allowed but not enforced.\ntypes:\n note:\n description: Generic note — default type when \\`type\\` frontmatter is absent\n required: [title, status]\n\n decision:\n description: Architectural or process decision with dated rationale\n required: [title, status, decided_on, decision_maker]\n\n gotcha:\n description: Compressed lesson from a past incident/bug/surprise\n required: [title, status, last_verified, severity]\n\n source:\n description: External reference (paper, blog post, doc) used as provenance\n required: [title, status, source]\n\n alias:\n description: Learned human→artifact vocabulary entry (v1.4 lexicon). Maps the human's phrasing to a concrete path/symbol.\n required: [canonical]\n\n dossier:\n description: Entity-centric living note (v1.5). One per critical component — purpose, failure modes, knobs, incident log, current state. Accretes in place.\n required: [title, status, entity]\n\n profile:\n description: Speaker profile (v1.5). Models how THIS human communicates — severity calibration, chronic omissions, verbosity, shorthand. One human, one profile.\n required: [title, status]\n\n# Severity values for gotcha type.\nseverity_enum: [low, medium, high, critical]\n\n# Lint policy. Each rule has a default severity that pre-commit/CI consumes.\nlint:\n missing_provenance:\n # Triggers when a note type in this list has no sources AND no derived_from.\n applies_to: [note, decision, gotcha]\n severity: warn\n stale:\n # Triggers when last_verified is older than this many days (0 = disabled).\n max_age_days: 180\n severity: warn\n schema_violations:\n # Triggers on missing required fields, unknown type, or enum mismatch.\n severity: error\n\n# Confidence decay (v1.3). Down-weights search results by time since last_verified.\n# Composes multiplicatively with load_priority — it never removes notes, only ranks\n# them lower (floored). Set enabled: false for byte-identical pre-v1.3 ranking.\ndecay:\n enabled: true\n default_half_life_days: 365 # multiplier = 0.5 ^ (age_days / half_life)\n per_type:\n decision: 365\n note: 365\n gotcha: 180\n source: null # null = never decays\n proposal: 14\n floor: 0.1 # notes never drop below this multiplier\n hotness_boost:\n enabled: false # extend half-life for heavily-backlinked notes (opt-in)\n cap: 2.0\n\n# Path-class ranking (v1.4). Down-weights whole regions of the vault by path glob\n# (composes multiplicatively with decay + load_priority). First matching rule wins.\n# The default down-weights archived notes so retired docs stop dominating results.\npath_class:\n enabled: true\n rules:\n - glob: \"archive/**\"\n multiplier: 0.3\n\n# Usage-feedback ranking (v1.5). Boosts notes that get CITED after retrieval (per\n# the local selection log) so load-bearing knowledge floats up. Bounded by cap to\n# stop a feedback runaway; composes multiplicatively with decay + path_class +\n# load_priority. A no-op until the selection log has citations. The complementary\n# signal (retrieved-but-never-cited \"decoys\") is surfaced by lint_vault, NEVER\n# auto-down-ranked. Set enabled: false for byte-identical pre-v1.5 ranking.\nusage_boost:\n enabled: true\n cap: 1.5 # max multiplier a note can earn from citations\n per_citation: 0.1 # multiplier += per_citation × citation_count, then capped\n\n# Token-frugal tool surface (v1.4). When enabled, the server registers a reduced\n# tool set in modes that don't need the full surface (outage-silence → search/read\n# only). DEFAULT OFF — mid-session mode switches don't re-register, so opt in and\n# dogfood before relying on it.\ntools:\n conditional: false\n\n# Selection-logging telemetry (v1.3.1). Records what search returned and which\n# note the answer used, to a LOCAL append-only JSONL at\n# .claude/.semantic-memory/selection.jsonl (gitignored). Never leaves the machine.\n# Feeds the decay_candidates lint and (later) usage-feedback ranking. Set\n# enabled: false to disable all logging.\ntelemetry:\n enabled: true\n`;\n"],"mappings":"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","names":[]}
@@ -0,0 +1,203 @@
1
+ // src/core/session.ts
2
+ import { mkdir, readFile, writeFile, rm } from "fs/promises";
3
+ import { existsSync } from "fs";
4
+ import { join } from "path";
5
+ import { spawn } from "child_process";
6
+ var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
7
+ var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
8
+ var TAIL_BYTES = 4096;
9
+ var SessionManager = class {
10
+ constructor(sessionDir) {
11
+ this.sessionDir = sessionDir;
12
+ }
13
+ sessionDir;
14
+ get sessionPath() {
15
+ return join(this.sessionDir, "session.json");
16
+ }
17
+ async start(task, opts = {}) {
18
+ if (!task || task.trim().length === 0) {
19
+ return { ok: false, error: "session_start requires a non-empty task." };
20
+ }
21
+ await mkdir(this.sessionDir, { recursive: true });
22
+ const existing = await this.read();
23
+ if (existing && !existing.closed_at) {
24
+ const reuseSameTask = opts.reuseSameTask !== false;
25
+ if (reuseSameTask && existing.task === task) {
26
+ return { ok: true, id: existing.id, reused: true };
27
+ }
28
+ return {
29
+ ok: false,
30
+ error: `Session already open: id=${existing.id}, task=${JSON.stringify(existing.task)}. Close it with session_finish first.`
31
+ };
32
+ }
33
+ const now = (/* @__PURE__ */ new Date()).toISOString();
34
+ const id = `ses_${now.replace(/[:.]/g, "-")}`;
35
+ const state = {
36
+ id,
37
+ task: task.trim(),
38
+ started_at: now,
39
+ last_activity_at: now,
40
+ verifications: [],
41
+ notes_touched: []
42
+ };
43
+ await writeFile(this.sessionPath, JSON.stringify(state, null, 2), "utf-8");
44
+ return { ok: true, id };
45
+ }
46
+ async status() {
47
+ const state = await this.read();
48
+ if (!state || state.closed_at) return { state: "no_session" };
49
+ const age = Date.now() - new Date(state.last_activity_at).getTime();
50
+ const label = age > STALE_THRESHOLD_MS ? "stale" : "active";
51
+ return { ...state, state: label };
52
+ }
53
+ async run(cmd, timeoutMs) {
54
+ const state = await this.read();
55
+ if (!state || state.closed_at) {
56
+ return { ok: false, error: "No active session. Run session_start first." };
57
+ }
58
+ if (!cmd || cmd.trim().length === 0) {
59
+ return { ok: false, error: "session_run requires a non-empty command." };
60
+ }
61
+ const start = Date.now();
62
+ const record = await this.spawnAndCapture(cmd, timeoutMs ?? DEFAULT_TIMEOUT_MS);
63
+ record.duration_ms = Date.now() - start;
64
+ state.verifications.push(record);
65
+ state.last_activity_at = (/* @__PURE__ */ new Date()).toISOString();
66
+ await writeFile(this.sessionPath, JSON.stringify(state, null, 2), "utf-8");
67
+ return { ok: true, record };
68
+ }
69
+ async finish(input) {
70
+ const state = await this.read();
71
+ if (!state || state.closed_at) {
72
+ return { ok: false, error: "No active session to finish." };
73
+ }
74
+ const explicitWaiver = input.verified === false;
75
+ if (!explicitWaiver && state.verifications.length === 0) {
76
+ return {
77
+ ok: false,
78
+ error: "session_finish refused: no verification commands recorded. Run session_run with a real verification command (tests, lint, etc.) first, OR pass verified=false with a reason to waive verification (e.g. for doc-only edits)."
79
+ };
80
+ }
81
+ if (explicitWaiver && (!input.reason || input.reason.trim().length === 0)) {
82
+ return {
83
+ ok: false,
84
+ error: "session_finish refused: verified=false requires a non-empty `reason` explaining why verification is not applicable to this task."
85
+ };
86
+ }
87
+ if (!input.summary || input.summary.trim().length === 0) {
88
+ return { ok: false, error: "session_finish requires a non-empty `summary`." };
89
+ }
90
+ const closed = {
91
+ ...state,
92
+ closed_at: (/* @__PURE__ */ new Date()).toISOString(),
93
+ closed_summary: input.summary.trim(),
94
+ ...explicitWaiver && {
95
+ closed_verified_waived: true,
96
+ closed_waiver_reason: input.reason.trim()
97
+ }
98
+ };
99
+ await rm(this.sessionPath, { force: true });
100
+ return { ok: true, closed };
101
+ }
102
+ /**
103
+ * Append paths to notes_touched on the active session, deduplicating. No-op when no
104
+ * session is active. Used by patch tools to record what the session edited.
105
+ */
106
+ async recordNotesTouched(paths) {
107
+ if (paths.length === 0) return;
108
+ const state = await this.read();
109
+ if (!state || state.closed_at) return;
110
+ const set = new Set(state.notes_touched);
111
+ for (const p of paths) set.add(p);
112
+ state.notes_touched = Array.from(set);
113
+ state.last_activity_at = (/* @__PURE__ */ new Date()).toISOString();
114
+ await writeFile(this.sessionPath, JSON.stringify(state, null, 2), "utf-8");
115
+ }
116
+ /**
117
+ * Read the active session WITHOUT computing the stale label. Returns null when no
118
+ * session is active or when the file is unparseable.
119
+ */
120
+ async readActive() {
121
+ const state = await this.read();
122
+ if (!state || state.closed_at) return null;
123
+ return state;
124
+ }
125
+ async read() {
126
+ if (!existsSync(this.sessionPath)) return null;
127
+ try {
128
+ const raw = await readFile(this.sessionPath, "utf-8");
129
+ const parsed = JSON.parse(raw);
130
+ if (typeof parsed?.id !== "string") return null;
131
+ return parsed;
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
136
+ spawnAndCapture(cmd, timeoutMs) {
137
+ return new Promise((resolve) => {
138
+ const proc = spawn(cmd, { shell: true });
139
+ let buf = "";
140
+ let killedByTimeout = false;
141
+ const timer = setTimeout(() => {
142
+ killedByTimeout = true;
143
+ proc.kill("SIGTERM");
144
+ setTimeout(() => {
145
+ try {
146
+ proc.kill("SIGKILL");
147
+ } catch {
148
+ }
149
+ }, 1e3).unref();
150
+ }, timeoutMs);
151
+ const onData = (chunk) => {
152
+ buf += chunk.toString("utf-8");
153
+ if (buf.length > TAIL_BYTES * 4) {
154
+ buf = buf.slice(buf.length - TAIL_BYTES * 2);
155
+ }
156
+ };
157
+ proc.stdout?.on("data", onData);
158
+ proc.stderr?.on("data", onData);
159
+ proc.on("close", (code, signal) => {
160
+ clearTimeout(timer);
161
+ const tail = buf.slice(-TAIL_BYTES);
162
+ resolve({
163
+ cmd,
164
+ exit: killedByTimeout ? null : code,
165
+ signal: killedByTimeout ? "SIGTERM (timeout)" : signal ?? null,
166
+ duration_ms: 0,
167
+ tail,
168
+ at: (/* @__PURE__ */ new Date()).toISOString()
169
+ });
170
+ });
171
+ proc.on("error", (err) => {
172
+ clearTimeout(timer);
173
+ const tail = buf + `
174
+ [spawn error: ${err.message}]`;
175
+ resolve({
176
+ cmd,
177
+ exit: null,
178
+ signal: null,
179
+ duration_ms: 0,
180
+ tail: tail.slice(-TAIL_BYTES),
181
+ at: (/* @__PURE__ */ new Date()).toISOString()
182
+ });
183
+ });
184
+ });
185
+ }
186
+ };
187
+ function deriveProjectRoot(notesPath) {
188
+ const norm = notesPath.replace(/[\\/]+$/, "");
189
+ if (norm.endsWith("/.claude/.vault") || norm.endsWith("\\.claude\\.vault")) {
190
+ return norm.slice(0, -".claude/.vault".length).replace(/[\\/]+$/, "");
191
+ }
192
+ return norm;
193
+ }
194
+ function deriveSessionDir(notesPath) {
195
+ return join(deriveProjectRoot(notesPath), ".claude", ".semantic-memory");
196
+ }
197
+
198
+ export {
199
+ SessionManager,
200
+ deriveProjectRoot,
201
+ deriveSessionDir
202
+ };
203
+ //# sourceMappingURL=chunk-S4KW7VH4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/session.ts"],"sourcesContent":["import { mkdir, readFile, writeFile, rm } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { spawn } from \"node:child_process\";\n\nexport interface VerificationRecord {\n cmd: string;\n exit: number | null;\n signal: string | null;\n duration_ms: number;\n tail: string;\n at: string;\n}\n\nexport interface SessionState {\n id: string;\n task: string;\n started_at: string;\n last_activity_at: string;\n verifications: VerificationRecord[];\n notes_touched: string[];\n closed_at?: string;\n closed_summary?: string;\n closed_verified_waived?: boolean;\n closed_waiver_reason?: string;\n}\n\nexport type SessionStatus =\n | { state: \"no_session\" }\n | (SessionState & { state: \"active\" | \"stale\" });\n\nexport interface SessionStartOptions {\n /** When true and a session for the same task is already open, return the existing id rather than failing. Defaults to true. */\n reuseSameTask?: boolean;\n}\n\nexport interface SessionFinishInput {\n summary: string;\n /**\n * When false, the caller is explicitly waiving verification (e.g. for doc-only edits).\n * Requires a non-empty `reason`. When true (default) and no verifications have been\n * recorded, the call is refused.\n */\n verified?: boolean;\n reason?: string;\n}\n\nconst STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;\nconst DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;\nconst TAIL_BYTES = 4096;\n\n/**\n * SessionManager owns a single session.json file at `<sessionDir>/session.json`.\n *\n * State is durable across MCP server restarts (the file persists). At most one session\n * is active per project at a time — concurrent sessions are explicitly out of scope\n * for v1.1; brain has the same constraint.\n *\n * Hard gates:\n * - session_finish refuses without verifications unless verified=false + reason\n * - session_run refuses without an open session\n * - session_start refuses if another session is open with a different task\n *\n * Stale detection: a session whose last_activity_at is >24h old is reported as\n * state:\"stale\" by status(). Stale sessions can still be finished or extended; the\n * label is informational so callers can decide whether to abandon (rm + start new) or\n * resume.\n */\nexport class SessionManager {\n constructor(private sessionDir: string) {}\n\n get sessionPath(): string {\n return join(this.sessionDir, \"session.json\");\n }\n\n async start(task: string, opts: SessionStartOptions = {}): Promise<{ ok: true; id: string; reused?: boolean } | { ok: false; error: string }> {\n if (!task || task.trim().length === 0) {\n return { ok: false, error: \"session_start requires a non-empty task.\" };\n }\n await mkdir(this.sessionDir, { recursive: true });\n const existing = await this.read();\n if (existing && !existing.closed_at) {\n const reuseSameTask = opts.reuseSameTask !== false;\n if (reuseSameTask && existing.task === task) {\n return { ok: true, id: existing.id, reused: true };\n }\n return {\n ok: false,\n error: `Session already open: id=${existing.id}, task=${JSON.stringify(existing.task)}. Close it with session_finish first.`,\n };\n }\n const now = new Date().toISOString();\n const id = `ses_${now.replace(/[:.]/g, \"-\")}`;\n const state: SessionState = {\n id,\n task: task.trim(),\n started_at: now,\n last_activity_at: now,\n verifications: [],\n notes_touched: [],\n };\n await writeFile(this.sessionPath, JSON.stringify(state, null, 2), \"utf-8\");\n return { ok: true, id };\n }\n\n async status(): Promise<SessionStatus> {\n const state = await this.read();\n if (!state || state.closed_at) return { state: \"no_session\" };\n const age = Date.now() - new Date(state.last_activity_at).getTime();\n const label: \"active\" | \"stale\" = age > STALE_THRESHOLD_MS ? \"stale\" : \"active\";\n return { ...state, state: label };\n }\n\n async run(cmd: string, timeoutMs?: number): Promise<{ ok: true; record: VerificationRecord } | { ok: false; error: string }> {\n const state = await this.read();\n if (!state || state.closed_at) {\n return { ok: false, error: \"No active session. Run session_start first.\" };\n }\n if (!cmd || cmd.trim().length === 0) {\n return { ok: false, error: \"session_run requires a non-empty command.\" };\n }\n const start = Date.now();\n const record = await this.spawnAndCapture(cmd, timeoutMs ?? DEFAULT_TIMEOUT_MS);\n record.duration_ms = Date.now() - start;\n state.verifications.push(record);\n state.last_activity_at = new Date().toISOString();\n await writeFile(this.sessionPath, JSON.stringify(state, null, 2), \"utf-8\");\n return { ok: true, record };\n }\n\n async finish(input: SessionFinishInput): Promise<{ ok: true; closed: SessionState } | { ok: false; error: string }> {\n const state = await this.read();\n if (!state || state.closed_at) {\n return { ok: false, error: \"No active session to finish.\" };\n }\n const explicitWaiver = input.verified === false;\n if (!explicitWaiver && state.verifications.length === 0) {\n return {\n ok: false,\n error:\n \"session_finish refused: no verification commands recorded. Run session_run with a real verification command (tests, lint, etc.) first, OR pass verified=false with a reason to waive verification (e.g. for doc-only edits).\",\n };\n }\n if (explicitWaiver && (!input.reason || input.reason.trim().length === 0)) {\n return {\n ok: false,\n error: \"session_finish refused: verified=false requires a non-empty `reason` explaining why verification is not applicable to this task.\",\n };\n }\n if (!input.summary || input.summary.trim().length === 0) {\n return { ok: false, error: \"session_finish requires a non-empty `summary`.\" };\n }\n const closed: SessionState = {\n ...state,\n closed_at: new Date().toISOString(),\n closed_summary: input.summary.trim(),\n ...(explicitWaiver && {\n closed_verified_waived: true,\n closed_waiver_reason: input.reason!.trim(),\n }),\n };\n // Sessions are ephemeral runtime state — finished sessions are removed from disk.\n // Durable artifacts (synthesize_note proposals, log entries) capture what mattered.\n await rm(this.sessionPath, { force: true });\n return { ok: true, closed };\n }\n\n /**\n * Append paths to notes_touched on the active session, deduplicating. No-op when no\n * session is active. Used by patch tools to record what the session edited.\n */\n async recordNotesTouched(paths: string[]): Promise<void> {\n if (paths.length === 0) return;\n const state = await this.read();\n if (!state || state.closed_at) return;\n const set = new Set(state.notes_touched);\n for (const p of paths) set.add(p);\n state.notes_touched = Array.from(set);\n state.last_activity_at = new Date().toISOString();\n await writeFile(this.sessionPath, JSON.stringify(state, null, 2), \"utf-8\");\n }\n\n /**\n * Read the active session WITHOUT computing the stale label. Returns null when no\n * session is active or when the file is unparseable.\n */\n async readActive(): Promise<SessionState | null> {\n const state = await this.read();\n if (!state || state.closed_at) return null;\n return state;\n }\n\n private async read(): Promise<SessionState | null> {\n if (!existsSync(this.sessionPath)) return null;\n try {\n const raw = await readFile(this.sessionPath, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (typeof parsed?.id !== \"string\") return null;\n return parsed as SessionState;\n } catch {\n return null;\n }\n }\n\n private spawnAndCapture(cmd: string, timeoutMs: number): Promise<VerificationRecord> {\n return new Promise((resolve) => {\n const proc = spawn(cmd, { shell: true });\n let buf = \"\";\n let killedByTimeout = false;\n const timer = setTimeout(() => {\n killedByTimeout = true;\n proc.kill(\"SIGTERM\");\n setTimeout(() => {\n try { proc.kill(\"SIGKILL\"); } catch { /* already gone */ }\n }, 1000).unref();\n }, timeoutMs);\n\n const onData = (chunk: Buffer) => {\n buf += chunk.toString(\"utf-8\");\n if (buf.length > TAIL_BYTES * 4) {\n buf = buf.slice(buf.length - TAIL_BYTES * 2);\n }\n };\n proc.stdout?.on(\"data\", onData);\n proc.stderr?.on(\"data\", onData);\n\n proc.on(\"close\", (code, signal) => {\n clearTimeout(timer);\n const tail = buf.slice(-TAIL_BYTES);\n resolve({\n cmd,\n exit: killedByTimeout ? null : code,\n signal: killedByTimeout ? \"SIGTERM (timeout)\" : (signal ?? null),\n duration_ms: 0,\n tail,\n at: new Date().toISOString(),\n });\n });\n\n proc.on(\"error\", (err) => {\n clearTimeout(timer);\n const tail = buf + `\\n[spawn error: ${err.message}]`;\n resolve({\n cmd,\n exit: null,\n signal: null,\n duration_ms: 0,\n tail: tail.slice(-TAIL_BYTES),\n at: new Date().toISOString(),\n });\n });\n });\n }\n}\n\n/**\n * Heuristic: derive the project root from the vault path. If the vault path ends in\n * `.claude/.vault`, the project root is two levels up. Otherwise the vault path is\n * treated as the project root.\n */\nexport function deriveProjectRoot(notesPath: string): string {\n const norm = notesPath.replace(/[\\\\/]+$/, \"\");\n if (norm.endsWith(\"/.claude/.vault\") || norm.endsWith(\"\\\\.claude\\\\.vault\")) {\n return norm.slice(0, -\".claude/.vault\".length).replace(/[\\\\/]+$/, \"\");\n }\n return norm;\n}\n\nexport function deriveSessionDir(notesPath: string): string {\n return join(deriveProjectRoot(notesPath), \".claude\", \".semantic-memory\");\n}\n"],"mappings":"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","names":[]}
@@ -58,6 +58,8 @@ var Indexer = class {
58
58
  const headers = this.extractHeaders(tree);
59
59
  const plainText = this.stripMarkdown(content);
60
60
  const chunks = this.chunkText(plainText);
61
+ const symptoms = Array.isArray(frontmatter.symptoms) ? frontmatter.symptoms.filter((s) => typeof s === "string" && s.trim().length > 0).map((s) => s.trim()) : [];
62
+ const allChunks = symptoms.length > 0 ? [...chunks, ...symptoms] : chunks;
61
63
  const title = frontmatter.title || headers[0] || basename(relativePath, ".md");
62
64
  const mtime = this.resolveMtime(frontmatter, fileStat.mtime);
63
65
  const loadPriority = typeof frontmatter.load_priority === "number" ? Math.min(10, Math.max(1, frontmatter.load_priority)) : void 0;
@@ -74,7 +76,7 @@ var Indexer = class {
74
76
  wikilinks,
75
77
  tags,
76
78
  headers,
77
- chunks,
79
+ chunks: allChunks,
78
80
  mtime,
79
81
  ...loadPriority !== void 0 && { loadPriority },
80
82
  ...status !== void 0 && { status },
@@ -166,4 +168,4 @@ var Indexer = class {
166
168
  export {
167
169
  Indexer
168
170
  };
169
- //# sourceMappingURL=chunk-HODDRXJK.js.map
171
+ //# sourceMappingURL=chunk-V45NJIHI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/indexer.ts"],"sourcesContent":["import { unified } from \"unified\";\nimport remarkParse from \"remark-parse\";\nimport remarkWikiLink from \"remark-wiki-link\";\nimport matter from \"gray-matter\";\nimport { glob } from \"glob\";\nimport { readFile, writeFile, stat } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { basename, join, relative } from \"node:path\";\nimport type { IndexedDocument } from \"./types.js\";\n\nconst DOCS_CACHE_FILE = \"docs.cache.json\";\n\nconst CHUNK_TARGET_CHARS = 2000; // ~512 tokens\n\nexport class Indexer {\n private notesPath: string;\n private processor: any;\n\n constructor(notesPath: string) {\n this.notesPath = notesPath;\n this.processor = unified().use(remarkParse).use(remarkWikiLink);\n }\n\n async indexAll(): Promise<IndexedDocument[]> {\n // follow: true descends into symlinked directories. Without this, users\n // who symlink an external folder into their vault (a common pattern for\n // sharing notes across projects) get silently empty indexing for the\n // symlinked content. glob does cycle detection via visited inodes so\n // circular symlinks are handled safely.\n const files = await glob(\"**/*.md\", { cwd: this.notesPath, follow: true });\n const docs = await Promise.all(\n files.map((file) => this.indexFile(join(this.notesPath, file), file))\n );\n return docs;\n }\n\n /**\n * Load parsed-doc cache from disk. Returns null if missing or unreadable.\n * The cache is a verbatim JSON dump of a prior indexAll() result, written by\n * saveDocsCache. Consumers (search CLI / hook path) prefer this over re-parsing\n * 500+ markdown files; the tradeoff is that cache may be stale if the vault\n * changed since the last reindex — same staleness window as the vector cache.\n */\n async loadDocsCache(indexPath: string): Promise<IndexedDocument[] | null> {\n const p = join(indexPath, DOCS_CACHE_FILE);\n if (!existsSync(p)) return null;\n try {\n const raw = await readFile(p, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) return null;\n return parsed as IndexedDocument[];\n } catch {\n return null;\n }\n }\n\n async saveDocsCache(indexPath: string, docs: IndexedDocument[]): Promise<void> {\n const p = join(indexPath, DOCS_CACHE_FILE);\n await writeFile(p, JSON.stringify(docs), \"utf-8\");\n }\n\n async indexFile(\n absolutePath: string,\n relativePath: string\n ): Promise<IndexedDocument> {\n const [raw, fileStat] = await Promise.all([\n readFile(absolutePath, \"utf-8\"),\n stat(absolutePath),\n ]);\n const { data: frontmatter, content } = matter(raw);\n const tree = this.processor.parse(content);\n\n const wikilinks = this.extractWikilinks(tree);\n const tags = this.extractTags(content, frontmatter);\n const headers = this.extractHeaders(tree);\n const plainText = this.stripMarkdown(content);\n const chunks = this.chunkText(plainText);\n\n // Symptom-keyed indexing (v1.4 Phase 5). Each verbatim `symptoms:` phrase is\n // added as its own chunk, so a terse symptom query (\"it hangs after reindex\")\n // matches the note directly — the fix for asymmetric retrieval (query-shaped\n // keys stored alongside document-shaped content). Synthetic child chunks point\n // at the same note path; no index-format change.\n const symptoms = Array.isArray(frontmatter.symptoms)\n ? (frontmatter.symptoms as unknown[]).filter((s): s is string => typeof s === \"string\" && s.trim().length > 0).map((s) => s.trim())\n : [];\n const allChunks = symptoms.length > 0 ? [...chunks, ...symptoms] : chunks;\n\n const title =\n (frontmatter.title as string) ||\n headers[0] ||\n basename(relativePath, \".md\");\n\n // Resolve modification time: prefer frontmatter date fields over fs.stat\n // Supports hit-em-with-the-docs (last_updated) and common alternatives\n const mtime = this.resolveMtime(frontmatter, fileStat.mtime);\n\n // Optional hit-em-with-the-docs fields (only populated when present)\n const loadPriority =\n typeof frontmatter.load_priority === \"number\"\n ? Math.min(10, Math.max(1, frontmatter.load_priority))\n : undefined;\n const status =\n typeof frontmatter.status === \"string\" ? frontmatter.status : undefined;\n const tier =\n typeof frontmatter.tier === \"string\" ? frontmatter.tier : undefined;\n const domains = Array.isArray(frontmatter.domains)\n ? (frontmatter.domains as string[])\n : undefined;\n const purpose =\n typeof frontmatter.purpose === \"string\" ? frontmatter.purpose : undefined;\n const relatedDocs = Array.isArray(frontmatter.related_docs)\n ? (frontmatter.related_docs as string[]).filter((v) => typeof v === \"string\")\n : undefined;\n\n return {\n path: relativePath,\n title,\n content: plainText,\n frontmatter,\n wikilinks,\n tags,\n headers,\n chunks: allChunks,\n mtime,\n ...(loadPriority !== undefined && { loadPriority }),\n ...(status !== undefined && { status }),\n ...(tier !== undefined && { tier }),\n ...(domains !== undefined && { domains }),\n ...(purpose !== undefined && { purpose }),\n ...(relatedDocs !== undefined && { relatedDocs }),\n };\n }\n\n /**\n * Resolve the best available modification date for a document.\n * Priority: last_updated → updated → date → lastmod → fs.stat mtime\n * Accepts YYYY-MM-DD strings or full ISO timestamps.\n */\n private resolveMtime(\n frontmatter: Record<string, unknown>,\n statMtime: Date\n ): string {\n const candidates = [\n frontmatter.last_updated,\n frontmatter.updated,\n frontmatter.date,\n frontmatter.lastmod,\n ];\n for (const val of candidates) {\n if (!val) continue;\n const str = val instanceof Date ? val.toISOString() : String(val);\n const parsed = new Date(str);\n if (!isNaN(parsed.getTime())) return parsed.toISOString();\n }\n return statMtime.toISOString();\n }\n\n private extractWikilinks(tree: any): string[] {\n const links: string[] = [];\n const walk = (node: any) => {\n if (node.type === \"wikiLink\") {\n links.push(node.value || node.data?.alias || \"\");\n }\n if (node.children) {\n for (const child of node.children) walk(child);\n }\n };\n walk(tree);\n return [...new Set(links.filter(Boolean))];\n }\n\n private extractTags(content: string, frontmatter: Record<string, unknown>): string[] {\n const inlineTags = [...content.matchAll(/(?:^|\\s)#([a-zA-Z][\\w-/]*)/g)].map(\n (m) => m[1]\n );\n\n const fmTags = Array.isArray(frontmatter.tags)\n ? (frontmatter.tags as string[])\n : [];\n\n return [...new Set([...fmTags, ...inlineTags])];\n }\n\n private extractHeaders(tree: any): string[] {\n const headers: string[] = [];\n const walk = (node: any) => {\n if (node.type === \"heading\") {\n const text = this.nodeToText(node);\n if (text) headers.push(text);\n }\n if (node.children) {\n for (const child of node.children) walk(child);\n }\n };\n walk(tree);\n return headers;\n }\n\n private nodeToText(node: any): string {\n if (node.type === \"text\") return node.value;\n if (node.children) return node.children.map((c: any) => this.nodeToText(c)).join(\"\");\n return \"\";\n }\n\n private stripMarkdown(content: string): string {\n return content\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`[^`]+`/g, \"\")\n .replace(/!\\[.*?\\]\\(.*?\\)/g, \"\")\n .replace(/\\[([^\\]]+)\\]\\(.*?\\)/g, \"$1\")\n .replace(/#{1,6}\\s+/g, \"\")\n .replace(/[*_~]{1,3}/g, \"\")\n .replace(/>\\s+/g, \"\")\n .replace(/\\|.*\\|/g, \"\")\n .replace(/-{3,}/g, \"\")\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trim();\n }\n\n chunkText(text: string): string[] {\n if (text.length <= CHUNK_TARGET_CHARS) return [text];\n\n const sentences = text.match(/[^.!?\\n]+[.!?\\n]+|[^.!?\\n]+$/g) || [text];\n const chunks: string[] = [];\n let current = \"\";\n\n for (const sentence of sentences) {\n if (current.length + sentence.length > CHUNK_TARGET_CHARS && current) {\n chunks.push(current.trim());\n current = \"\";\n }\n current += sentence;\n }\n if (current.trim()) chunks.push(current.trim());\n\n return chunks;\n }\n}\n"],"mappings":"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","names":[]}