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,122 @@
1
+ // Slash commands for the Tiptap surface — type "/" at the start of an empty
2
+ // block to open a menu that inserts headings, lists, a task list, a table, a
3
+ // code block, a quote, or a divider. Built on Tiptap's Suggestion utility with
4
+ // a small self-rendered popup (no tippy dependency), so the block-insertion
5
+ // affordance the hand-rolled editor never had comes for free with the WYSIWYG
6
+ // surface.
7
+ import { Extension } from '@tiptap/core';
8
+ import { Suggestion } from '@tiptap/suggestion';
9
+ import { PluginKey } from '@tiptap/pm/state';
10
+
11
+ // The command palette. `run({ editor, range })` replaces the typed "/query"
12
+ // (deleteRange) then applies the block transform.
13
+ const COMMANDS = [
14
+ { title: 'Heading 1', keys: 'h1 heading title', run: ({ editor, range }) => editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run() },
15
+ { title: 'Heading 2', keys: 'h2 heading subtitle', run: ({ editor, range }) => editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run() },
16
+ { title: 'Heading 3', keys: 'h3 heading', run: ({ editor, range }) => editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run() },
17
+ { title: 'Bullet list', keys: 'ul bullet unordered list', run: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleBulletList().run() },
18
+ { title: 'Numbered list', keys: 'ol ordered numbered list', run: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleOrderedList().run() },
19
+ { title: 'Task list', keys: 'todo task checkbox check', run: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleTaskList().run() },
20
+ { title: 'Table', keys: 'table grid', run: ({ editor, range }) => editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
21
+ { title: 'Code block', keys: 'code pre snippet', run: ({ editor, range }) => editor.chain().focus().deleteRange(range).setCodeBlock().run() },
22
+ { title: 'Quote', keys: 'quote blockquote', run: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleBlockquote().run() },
23
+ { title: 'Divider', keys: 'hr rule divider separator', run: ({ editor, range }) => editor.chain().focus().deleteRange(range).setHorizontalRule().run() },
24
+ ];
25
+
26
+ function filter(query) {
27
+ const q = (query || '').toLowerCase().trim();
28
+ if (!q) return COMMANDS.slice(0, 10);
29
+ return COMMANDS.filter(c => (c.title + ' ' + c.keys).toLowerCase().includes(q)).slice(0, 10);
30
+ }
31
+
32
+ // The self-rendered popup — a plain list positioned at the caret. Returns the
33
+ // Suggestion render hooks. Kept dependency-free (no floating-ui/tippy) so it
34
+ // adds nothing to the bundle beyond the Suggestion utility itself. Shared with
35
+ // the "@" mention (at-mention.mjs) so both palettes look and behave identically.
36
+ export function makeRenderer() {
37
+ let el = null, items = [], active = 0, cmd = null;
38
+
39
+ const paint = () => {
40
+ if (!el) return;
41
+ el.innerHTML = '';
42
+ if (!items.length) { el.classList.remove('show'); return; }
43
+ el.classList.add('show');
44
+ items.forEach((it, i) => {
45
+ const b = document.createElement('button');
46
+ b.type = 'button';
47
+ b.className = 'slashitem' + (i === active ? ' active' : '');
48
+ b.textContent = it.title;
49
+ b.addEventListener('mousedown', (e) => { e.preventDefault(); pick(i); });
50
+ el.appendChild(b);
51
+ });
52
+ };
53
+
54
+ const place = (props) => {
55
+ if (!el) return;
56
+ let rect = null;
57
+ try { rect = props.clientRect && props.clientRect(); } catch { rect = null; }
58
+ const vw = window.innerWidth || 1200, vh = window.innerHeight || 800;
59
+ const w = el.offsetWidth || 220, h = el.offsetHeight || 240;
60
+ const left = rect ? Math.min(rect.left, vw - w - 8) : 40;
61
+ const top = rect ? Math.min(rect.bottom + 6, vh - h - 8) : 80;
62
+ el.style.left = Math.max(8, left) + 'px';
63
+ el.style.top = Math.max(8, top) + 'px';
64
+ };
65
+
66
+ const pick = (i) => {
67
+ const it = items[i]; if (!it || !cmd) return;
68
+ cmd(it);
69
+ };
70
+
71
+ return {
72
+ onStart(props) {
73
+ cmd = props.command; items = props.items; active = 0;
74
+ el = document.createElement('div');
75
+ el.className = 'slashmenu';
76
+ el.setAttribute('role', 'listbox');
77
+ document.body.appendChild(el);
78
+ paint(); place(props);
79
+ },
80
+ onUpdate(props) {
81
+ cmd = props.command; items = props.items; if (active >= items.length) active = 0;
82
+ paint(); place(props);
83
+ },
84
+ onKeyDown(props) {
85
+ const k = props.event.key;
86
+ if (k === 'Escape') { destroy(); return true; }
87
+ if (!items.length) return false;
88
+ if (k === 'ArrowDown') { active = (active + 1) % items.length; paint(); return true; }
89
+ if (k === 'ArrowUp') { active = (active - 1 + items.length) % items.length; paint(); return true; }
90
+ if (k === 'Enter') { pick(active); return true; }
91
+ return false;
92
+ },
93
+ onExit() { destroy(); },
94
+ };
95
+
96
+ function destroy() { if (el) { el.remove(); el = null; } items = []; }
97
+ }
98
+
99
+ export const SlashCommands = Extension.create({
100
+ name: 'slashCommands',
101
+ addProseMirrorPlugins() {
102
+ return [
103
+ Suggestion({
104
+ editor: this.editor,
105
+ pluginKey: new PluginKey('slashSuggestion'),
106
+ char: '/',
107
+ startOfLine: false,
108
+ allowSpaces: false,
109
+ // Only trigger at the start of an empty-ish block, so "/" inside prose
110
+ // (e.g. and/or, a path) never pops the menu.
111
+ allow: ({ state, range }) => {
112
+ const $from = state.doc.resolve(range.from);
113
+ const before = $from.parent.textBetween(0, $from.parentOffset, undefined, ' ');
114
+ return /(^|\s)\/?$/.test(before) && $from.parent.type.name === 'paragraph';
115
+ },
116
+ items: ({ query }) => filter(query),
117
+ command: ({ editor, range, props }) => props.run({ editor, range }),
118
+ render: makeRenderer,
119
+ }),
120
+ ];
121
+ },
122
+ });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * vault-registry.mjs — the Sonorance multi-vault registry.
3
+ *
4
+ * A **vault** is a plain folder that is its own source of truth; opening one marks it with a
5
+ * hidden `.sonorance/` platform dir whose single `config.json` holds everything per-vault:
6
+ * identity (id/name/created_at), the optional engine pointer (flavor), grounding sources, and
7
+ * workbench state (tabs/explorer). This module owns the ONE global list of known vaults and
8
+ * which is current, stored at `~/.sonorance/config.json` (override the home with
9
+ * `SONORANCE_HOME`, e.g. in tests).
10
+ *
11
+ * It is the platform's registry, not a product's: the standalone Sonorance editor uses it to
12
+ * open and switch between folders, and the `deliberate` host reuses it for its projects (a
13
+ * Deliberate vault is just a folder that also carries `deliberate/` content). Because it is a
14
+ * shared seam, both sides read/write the SAME `~/.sonorance/config.json`.
15
+ *
16
+ * Design:
17
+ * • Vaults are keyed by ABSOLUTE PATH (dedup by path, not by a name-slug that can collide).
18
+ * • Identity (id/name) is READ from each vault's `.sonorance/config.json`; a live folder that
19
+ * lacks one is SELF-HEALED (the identity is written from the basename) rather than dropped —
20
+ * so a registered folder never silently disappears from the switcher.
21
+ * • A registered folder whose directory no longer exists is reported with `exists:false` (the
22
+ * UI offers Remove); it is never hidden silently.
23
+ * • Only the `vaults` + `current` keys are owned in the GLOBAL config; the per-vault config's
24
+ * other keys (sources, tabs, engine, …) are preserved on write.
25
+ */
26
+ import { homedir } from 'node:os';
27
+ import { join, resolve, dirname, basename } from 'node:path';
28
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from 'node:fs';
29
+
30
+ // The single platform home. `SONORANCE_HOME` overrides it (tests point it at a temp dir so the
31
+ // suite never touches the real `~/.sonorance`).
32
+ export const sonoranceHome = () => process.env.SONORANCE_HOME || join(homedir(), '.sonorance');
33
+ export const registryConfigPath = () => join(sonoranceHome(), 'config.json');
34
+
35
+ const slug = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 60) || 'project';
36
+ const stateDir = (dir) => join(dir, '.sonorance');
37
+ // A vault's single per-vault config file — identity + sources + engine + workbench state.
38
+ export const vaultConfigPath = (dir) => join(stateDir(dir), 'config.json');
39
+
40
+ const readConfig = () => { try { return JSON.parse(readFileSync(registryConfigPath(), 'utf8')); } catch { return {}; } };
41
+ const writeConfig = (c) => {
42
+ const p = registryConfigPath();
43
+ mkdirSync(dirname(p), { recursive: true });
44
+ const tmp = `${p}.tmp-${process.pid}`;
45
+ writeFileSync(tmp, JSON.stringify(c, null, 2) + '\n');
46
+ renameSync(tmp, p); // atomic: readers see the old or the new whole
47
+ };
48
+
49
+ // A vault's per-vault config (`.sonorance/config.json`) — the ONE file per vault. Read-modify-write
50
+ // preserves keys other writers own (the engine's tabs/explorer, the host's sources/gate/engine).
51
+ const readVaultConfig = (dir) => { try { return JSON.parse(readFileSync(vaultConfigPath(dir), 'utf8')); } catch { return {}; } };
52
+ const writeVaultConfig = (dir, cfg) => {
53
+ const p = vaultConfigPath(dir);
54
+ mkdirSync(dirname(p), { recursive: true });
55
+ const tmp = `${p}.tmp-${process.pid}`;
56
+ writeFileSync(tmp, JSON.stringify(cfg, null, 2) + '\n');
57
+ renameSync(tmp, p);
58
+ };
59
+
60
+ // Read `id`/`name` from a vault's `.sonorance/config.json`. Absent → {}.
61
+ function readMeta(dir) {
62
+ const c = readVaultConfig(dir);
63
+ return { id: c.id, name: c.name };
64
+ }
65
+
66
+ // Ensure the vault has identity in its config — this is what makes a folder a project. Called on
67
+ // register and as SELF-HEAL when a live folder is missing it. Never overwrites an existing id;
68
+ // merges into whatever else the config already holds (sources, tabs, engine, …).
69
+ function ensureMarker(dir, name) {
70
+ const c = readVaultConfig(dir);
71
+ if (c.id) return;
72
+ const nm = name || basename(dir) || 'project';
73
+ c.id = slug(nm);
74
+ if (!c.name) c.name = nm;
75
+ if (c.created_at == null) c.created_at = Date.now();
76
+ writeVaultConfig(dir, c);
77
+ }
78
+
79
+ const identityOf = (dir) => {
80
+ const meta = existsSync(dir) ? readMeta(dir) : {};
81
+ return { id: meta.id || slug(basename(dir)), name: meta.name || basename(dir) || 'project', dir };
82
+ };
83
+
84
+ export function createRegistry() {
85
+ const vaultDirs = () => { const c = readConfig(); return Array.isArray(c.vaults) ? c.vaults : []; };
86
+
87
+ const reg = {
88
+ home: sonoranceHome,
89
+ configPath: registryConfigPath,
90
+
91
+ // Every known vault: { id, name, dir, exists }. A live folder missing its marker is healed
92
+ // (marker recreated from the basename) so it keeps a stable identity and never drops out.
93
+ list() {
94
+ return vaultDirs().map(dir => {
95
+ const exists = existsSync(dir);
96
+ if (exists) ensureMarker(dir); // self-heal a live folder that lost its marker
97
+ return { ...identityOf(dir), exists };
98
+ });
99
+ },
100
+ // Resolve a vault by absolute path OR by id. Null if not registered.
101
+ get(idOrDir) {
102
+ if (!idOrDir) return null;
103
+ const abs = resolve(String(idOrDir));
104
+ return this.list().find(v => v.dir === abs || v.id === idOrDir) || null;
105
+ },
106
+ // The current vault. Falls back to the first EXISTING vault so a fresh/stale pointer still
107
+ // resolves to something usable.
108
+ current() {
109
+ const c = readConfig();
110
+ const list = this.list();
111
+ return (c.current && list.find(v => v.dir === c.current)) || list.find(v => v.exists) || null;
112
+ },
113
+ setCurrent(idOrDir) {
114
+ const v = this.get(idOrDir); if (!v) return null;
115
+ const c = readConfig(); c.current = v.dir; writeConfig(c);
116
+ return v;
117
+ },
118
+ // Read-only id → dir resolution (no self-heal write) — for hot path resolvers that must
119
+ // stay cheap and side-effect-free (e.g. Deliberate's `vaultPath`). Null if unregistered.
120
+ dirById(id) {
121
+ if (!id) return null;
122
+ for (const dir of vaultDirs()) { if (identityOf(dir).id === id) return dir; }
123
+ return null;
124
+ },
125
+ // Register a folder as a vault (creating its `.sonorance/` marker) and, by default, make it
126
+ // current — the "open folder" / "open another vault" action. Idempotent per absolute path.
127
+ register(dir, { name, makeCurrent = true } = {}) {
128
+ const abs = resolve(dir);
129
+ ensureMarker(abs, name);
130
+ const c = readConfig();
131
+ c.vaults = Array.isArray(c.vaults) ? c.vaults : [];
132
+ if (!c.vaults.includes(abs)) c.vaults.push(abs);
133
+ if (makeCurrent || !c.current) c.current = abs;
134
+ writeConfig(c);
135
+ return this.get(abs);
136
+ },
137
+ // Drop a vault from the registry (the UI's Remove for a missing folder). Does NOT delete the
138
+ // folder or its `.sonorance/`; only forgets it. Re-points `current` if it was the removed one.
139
+ remove(idOrDir) {
140
+ const v = this.get(idOrDir);
141
+ const abs = v ? v.dir : resolve(String(idOrDir));
142
+ const c = readConfig();
143
+ c.vaults = (Array.isArray(c.vaults) ? c.vaults : []).filter(d => d !== abs);
144
+ if (c.current === abs) c.current = c.vaults[0] || null;
145
+ writeConfig(c);
146
+ return true;
147
+ },
148
+ };
149
+ return reg;
150
+ }