sonorance 0.1.0-beta.4.1

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 (43) hide show
  1. package/LICENSE +174 -0
  2. package/README.md +103 -0
  3. package/build/icon.png +0 -0
  4. package/package.json +86 -0
  5. package/skill/SKILL.md +41 -0
  6. package/skill/scripts/sonorance.mjs +31 -0
  7. package/src/azure-monitor.mjs +221 -0
  8. package/src/cli.mjs +315 -0
  9. package/src/comment-client.mjs +75 -0
  10. package/src/engine-default.mjs +136 -0
  11. package/src/feedback.mjs +151 -0
  12. package/src/gitignore.mjs +27 -0
  13. package/src/grammar.mjs +55 -0
  14. package/src/identity.mjs +126 -0
  15. package/src/otlp.mjs +166 -0
  16. package/src/plugins/deliberate/contribute.mjs +28 -0
  17. package/src/plugins/deliberate/domain.mjs +40 -0
  18. package/src/plugins/deliberate/frontmatter.mjs +85 -0
  19. package/src/plugins/deliberate/gitignore.mjs +21 -0
  20. package/src/plugins/deliberate/kinds.mjs +44 -0
  21. package/src/plugins/deliberate/markdown.mjs +42 -0
  22. package/src/plugins/deliberate/paths.mjs +245 -0
  23. package/src/plugins/deliberate/stages.mjs +27 -0
  24. package/src/plugins/deliberate/vault.mjs +1043 -0
  25. package/src/plugins.mjs +91 -0
  26. package/src/release-config.mjs +2 -0
  27. package/src/scrubber.mjs +64 -0
  28. package/src/server/index.mjs +993 -0
  29. package/src/sources.mjs +80 -0
  30. package/src/telemetry-schema.mjs +187 -0
  31. package/src/telemetry.mjs +390 -0
  32. package/src/ui/active-line.mjs +42 -0
  33. package/src/ui/app.js +3553 -0
  34. package/src/ui/at-mention.mjs +67 -0
  35. package/src/ui/comments-plugin.mjs +107 -0
  36. package/src/ui/diff-plugin.mjs +73 -0
  37. package/src/ui/editor.mjs +210 -0
  38. package/src/ui/index.html +1723 -0
  39. package/src/ui/md.mjs +233 -0
  40. package/src/ui/paste-md.mjs +54 -0
  41. package/src/ui/shell.html +1374 -0
  42. package/src/ui/slash.mjs +122 -0
  43. package/src/vault-registry.mjs +150 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * plugins.mjs — per-vault PLUGIN loading for the Sonorance app.
3
+ *
4
+ * Sonorance is the platform; capabilities beyond the plain Markdown editor arrive as **plugins** a
5
+ * vault opts into. A plugin is declared in the vault's `.sonorance/plugins.json` and can be enabled
6
+ * or disabled individually:
7
+ *
8
+ * { "plugins": [ { "id": "deliberate", "enabled": true } ] }
9
+ *
10
+ * The engine is built ADDITIVELY: the host always constructs the generic default engine, then, for
11
+ * each enabled plugin, MERGES that plugin's contribution on top (kinds, a record-store overlay,
12
+ * gitignore) — a plugin never replaces the whole engine, and the app's identity stays Sonorance.
13
+ *
14
+ * Plugins ship INSIDE this package and are resolved by `id` from the BUILTIN map below. Vault
15
+ * configuration never loads executable filesystem paths and no plugin can replace the host engine.
16
+ */
17
+ import { readFileSync } from 'node:fs';
18
+ import { join, resolve } from 'node:path';
19
+ import { makeDefaultEngine } from './engine-default.mjs';
20
+
21
+ export const pluginsPath = (root) => join(resolve(root), '.sonorance', 'plugins.json');
22
+
23
+ // Built-in plugins bundled in this package, resolved by id (no path in the vault's config).
24
+ const BUILTIN = {
25
+ deliberate: () => import('./plugins/deliberate/contribute.mjs'),
26
+ };
27
+
28
+ // The vault's declared plugins (in order). Missing / malformed file → none.
29
+ export function listPlugins(root) {
30
+ try { const j = JSON.parse(readFileSync(pluginsPath(root), 'utf8')); return Array.isArray(j.plugins) ? j.plugins : []; }
31
+ catch { return []; }
32
+ }
33
+
34
+ // Layer a plugin's store (which may be a CLASS instance) on top of the base store. A plain spread
35
+ // drops a class instance's prototype methods, so walk the overlay's prototype chain and bind each
36
+ // method to the instance. The base store supplies the generic project/current-pointer machinery
37
+ // (`_current`/`_setCurrent`/…) the engine's resolvers call; the overlay overrides the record,
38
+ // comment and state methods it defines (exactly the surface it owned in the pre-composition model).
39
+ function overlayStore(base, overlay) {
40
+ const merged = { ...base };
41
+ const skip = new Set([...Object.getOwnPropertyNames(Object.prototype), 'constructor']);
42
+ for (let o = overlay; o && o !== Object.prototype; o = Object.getPrototypeOf(o)) {
43
+ for (const key of Object.getOwnPropertyNames(o)) {
44
+ if (skip.has(key)) continue;
45
+ skip.add(key);
46
+ const v = overlay[key];
47
+ merged[key] = typeof v === 'function' ? v.bind(overlay) : v;
48
+ }
49
+ }
50
+ return merged;
51
+ }
52
+
53
+ // Merge one plugin's contribution onto the current engine. Only the fields a plugin supplies are
54
+ // overlaid; everything else (project resolution, current-vault pointer, logger, identity) stays
55
+ // app-owned from the base engine. The record store is layered on top of the base store so generic
56
+ // vault machinery keeps working for a plugin vault.
57
+ function compose(engine, contrib) {
58
+ if (!contrib) return engine;
59
+ return {
60
+ ...engine,
61
+ KINDS: contrib.KINDS ?? engine.KINDS,
62
+ gitignoreEntries: contrib.gitignoreEntries ?? engine.gitignoreEntries,
63
+ openVault: typeof contrib.openVault === 'function'
64
+ ? () => overlayStore(engine.openVault(), contrib.openVault())
65
+ : engine.openVault,
66
+ };
67
+ }
68
+
69
+ // Load a bundled plugin by its stable id.
70
+ async function loadPluginModule(p) {
71
+ if (p.id && BUILTIN[p.id]) return BUILTIN[p.id]();
72
+ return null;
73
+ }
74
+
75
+ // Resolve the engine for a vault: build the generic base engine, then compose every enabled
76
+ // bundled plugin's contribution on top. A plugin that fails to load is skipped with a warning.
77
+ export async function resolveEngine(root) {
78
+ const dir = resolve(root);
79
+ let engine = makeDefaultEngine(dir);
80
+ for (const p of listPlugins(dir)) {
81
+ if (!p || p.enabled === false || !p.id) continue;
82
+ try {
83
+ const mod = await loadPluginModule(p);
84
+ if (!mod) continue;
85
+ if (typeof mod.contribute === 'function') engine = compose(engine, mod.contribute(dir));
86
+ } catch (e) {
87
+ console.error(`sonorance: plugin "${p.id}" failed to load — ${e.message}. Continuing with the base engine.`);
88
+ }
89
+ }
90
+ return engine;
91
+ }
@@ -0,0 +1,2 @@
1
+ // Generated only in the npm release workspace. Do not commit this file.
2
+ export const RELEASE_AZURE_CONNECTION_STRING = "InstrumentationKey=39214618-bcec-469f-8ee4-5476d4f95b81;IngestionEndpoint=https://centralus-2.in.applicationinsights.azure.com/";
@@ -0,0 +1,64 @@
1
+ /**
2
+ * scrubber.mjs — the pattern-based privacy scrubber for the ONE place free text is allowed near
3
+ * the wire: bug-report diagnostics and redacted error stacks (feedback-ux spec §4).
4
+ *
5
+ * It is a defence layer, NOT the primary control. It removes the mechanical leaks a pattern can
6
+ * catch — absolute paths, home dir, emails, URLs with credentials, long tokens/base64/hex,
7
+ * IP/MAC — so a pasted stack or log never carries a filesystem path or secret. It CANNOT catch a
8
+ * semantic leak (a sentence that happens to describe private content); for feedback, the user's
9
+ * own preview/approve step is that control, and telemetry has no free-text field at all.
10
+ *
11
+ * Everything here is unit-tested (`test/telemetry.test.mjs`): each pattern has a positive case.
12
+ */
13
+ import { homedir } from 'node:os';
14
+
15
+ const REDACTED = '⟨redacted⟩';
16
+
17
+ // Order matters: redact structured secrets first, then paths, then bare identifiers.
18
+ const PATTERNS = [
19
+ // emails
20
+ [/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, REDACTED],
21
+ // URLs carrying credentials or query strings (scheme://user:pass@host or ?token=…)
22
+ [/\b[a-z][a-z0-9+.-]*:\/\/[^\s'"]*@[^\s'"]*/gi, REDACTED],
23
+ [/\b(?:token|key|secret|password|pwd|auth|bearer|api[_-]?key)\b\s*[=:]\s*[^\s'"]+/gi, REDACTED],
24
+ // long opaque tokens / base64 / hex blobs (>=24 chars) — API keys, JWTs, hashes
25
+ [/\b[A-Za-z0-9_-]{24,}\b/g, REDACTED],
26
+ [/\b[0-9a-fA-F]{24,}\b/g, REDACTED],
27
+ // JWTs (three base64url segments)
28
+ [/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, REDACTED],
29
+ // IPv4 + MAC addresses
30
+ [/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, REDACTED],
31
+ [/\b(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}\b/g, REDACTED],
32
+ // Windows absolute paths (C:\Users\… )
33
+ [/\b[A-Za-z]:\\[^\s'"]+/g, REDACTED],
34
+ // POSIX absolute paths (/Users/…, /home/…, /var/…) — 2+ segments so a bare "/" survives
35
+ [/(?:^|[\s'"(])(\/[^\s'"/]+){2,}\/?/g, (m) => m.replace(/\/\S+/, ' ' + REDACTED)],
36
+ ];
37
+
38
+ /**
39
+ * Scrub a free-text blob (a pasted log, repro notes, an error message) to a content-free-ish
40
+ * form: home dir → `~`, then every pattern above. Non-strings return ''. Capped so a giant paste
41
+ * can't be used to smuggle content past the length the UI shows.
42
+ */
43
+ export function scrub(text, { max = 4000 } = {}) {
44
+ if (typeof text !== 'string' || !text) return '';
45
+ let s = text.slice(0, max);
46
+ // Collapse the user's home dir first so a path under it becomes `~/…` before path redaction.
47
+ try { const h = homedir(); if (h) s = s.split(h).join('~'); } catch { /* best-effort */ }
48
+ for (const [re, repl] of PATTERNS) s = s.replace(re, repl);
49
+ return s;
50
+ }
51
+
52
+ /**
53
+ * Redact an Error into a content-free diagnostic: the error CLASS (constructor name) plus a
54
+ * scrubbed, path-stripped, top-few-frames stack. NEVER the message (which may quote user content).
55
+ * Returns `{ class, stack }` — the shape the `error` telemetry event + bug diagnostics use.
56
+ */
57
+ export function redactError(err, { frames = 5 } = {}) {
58
+ const cls = (err && (err.name || err.constructor?.name)) || 'Error';
59
+ const raw = (err && typeof err.stack === 'string') ? err.stack : '';
60
+ // Drop the first line (it embeds the message), keep the next `frames` "at …" lines, scrubbed.
61
+ const lines = raw.split('\n').slice(1).filter(l => /^\s*at\s/.test(l)).slice(0, frames);
62
+ const stack = scrub(lines.join('\n'), { max: 2000 });
63
+ return { class: String(cls).slice(0, 60), stack };
64
+ }