@remnic/core 9.3.624 → 9.3.626

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 (261) hide show
  1. package/dist/access-cli.js +18 -16
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.d.ts +12 -5
  4. package/dist/access-http.js +10 -9
  5. package/dist/access-mcp.d.ts +5 -5
  6. package/dist/access-mcp.js +8 -8
  7. package/dist/access-schema.d.ts +5 -5
  8. package/dist/{access-service-CBNEKjzN.d.ts → access-service-C_sfOHsX.d.ts} +26 -3
  9. package/dist/access-service.d.ts +5 -5
  10. package/dist/access-service.js +7 -7
  11. package/dist/action-confidence.d.ts +1 -1
  12. package/dist/active-memory-bridge.d.ts +1 -1
  13. package/dist/active-recall.d.ts +1 -1
  14. package/dist/active-recall.js +2 -1
  15. package/dist/active-recall.js.map +1 -1
  16. package/dist/behavior-learner.d.ts +1 -1
  17. package/dist/behavior-signals.d.ts +1 -1
  18. package/dist/bootstrap.d.ts +4 -4
  19. package/dist/briefing.d.ts +1 -1
  20. package/dist/briefing.js +3 -3
  21. package/dist/buffer-surprise-report.d.ts +1 -1
  22. package/dist/buffer.d.ts +1 -1
  23. package/dist/calibration.d.ts +1 -1
  24. package/dist/causal-behavior.d.ts +1 -1
  25. package/dist/causal-consolidation.d.ts +1 -1
  26. package/dist/causal-consolidation.js +4 -4
  27. package/dist/{chunk-7TPH6UZL.js → chunk-2RHI3FGV.js} +540 -17
  28. package/dist/chunk-2RHI3FGV.js.map +1 -0
  29. package/dist/{chunk-GYTVOLNX.js → chunk-3MNBW7R7.js} +2 -2
  30. package/dist/{chunk-QFQQFX2H.js → chunk-3R2UZV3U.js} +2 -2
  31. package/dist/{chunk-O4UNM6OR.js → chunk-532VCWYW.js} +2 -2
  32. package/dist/{chunk-2UFQYU5F.js → chunk-57QXN2CS.js} +2 -2
  33. package/dist/chunk-7WV3F5DQ.js +22 -0
  34. package/dist/chunk-7WV3F5DQ.js.map +1 -0
  35. package/dist/{chunk-RKW6QR7W.js → chunk-AZ4RI3QD.js} +1461 -78
  36. package/dist/chunk-AZ4RI3QD.js.map +1 -0
  37. package/dist/{chunk-KQFQ3IS5.js → chunk-F3FY3D3S.js} +43 -7
  38. package/dist/chunk-F3FY3D3S.js.map +1 -0
  39. package/dist/{chunk-4R4KTDIE.js → chunk-FPNQF475.js} +1 -1
  40. package/dist/chunk-FPNQF475.js.map +1 -0
  41. package/dist/{chunk-UGEBPVNI.js → chunk-GE7Q7KXP.js} +2 -2
  42. package/dist/{chunk-GLWW3EJQ.js → chunk-KB4MFBF5.js} +3 -3
  43. package/dist/{chunk-5GOMXHLC.js → chunk-KKTXCFD7.js} +255 -1
  44. package/dist/chunk-KKTXCFD7.js.map +1 -0
  45. package/dist/{chunk-FH3PPO42.js → chunk-KVFYTRMV.js} +2 -2
  46. package/dist/{chunk-BNW5NJJH.js → chunk-LQYTQCXM.js} +2 -2
  47. package/dist/{chunk-AYHXQR53.js → chunk-MVQN73GT.js} +2 -2
  48. package/dist/{chunk-ZZPIJPPD.js → chunk-N5RGXWLQ.js} +2 -2
  49. package/dist/chunk-NDAH7BJ5.js +213 -0
  50. package/dist/chunk-NDAH7BJ5.js.map +1 -0
  51. package/dist/{chunk-R3OQGYOU.js → chunk-P2D2MM47.js} +2 -2
  52. package/dist/{chunk-PSUB67YB.js → chunk-PW6GURU3.js} +2 -2
  53. package/dist/{chunk-W3BKVM64.js → chunk-QDV6VAD4.js} +2 -2
  54. package/dist/{chunk-3QSU4NFF.js → chunk-QHXW3LZV.js} +3 -3
  55. package/dist/{chunk-I6UCUHLK.js → chunk-SHV5Y2WU.js} +182 -3
  56. package/dist/chunk-SHV5Y2WU.js.map +1 -0
  57. package/dist/{chunk-OZXVGYGZ.js → chunk-STDAAGH7.js} +2 -2
  58. package/dist/{chunk-FMGWXIES.js → chunk-TZDSNIRO.js} +5 -5
  59. package/dist/{chunk-2L54V4ZO.js → chunk-UELS6WWF.js} +2 -2
  60. package/dist/{chunk-PJGB7XRR.js → chunk-UGHUNQ74.js} +502 -134
  61. package/dist/chunk-UGHUNQ74.js.map +1 -0
  62. package/dist/{chunk-FG76RDVI.js → chunk-Y3TMFC6I.js} +136 -4
  63. package/dist/chunk-Y3TMFC6I.js.map +1 -0
  64. package/dist/{chunk-BPSGLMQ4.js → chunk-YQNADJCT.js} +2 -2
  65. package/dist/{cli-Cw729yLf.d.ts → cli-EZv6YE6_.d.ts} +3 -3
  66. package/dist/cli.d.ts +6 -6
  67. package/dist/cli.js +23 -21
  68. package/dist/compounding/engine.d.ts +1 -1
  69. package/dist/compounding/engine.js +3 -3
  70. package/dist/compounding/preference-consolidator.d.ts +1 -1
  71. package/dist/compression-optimizer.d.ts +1 -1
  72. package/dist/config.d.ts +1 -1
  73. package/dist/config.js +2 -1
  74. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  75. package/dist/connectors/codex-materialize-runner.js +3 -3
  76. package/dist/connectors/codex-materialize.d.ts +1 -1
  77. package/dist/connectors/index.d.ts +1 -1
  78. package/dist/connectors/index.js +3 -3
  79. package/dist/consolidation-provenance-check.d.ts +1 -1
  80. package/dist/consolidation-undo.d.ts +1 -1
  81. package/dist/contradiction/index.d.ts +2 -2
  82. package/dist/conversation-index/backend.d.ts +1 -1
  83. package/dist/conversation-index/chunker.d.ts +1 -1
  84. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  85. package/dist/conversation-index/indexer.d.ts +1 -1
  86. package/dist/conversation-index/search.d.ts +1 -1
  87. package/dist/day-summary.d.ts +1 -1
  88. package/dist/delinearize.d.ts +1 -1
  89. package/dist/direct-answer-wiring.d.ts +1 -1
  90. package/dist/direct-answer.d.ts +1 -1
  91. package/dist/embedding-fallback.d.ts +1 -1
  92. package/dist/enrichment/index.d.ts +1 -1
  93. package/dist/entity-retrieval.d.ts +1 -1
  94. package/dist/entity-retrieval.js +3 -3
  95. package/dist/entity-schema.d.ts +1 -1
  96. package/dist/explicit-capture.d.ts +4 -4
  97. package/dist/extraction-judge-telemetry.d.ts +1 -1
  98. package/dist/extraction-judge-training.d.ts +1 -1
  99. package/dist/extraction-judge.d.ts +1 -1
  100. package/dist/extraction.d.ts +1 -1
  101. package/dist/fallback-llm.d.ts +1 -1
  102. package/dist/identity-continuity.d.ts +1 -1
  103. package/dist/importance.d.ts +1 -1
  104. package/dist/index.d.ts +307 -9
  105. package/dist/index.js +155 -29
  106. package/dist/index.js.map +1 -1
  107. package/dist/intent.d.ts +1 -1
  108. package/dist/lcm/engine.d.ts +1 -1
  109. package/dist/lcm/index.d.ts +1 -1
  110. package/dist/lcm/tools.d.ts +1 -1
  111. package/dist/lifecycle.d.ts +1 -1
  112. package/dist/live-connectors-runner.d.ts +1 -1
  113. package/dist/local-llm.d.ts +1 -1
  114. package/dist/maintenance/memory-governance.d.ts +1 -1
  115. package/dist/maintenance/memory-governance.js +3 -3
  116. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
  117. package/dist/maintenance/rebuild-memory-projection.js +4 -4
  118. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  119. package/dist/memory-action-policy.d.ts +1 -1
  120. package/dist/memory-cache.d.ts +1 -1
  121. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  122. package/dist/memory-projection-store.d.ts +1 -1
  123. package/dist/memory-provenance.d.ts +1 -1
  124. package/dist/memory-worth-outcomes.d.ts +1 -1
  125. package/dist/models-json.d.ts +1 -1
  126. package/dist/namespaces/migrate.d.ts +1 -1
  127. package/dist/namespaces/migrate.js +4 -4
  128. package/dist/namespaces/principal.d.ts +1 -1
  129. package/dist/namespaces/search.d.ts +1 -1
  130. package/dist/namespaces/storage.d.ts +1 -1
  131. package/dist/namespaces/storage.js +3 -3
  132. package/dist/native-knowledge.d.ts +1 -1
  133. package/dist/operator-toolkit.d.ts +1 -1
  134. package/dist/operator-toolkit.js +8 -7
  135. package/dist/{orchestrator-CqWOjfgl.d.ts → orchestrator-CEycaY3M.d.ts} +361 -4
  136. package/dist/orchestrator.d.ts +4 -4
  137. package/dist/orchestrator.js +13 -11
  138. package/dist/patterns-cli.d.ts +1 -1
  139. package/dist/policy-runtime.d.ts +1 -1
  140. package/dist/qmd-recall-cache.d.ts +1 -1
  141. package/dist/qmd.d.ts +1 -1
  142. package/dist/recall-disclosure-escalation.d.ts +1 -1
  143. package/dist/recall-explain-renderer.d.ts +1 -1
  144. package/dist/recall-explain-renderer.js +3 -3
  145. package/dist/recall-planner-llm.d.ts +1 -1
  146. package/dist/recall-state.d.ts +1 -1
  147. package/dist/recall-tag-filter.d.ts +1 -1
  148. package/dist/recall-xray-cli.d.ts +1 -1
  149. package/dist/recall-xray-cli.js +4 -4
  150. package/dist/recall-xray-renderer.d.ts +1 -1
  151. package/dist/recall-xray-renderer.js +3 -3
  152. package/dist/recall-xray.d.ts +1 -1
  153. package/dist/recall-xray.js +2 -2
  154. package/dist/resolve-auth-token.d.ts +1 -1
  155. package/dist/resume-bundles.js +3 -2
  156. package/dist/retrieval-agents.d.ts +1 -1
  157. package/dist/retrieval-tiers.d.ts +1 -1
  158. package/dist/routing/engine.d.ts +1 -1
  159. package/dist/routing/store.d.ts +1 -1
  160. package/dist/schemas.d.ts +10 -10
  161. package/dist/search/embed-helper.d.ts +1 -1
  162. package/dist/search/factory.d.ts +1 -1
  163. package/dist/search/index.d.ts +1 -1
  164. package/dist/search/lancedb-backend.d.ts +1 -1
  165. package/dist/search/meilisearch-backend.d.ts +1 -1
  166. package/dist/search/noop-backend.d.ts +1 -1
  167. package/dist/search/orama-backend.d.ts +1 -1
  168. package/dist/search/port.d.ts +1 -1
  169. package/dist/search/remote-backend.d.ts +1 -1
  170. package/dist/{semantic-SLAa_prH.d.ts → semantic-DJR8_DMQ.d.ts} +1 -1
  171. package/dist/{semantic-consolidation-4HkHWgeI.d.ts → semantic-consolidation-FbhPeJjB.d.ts} +1 -1
  172. package/dist/semantic-consolidation.d.ts +2 -2
  173. package/dist/semantic-consolidation.js +4 -4
  174. package/dist/semantic-rule-promotion.js +3 -3
  175. package/dist/semantic-rule-verifier.d.ts +1 -1
  176. package/dist/semantic-rule-verifier.js +3 -3
  177. package/dist/session-observer-bands.d.ts +1 -1
  178. package/dist/session-observer-state.d.ts +1 -1
  179. package/dist/shared-context/manager.d.ts +5 -5
  180. package/dist/signal.d.ts +1 -1
  181. package/dist/storage.d.ts +19 -1
  182. package/dist/storage.js +2 -2
  183. package/dist/summarizer.d.ts +1 -1
  184. package/dist/summary-snapshot.d.ts +1 -1
  185. package/dist/temporal-supersession.d.ts +1 -1
  186. package/dist/temporal-validity.d.ts +1 -1
  187. package/dist/threading.d.ts +1 -1
  188. package/dist/tier-migration.d.ts +1 -1
  189. package/dist/tier-routing.d.ts +1 -1
  190. package/dist/topics.d.ts +1 -1
  191. package/dist/transcript.d.ts +1 -1
  192. package/dist/types-D5VRAI04.d.ts +3134 -0
  193. package/dist/types.d.ts +3 -2862
  194. package/dist/types.js +1 -1
  195. package/dist/utility-runtime.d.ts +1 -1
  196. package/dist/verified-recall.js +3 -3
  197. package/package.json +1 -1
  198. package/src/access-http.ts +167 -0
  199. package/src/access-mcp.ts +198 -0
  200. package/src/access-service.ts +65 -0
  201. package/src/cli.ts +187 -0
  202. package/src/config.ts +7 -0
  203. package/src/index.ts +7 -0
  204. package/src/orchestrator.ts +42 -0
  205. package/src/storage.ts +106 -0
  206. package/src/types.ts +5 -0
  207. package/src/wearables/cleanup.test.ts +134 -0
  208. package/src/wearables/cleanup.ts +188 -0
  209. package/src/wearables/cli.test.ts +170 -0
  210. package/src/wearables/cli.ts +441 -0
  211. package/src/wearables/config.test.ts +143 -0
  212. package/src/wearables/config.ts +332 -0
  213. package/src/wearables/corrections.test.ts +118 -0
  214. package/src/wearables/corrections.ts +211 -0
  215. package/src/wearables/day-store.test.ts +143 -0
  216. package/src/wearables/day-store.ts +238 -0
  217. package/src/wearables/errors.test.ts +32 -0
  218. package/src/wearables/errors.ts +29 -0
  219. package/src/wearables/index.ts +114 -0
  220. package/src/wearables/memory-gen.test.ts +342 -0
  221. package/src/wearables/memory-gen.ts +413 -0
  222. package/src/wearables/pipeline.test.ts +608 -0
  223. package/src/wearables/pipeline.ts +519 -0
  224. package/src/wearables/redaction.test.ts +94 -0
  225. package/src/wearables/redaction.ts +156 -0
  226. package/src/wearables/registry.test.ts +62 -0
  227. package/src/wearables/registry.ts +133 -0
  228. package/src/wearables/service.test.ts +425 -0
  229. package/src/wearables/service.ts +691 -0
  230. package/src/wearables/speakers.test.ts +110 -0
  231. package/src/wearables/speakers.ts +174 -0
  232. package/src/wearables/storage-io.test.ts +105 -0
  233. package/src/wearables/sync-state.test.ts +134 -0
  234. package/src/wearables/sync-state.ts +186 -0
  235. package/src/wearables/types.ts +285 -0
  236. package/dist/chunk-4R4KTDIE.js.map +0 -1
  237. package/dist/chunk-5GOMXHLC.js.map +0 -1
  238. package/dist/chunk-7TPH6UZL.js.map +0 -1
  239. package/dist/chunk-FG76RDVI.js.map +0 -1
  240. package/dist/chunk-I6UCUHLK.js.map +0 -1
  241. package/dist/chunk-KQFQ3IS5.js.map +0 -1
  242. package/dist/chunk-PJGB7XRR.js.map +0 -1
  243. package/dist/chunk-RKW6QR7W.js.map +0 -1
  244. /package/dist/{chunk-GYTVOLNX.js.map → chunk-3MNBW7R7.js.map} +0 -0
  245. /package/dist/{chunk-QFQQFX2H.js.map → chunk-3R2UZV3U.js.map} +0 -0
  246. /package/dist/{chunk-O4UNM6OR.js.map → chunk-532VCWYW.js.map} +0 -0
  247. /package/dist/{chunk-2UFQYU5F.js.map → chunk-57QXN2CS.js.map} +0 -0
  248. /package/dist/{chunk-UGEBPVNI.js.map → chunk-GE7Q7KXP.js.map} +0 -0
  249. /package/dist/{chunk-GLWW3EJQ.js.map → chunk-KB4MFBF5.js.map} +0 -0
  250. /package/dist/{chunk-FH3PPO42.js.map → chunk-KVFYTRMV.js.map} +0 -0
  251. /package/dist/{chunk-BNW5NJJH.js.map → chunk-LQYTQCXM.js.map} +0 -0
  252. /package/dist/{chunk-AYHXQR53.js.map → chunk-MVQN73GT.js.map} +0 -0
  253. /package/dist/{chunk-ZZPIJPPD.js.map → chunk-N5RGXWLQ.js.map} +0 -0
  254. /package/dist/{chunk-R3OQGYOU.js.map → chunk-P2D2MM47.js.map} +0 -0
  255. /package/dist/{chunk-PSUB67YB.js.map → chunk-PW6GURU3.js.map} +0 -0
  256. /package/dist/{chunk-W3BKVM64.js.map → chunk-QDV6VAD4.js.map} +0 -0
  257. /package/dist/{chunk-3QSU4NFF.js.map → chunk-QHXW3LZV.js.map} +0 -0
  258. /package/dist/{chunk-OZXVGYGZ.js.map → chunk-STDAAGH7.js.map} +0 -0
  259. /package/dist/{chunk-FMGWXIES.js.map → chunk-TZDSNIRO.js.map} +0 -0
  260. /package/dist/{chunk-2L54V4ZO.js.map → chunk-UELS6WWF.js.map} +0 -0
  261. /package/dist/{chunk-BPSGLMQ4.js.map → chunk-YQNADJCT.js.map} +0 -0
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Wearable transcript cleanup — deterministic, zero-LLM normalization.
3
+ *
4
+ * ASR output from always-on wearables is noisy: fragmented utterances,
5
+ * filler tokens, stuttered repeats, and occasional pure garbage. This
6
+ * module cleans a conversation in place-order without changing meaning:
7
+ * everything here is conservative and reversible by re-syncing.
8
+ */
9
+
10
+ import type {
11
+ WearableCleanupSettings,
12
+ WearableConversation,
13
+ WearableTranscriptSegment,
14
+ } from "./types.js";
15
+
16
+ export interface CleanupResult {
17
+ conversation: WearableConversation;
18
+ /** Segments removed by the low-quality heuristic. */
19
+ droppedSegments: number;
20
+ /** Segments merged into a predecessor. */
21
+ mergedSegments: number;
22
+ }
23
+
24
+ /** Merge consecutive same-speaker segments when gaps are below this. */
25
+ const MERGE_GAP_MS = 30_000;
26
+
27
+ /**
28
+ * Standalone filler tokens stripped when `stripFillers` is on. Matched
29
+ * case-insensitively on word boundaries, only as whole tokens — "um"
30
+ * inside "umbrella" is never touched. Deliberately short, low-risk
31
+ * list; meaning-bearing hedges ("like", "well") are NOT stripped.
32
+ */
33
+ const FILLER_TOKENS = ["um", "uh", "uhm", "umm", "uhh", "erm", "hmm", "mhm"];
34
+
35
+ const FILLER_PATTERN = new RegExp(
36
+ // Leading/trailing punctuation around the filler collapses with it so
37
+ // "Um, so we should" -> "so we should" rather than ", so we should".
38
+ `(?:^|\\s)(?:${FILLER_TOKENS.join("|")})[,.]?(?=\\s|$)`,
39
+ "gi",
40
+ );
41
+
42
+ /** Apply configured cleanup passes to one conversation. */
43
+ export function cleanConversation(
44
+ conversation: WearableConversation,
45
+ settings: WearableCleanupSettings,
46
+ ): CleanupResult {
47
+ let segments = conversation.segments.map((segment) => ({ ...segment }));
48
+ let droppedSegments = 0;
49
+ let mergedSegments = 0;
50
+
51
+ if (settings.stripFillers) {
52
+ for (const segment of segments) {
53
+ segment.text = stripFillerTokens(segment.text);
54
+ }
55
+ }
56
+
57
+ if (settings.collapseRepeats) {
58
+ for (const segment of segments) {
59
+ segment.text = collapseImmediateRepeats(segment.text);
60
+ }
61
+ }
62
+
63
+ for (const segment of segments) {
64
+ segment.text = normalizeWhitespace(segment.text);
65
+ }
66
+
67
+ if (settings.dropLowQuality) {
68
+ const kept: WearableTranscriptSegment[] = [];
69
+ for (const segment of segments) {
70
+ if (isLowQualitySegment(segment.text)) {
71
+ droppedSegments += 1;
72
+ } else {
73
+ kept.push(segment);
74
+ }
75
+ }
76
+ segments = kept;
77
+ } else {
78
+ // Even without the quality heuristic, segments whose text became
79
+ // empty after filler stripping carry no information.
80
+ const kept = segments.filter((segment) => segment.text.length > 0);
81
+ droppedSegments += segments.length - kept.length;
82
+ segments = kept;
83
+ }
84
+
85
+ if (settings.mergeSameSpeaker) {
86
+ const merged: WearableTranscriptSegment[] = [];
87
+ for (const segment of segments) {
88
+ const previous = merged[merged.length - 1];
89
+ if (previous && canMerge(previous, segment)) {
90
+ previous.text = `${previous.text} ${segment.text}`.trim();
91
+ if (segment.endIso) previous.endIso = segment.endIso;
92
+ mergedSegments += 1;
93
+ } else {
94
+ merged.push(segment);
95
+ }
96
+ }
97
+ segments = merged;
98
+ }
99
+
100
+ return {
101
+ conversation: { ...conversation, segments },
102
+ droppedSegments,
103
+ mergedSegments,
104
+ };
105
+ }
106
+
107
+ function canMerge(
108
+ previous: WearableTranscriptSegment,
109
+ next: WearableTranscriptSegment,
110
+ ): boolean {
111
+ if (previous.speakerKey !== next.speakerKey) return false;
112
+ const previousEnd = previous.endIso ? Date.parse(previous.endIso) : NaN;
113
+ const nextStart = next.startIso ? Date.parse(next.startIso) : NaN;
114
+ // Without timestamps, adjacency is the only signal — still merge.
115
+ if (Number.isNaN(previousEnd) || Number.isNaN(nextStart)) return true;
116
+ return nextStart - previousEnd <= MERGE_GAP_MS;
117
+ }
118
+
119
+ export function stripFillerTokens(text: string): string {
120
+ return normalizeWhitespace(text.replace(FILLER_PATTERN, " "));
121
+ }
122
+
123
+ /**
124
+ * Collapse immediate word/phrase stutters: "I I I think" -> "I think",
125
+ * "we should we should go" -> "we should go". Only collapses *adjacent*
126
+ * repeats (up to 4-word phrases) so intentional repetition across a
127
+ * sentence is preserved.
128
+ */
129
+ export function collapseImmediateRepeats(text: string): string {
130
+ const words = text.split(/\s+/).filter((word) => word.length > 0);
131
+ if (words.length < 2) return text.trim();
132
+ const out: string[] = [];
133
+ let index = 0;
134
+ while (index < words.length) {
135
+ out.push(words[index]);
136
+ index += 1;
137
+ // Greedily consume every adjacent repeat of the phrase that just
138
+ // ended at the output tail (largest phrase first, then re-check so
139
+ // "I I I think" fully collapses to "I think").
140
+ let matched = true;
141
+ while (matched) {
142
+ matched = false;
143
+ for (let size = 4; size >= 1; size--) {
144
+ if (out.length < size || index + size > words.length) continue;
145
+ const tail = out.slice(-size).join(" ").toLowerCase();
146
+ // Spoken digit sequences legitimately repeat ("555 555 1234");
147
+ // never collapse a phrase that carries no letters.
148
+ if (!/\p{L}/u.test(tail)) continue;
149
+ const ahead = words.slice(index, index + size).join(" ").toLowerCase();
150
+ if (tail === ahead) {
151
+ index += size;
152
+ matched = true;
153
+ break;
154
+ }
155
+ }
156
+ }
157
+ }
158
+ return out.join(" ");
159
+ }
160
+
161
+ /**
162
+ * Heuristic ASR-garbage detector. Intentionally conservative: it only
163
+ * drops segments that carry no plausible information.
164
+ */
165
+ export function isLowQualitySegment(text: string): boolean {
166
+ const trimmed = text.trim();
167
+ if (trimmed.length === 0) return true;
168
+ // Single repeated character runs ("aaaaaa", "######").
169
+ if (/^(.)\1{4,}$/.test(trimmed)) return true;
170
+ // Mostly non-letter content with no digits (timestamps/amounts are
171
+ // information; "%$#@!" is not).
172
+ const letters = trimmed.replace(/[^\p{L}\p{N}]/gu, "");
173
+ if (letters.length === 0) return true;
174
+ if (trimmed.length >= 12 && letters.length / trimmed.length < 0.3) {
175
+ return true;
176
+ }
177
+ // One identical token repeated many times ("yeah yeah yeah yeah yeah").
178
+ const words = trimmed.toLowerCase().split(/\s+/);
179
+ if (words.length >= 5) {
180
+ const unique = new Set(words);
181
+ if (unique.size === 1) return true;
182
+ }
183
+ return false;
184
+ }
185
+
186
+ export function normalizeWhitespace(text: string): string {
187
+ return text.replace(/\s+/g, " ").trim();
188
+ }
@@ -0,0 +1,170 @@
1
+ import assert from "node:assert/strict";
2
+ import { test } from "node:test";
3
+
4
+ import { runWearablesCliCommand } from "./cli.js";
5
+ import type { WearablesService } from "./service.js";
6
+
7
+ function makeIo(): {
8
+ io: { stdout: { write(chunk: string): void }; stderr: { write(chunk: string): void } };
9
+ out: string[];
10
+ err: string[];
11
+ } {
12
+ const out: string[] = [];
13
+ const err: string[] = [];
14
+ return {
15
+ io: {
16
+ stdout: { write: (chunk: string) => void out.push(chunk) },
17
+ stderr: { write: (chunk: string) => void err.push(chunk) },
18
+ },
19
+ out,
20
+ err,
21
+ };
22
+ }
23
+
24
+ function stubService(overrides: Partial<Record<keyof WearablesService, unknown>> = {}): WearablesService {
25
+ const base = {
26
+ status: async () => ({
27
+ enabled: true,
28
+ timezone: "UTC",
29
+ sources: [
30
+ {
31
+ source: "limitless",
32
+ displayName: "Limitless Pendant",
33
+ enabled: true,
34
+ connectorInstalled: true,
35
+ memoryMode: "review",
36
+ lastSyncAt: null,
37
+ lastDateSynced: null,
38
+ transcriptDays: 0,
39
+ },
40
+ ],
41
+ connectorsInstalled: ["limitless"],
42
+ }),
43
+ sync: async () => [
44
+ {
45
+ source: "limitless",
46
+ days: ["2026-06-11"],
47
+ conversations: 2,
48
+ segmentsKept: 10,
49
+ segmentsDropped: 1,
50
+ redactions: 1,
51
+ correctionsApplied: 3,
52
+ transcriptsWritten: ["2026-06-11"],
53
+ memoriesCreated: 2,
54
+ memoriesSkipped: 4,
55
+ nativeMemoriesImported: 0,
56
+ warnings: ["something minor"],
57
+ },
58
+ ],
59
+ dayTranscript: async () => [],
60
+ searchTranscripts: async () => [],
61
+ transcriptMemories: async () => [],
62
+ listSpeakers: async () => ({ version: 1 as const, selfName: "Me", speakers: {} }),
63
+ checkAuth: async () => ({ ok: true }),
64
+ };
65
+ return { ...base, ...overrides } as unknown as WearablesService;
66
+ }
67
+
68
+ test("no command prints usage and exits 1; help exits 0", async () => {
69
+ const { io, out } = makeIo();
70
+ assert.equal(await runWearablesCliCommand(stubService(), [], io), 1);
71
+ assert.match(out.join(""), /Usage: wearables/);
72
+ assert.equal(await runWearablesCliCommand(stubService(), ["help"], makeIo().io), 0);
73
+ });
74
+
75
+ test("unknown commands and flags fail with guidance", async () => {
76
+ const { io, err } = makeIo();
77
+ assert.equal(await runWearablesCliCommand(stubService(), ["frobnicate"], io), 1);
78
+ assert.match(err.join(""), /unknown wearables command/);
79
+
80
+ const second = makeIo();
81
+ assert.equal(
82
+ await runWearablesCliCommand(stubService(), ["sync", "--bogus"], second.io),
83
+ 1,
84
+ );
85
+ assert.match(second.err.join(""), /unknown flag '--bogus'/);
86
+ });
87
+
88
+ test("value-taking flags require a value", async () => {
89
+ const { io, err } = makeIo();
90
+ assert.equal(
91
+ await runWearablesCliCommand(stubService(), ["sync", "--source"], io),
92
+ 1,
93
+ );
94
+ assert.match(err.join(""), /--source requires a value/);
95
+
96
+ const second = makeIo();
97
+ assert.equal(
98
+ await runWearablesCliCommand(stubService(), ["search", "x", "--limit", "abc"], second.io),
99
+ 1,
100
+ );
101
+ assert.match(second.err.join(""), /--limit expects a positive integer/);
102
+ });
103
+
104
+ test("status renders sources and respects --json", async () => {
105
+ const { io, out } = makeIo();
106
+ assert.equal(await runWearablesCliCommand(stubService(), ["status"], io), 0);
107
+ const text = out.join("");
108
+ assert.match(text, /Wearables: enabled/);
109
+ assert.match(text, /limitless \(Limitless Pendant\)/);
110
+
111
+ const jsonIo = makeIo();
112
+ assert.equal(await runWearablesCliCommand(stubService(), ["status", "--json"], jsonIo.io), 0);
113
+ const parsed = JSON.parse(jsonIo.out.join(""));
114
+ assert.equal(parsed.enabled, true);
115
+ });
116
+
117
+ test("sync renders the summary including warnings", async () => {
118
+ const { io, out } = makeIo();
119
+ assert.equal(await runWearablesCliCommand(stubService(), ["sync"], io), 0);
120
+ const text = out.join("");
121
+ assert.match(text, /limitless: 2 conversations/);
122
+ assert.match(text, /memories created:\s+2 \(skipped 4\)/);
123
+ assert.match(text, /warning: something minor/);
124
+ assert.match(text, /OK/);
125
+ });
126
+
127
+ test("transcript requires --date and exits 1 when nothing is stored", async () => {
128
+ const { io, err } = makeIo();
129
+ assert.equal(await runWearablesCliCommand(stubService(), ["transcript"], io), 1);
130
+ assert.match(err.join(""), /requires --date/);
131
+
132
+ const second = makeIo();
133
+ assert.equal(
134
+ await runWearablesCliCommand(stubService(), ["transcript", "--date", "2026-06-11"], second.io),
135
+ 1,
136
+ );
137
+ assert.match(second.err.join(""), /No stored transcripts/);
138
+ });
139
+
140
+ test("search flags an index-unavailable scan fallback", async () => {
141
+ const service = stubService({
142
+ searchTranscripts: async () => [
143
+ { source: "limitless", date: "2026-06-10", score: 0, snippet: "…solar…", backend: "scan" as const },
144
+ ],
145
+ });
146
+ const { io, out } = makeIo();
147
+ assert.equal(await runWearablesCliCommand(service, ["search", "solar"], io), 0);
148
+ assert.match(out.join(""), /bounded text scan/);
149
+ });
150
+
151
+ test("check maps auth failures to a non-zero exit", async () => {
152
+ const service = stubService({
153
+ checkAuth: async () => ({ ok: false, detail: "bad key" }),
154
+ });
155
+ const { io, out } = makeIo();
156
+ assert.equal(await runWearablesCliCommand(service, ["check", "limitless"], io), 1);
157
+ assert.match(out.join(""), /FAILED — bad key/);
158
+ });
159
+
160
+ test("backend faults propagate instead of being swallowed as exit codes", async () => {
161
+ const service = stubService({
162
+ status: async () => {
163
+ throw new Error("disk exploded");
164
+ },
165
+ });
166
+ await assert.rejects(
167
+ runWearablesCliCommand(service, ["status"], makeIo().io),
168
+ /disk exploded/,
169
+ );
170
+ });