synap 0.6.0 → 0.7.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.
- package/.claude/skills/synap-assistant/SKILL.md +37 -2
- package/package.json +1 -1
- package/src/cli.js +167 -14
- package/src/preferences.js +181 -2
|
@@ -39,8 +39,11 @@ When assisting users with their synap entries:
|
|
|
39
39
|
synap stores long-term user preferences at `~/.config/synap/user-preferences.md`.
|
|
40
40
|
|
|
41
41
|
- Read preferences at the start of a session when present.
|
|
42
|
-
-
|
|
43
|
-
-
|
|
42
|
+
- Prefer idempotent updates with `synap preferences set --section "Tag Meanings" --entry "#urgent = must do today"`.
|
|
43
|
+
- Remove entries with `synap preferences remove --section tags --match "urgent"`.
|
|
44
|
+
- List entries with `synap preferences list --section tags --json`.
|
|
45
|
+
- `synap preferences --append "## Section" "..."` is still supported for raw appends.
|
|
46
|
+
- Avoid overwriting user-written content; prefer section-based updates.
|
|
44
47
|
|
|
45
48
|
## Operating Modes
|
|
46
49
|
|
|
@@ -79,6 +82,7 @@ Detect user intent and respond appropriately:
|
|
|
79
82
|
| Get stats | `synap stats` |
|
|
80
83
|
| Setup wizard | `synap setup` |
|
|
81
84
|
| Edit preferences | `synap preferences --edit` |
|
|
85
|
+
| Set preference | `synap preferences set --section tags --entry "#urgent = must do today"` |
|
|
82
86
|
|
|
83
87
|
## Pre-flight Check
|
|
84
88
|
|
|
@@ -435,6 +439,37 @@ After capture sessions, detect opportunities to group related entries:
|
|
|
435
439
|
- `synap add "[Topic] Project" --type project --tags "topic"`
|
|
436
440
|
- For each child: `synap link <child-id> <project-id> --as-parent`
|
|
437
441
|
|
|
442
|
+
### Daily Tracking Pattern
|
|
443
|
+
|
|
444
|
+
For projects requiring ongoing progress logging (standups, journals, learning logs):
|
|
445
|
+
|
|
446
|
+
| Signal | Action |
|
|
447
|
+
|--------|--------|
|
|
448
|
+
| Project mentions "daily", "track progress", "standup" | Suggest daily tracking setup |
|
|
449
|
+
| User says "I need to log progress on X" | Explain `synap log` workflow |
|
|
450
|
+
| Project has `--tags daily-tracking` | Ask for today's update |
|
|
451
|
+
|
|
452
|
+
**Daily tracking workflow:**
|
|
453
|
+
|
|
454
|
+
1. **Setup:** Create a project to track
|
|
455
|
+
```bash
|
|
456
|
+
synap add "Learn Rust" --type project --tags "learning,daily-tracking"
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
2. **Daily logging:** Add timestamped progress
|
|
460
|
+
```bash
|
|
461
|
+
synap log <project-id> "Completed chapter 3"
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
3. **Review progress:** View the log tree
|
|
465
|
+
```bash
|
|
466
|
+
synap tree <project-id>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**When to suggest:** User creates learning/progress project, mentions accountability, or asks about daily tracking.
|
|
470
|
+
|
|
471
|
+
**Do NOT auto-create** - always confirm with user first.
|
|
472
|
+
|
|
438
473
|
## Classification Rules
|
|
439
474
|
|
|
440
475
|
### Type Detection Heuristics
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1749,7 +1749,16 @@ async function main() {
|
|
|
1749
1749
|
// PREFERENCES COMMANDS
|
|
1750
1750
|
// ============================================
|
|
1751
1751
|
|
|
1752
|
-
|
|
1752
|
+
const respondPreferencesError = (options, message, code = 'INVALID_ARGS') => {
|
|
1753
|
+
if (options.json) {
|
|
1754
|
+
console.log(JSON.stringify({ success: false, error: message, code }));
|
|
1755
|
+
} else {
|
|
1756
|
+
console.error(chalk.red(message));
|
|
1757
|
+
}
|
|
1758
|
+
process.exit(1);
|
|
1759
|
+
};
|
|
1760
|
+
|
|
1761
|
+
const preferencesCommand = program
|
|
1753
1762
|
.command('preferences')
|
|
1754
1763
|
.description('View or update user preferences')
|
|
1755
1764
|
.option('--edit', 'Open preferences in $EDITOR')
|
|
@@ -1761,22 +1770,13 @@ async function main() {
|
|
|
1761
1770
|
const hasAppend = Array.isArray(options.append);
|
|
1762
1771
|
const activeFlags = [options.edit, options.reset, hasAppend].filter(Boolean).length;
|
|
1763
1772
|
|
|
1764
|
-
const respondError = (message, code = 'INVALID_ARGS') => {
|
|
1765
|
-
if (options.json) {
|
|
1766
|
-
console.log(JSON.stringify({ success: false, error: message, code }));
|
|
1767
|
-
} else {
|
|
1768
|
-
console.error(chalk.red(message));
|
|
1769
|
-
}
|
|
1770
|
-
process.exit(1);
|
|
1771
|
-
};
|
|
1772
|
-
|
|
1773
1773
|
if (activeFlags > 1) {
|
|
1774
|
-
|
|
1774
|
+
respondPreferencesError(options, 'Use only one of --edit, --reset, or --append');
|
|
1775
1775
|
}
|
|
1776
1776
|
try {
|
|
1777
1777
|
if (options.edit) {
|
|
1778
1778
|
if (options.json) {
|
|
1779
|
-
|
|
1779
|
+
respondPreferencesError(options, 'Cannot use --json with --edit', 'INVALID_MODE');
|
|
1780
1780
|
}
|
|
1781
1781
|
|
|
1782
1782
|
preferences.loadPreferences();
|
|
@@ -1832,7 +1832,7 @@ async function main() {
|
|
|
1832
1832
|
if (hasAppend) {
|
|
1833
1833
|
const values = options.append || [];
|
|
1834
1834
|
if (values.length < 2) {
|
|
1835
|
-
|
|
1835
|
+
respondPreferencesError(options, 'Usage: synap preferences --append "## Section" "Text to append"');
|
|
1836
1836
|
}
|
|
1837
1837
|
|
|
1838
1838
|
const [section, ...textParts] = values;
|
|
@@ -1865,7 +1865,160 @@ async function main() {
|
|
|
1865
1865
|
console.log(content);
|
|
1866
1866
|
}
|
|
1867
1867
|
} catch (err) {
|
|
1868
|
-
|
|
1868
|
+
respondPreferencesError(options, err.message || 'Failed to update preferences', 'PREFERENCES_ERROR');
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
|
|
1872
|
+
preferencesCommand
|
|
1873
|
+
.command('set')
|
|
1874
|
+
.description('Add a preference entry to a section')
|
|
1875
|
+
.option('--section <section>', 'Section name or alias')
|
|
1876
|
+
.option('--entry <entry>', 'Entry text')
|
|
1877
|
+
.option('--json', 'Output as JSON')
|
|
1878
|
+
.action((options) => {
|
|
1879
|
+
if (!options.section || !options.entry) {
|
|
1880
|
+
respondPreferencesError(
|
|
1881
|
+
options,
|
|
1882
|
+
'Usage: synap preferences set --section "tags" --entry "text to add"'
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
try {
|
|
1887
|
+
const result = preferences.setEntry(options.section, options.entry);
|
|
1888
|
+
const payload = { ...result, path: preferences.getPreferencesPath() };
|
|
1889
|
+
|
|
1890
|
+
if (options.json) {
|
|
1891
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1892
|
+
} else if (result.added) {
|
|
1893
|
+
console.log(chalk.green(`Added entry to ${result.section}`));
|
|
1894
|
+
} else {
|
|
1895
|
+
console.log(chalk.yellow(`Entry already exists in ${result.section}`));
|
|
1896
|
+
}
|
|
1897
|
+
} catch (err) {
|
|
1898
|
+
respondPreferencesError(options, err.message || 'Failed to set preferences', 'PREFERENCES_ERROR');
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
preferencesCommand
|
|
1903
|
+
.command('remove')
|
|
1904
|
+
.description('Remove preference entries from a section')
|
|
1905
|
+
.option('--section <section>', 'Section name or alias')
|
|
1906
|
+
.option('--match <pattern>', 'Match text (case-insensitive substring)')
|
|
1907
|
+
.option('--entry <entry>', 'Exact entry text')
|
|
1908
|
+
.option('--json', 'Output as JSON')
|
|
1909
|
+
.action((options) => {
|
|
1910
|
+
if (!options.section) {
|
|
1911
|
+
respondPreferencesError(
|
|
1912
|
+
options,
|
|
1913
|
+
'Usage: synap preferences remove --section "tags" --match "urgent"'
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
if (options.match && options.entry) {
|
|
1917
|
+
respondPreferencesError(options, 'Use only one of --match or --entry');
|
|
1918
|
+
}
|
|
1919
|
+
if (!options.match && !options.entry) {
|
|
1920
|
+
respondPreferencesError(
|
|
1921
|
+
options,
|
|
1922
|
+
'Usage: synap preferences remove --section "tags" --match "urgent"'
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
try {
|
|
1927
|
+
const result = preferences.removeFromSection(options.section, {
|
|
1928
|
+
match: options.match,
|
|
1929
|
+
entry: options.entry
|
|
1930
|
+
});
|
|
1931
|
+
const payload = { ...result, path: preferences.getPreferencesPath() };
|
|
1932
|
+
|
|
1933
|
+
if (options.json) {
|
|
1934
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1935
|
+
} else if (result.removed) {
|
|
1936
|
+
console.log(chalk.green(`Removed ${result.count} entr${result.count === 1 ? 'y' : 'ies'} from ${result.section}`));
|
|
1937
|
+
} else {
|
|
1938
|
+
console.log(chalk.yellow(`No entries matched in ${result.section}`));
|
|
1939
|
+
}
|
|
1940
|
+
} catch (err) {
|
|
1941
|
+
respondPreferencesError(options, err.message || 'Failed to remove preferences', 'PREFERENCES_ERROR');
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
|
|
1945
|
+
preferencesCommand
|
|
1946
|
+
.command('list')
|
|
1947
|
+
.description('List preference entries')
|
|
1948
|
+
.option('--section <section>', 'Section name or alias')
|
|
1949
|
+
.option('--json', 'Output as JSON')
|
|
1950
|
+
.action((options) => {
|
|
1951
|
+
try {
|
|
1952
|
+
if (options.section) {
|
|
1953
|
+
const section = preferences.resolveSection(options.section);
|
|
1954
|
+
const entries = preferences.getEntriesInSection(section);
|
|
1955
|
+
const payload = { section, entries, count: entries.length };
|
|
1956
|
+
|
|
1957
|
+
if (options.json) {
|
|
1958
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1959
|
+
} else if (entries.length === 0) {
|
|
1960
|
+
console.log(chalk.gray(`No entries found in ${section}`));
|
|
1961
|
+
} else {
|
|
1962
|
+
console.log(chalk.bold(section));
|
|
1963
|
+
entries.forEach((entry) => {
|
|
1964
|
+
console.log(`- ${entry}`);
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
return;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
const content = preferences.loadPreferences();
|
|
1971
|
+
const lines = content.split(/\r?\n/);
|
|
1972
|
+
const sections = [];
|
|
1973
|
+
let current = null;
|
|
1974
|
+
|
|
1975
|
+
for (const line of lines) {
|
|
1976
|
+
const headingMatch = line.match(/^(#{1,6})\s*(.+?)\s*$/);
|
|
1977
|
+
if (headingMatch) {
|
|
1978
|
+
const level = headingMatch[1].length;
|
|
1979
|
+
const name = headingMatch[2].trim();
|
|
1980
|
+
|
|
1981
|
+
if (level === 2) {
|
|
1982
|
+
current = { section: name, entries: [] };
|
|
1983
|
+
sections.push(current);
|
|
1984
|
+
} else if (level === 1) {
|
|
1985
|
+
current = null;
|
|
1986
|
+
}
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
if (!current) {
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
const trimmed = line.trim();
|
|
1995
|
+
if (!trimmed || trimmed.startsWith('<!--') || /^(#{1,6})\s+/.test(trimmed)) {
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
current.entries.push(trimmed);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
const populatedSections = sections.filter(section => section.entries.length > 0);
|
|
2003
|
+
const totalEntries = populatedSections.reduce((sum, section) => sum + section.entries.length, 0);
|
|
2004
|
+
|
|
2005
|
+
if (options.json) {
|
|
2006
|
+
console.log(JSON.stringify({ sections: populatedSections, count: totalEntries }, null, 2));
|
|
2007
|
+
} else if (populatedSections.length === 0) {
|
|
2008
|
+
console.log(chalk.gray('No preference entries found'));
|
|
2009
|
+
} else {
|
|
2010
|
+
populatedSections.forEach((section, index) => {
|
|
2011
|
+
if (index > 0) {
|
|
2012
|
+
console.log('');
|
|
2013
|
+
}
|
|
2014
|
+
console.log(chalk.bold(section.section));
|
|
2015
|
+
section.entries.forEach((entry) => {
|
|
2016
|
+
console.log(`- ${entry}`);
|
|
2017
|
+
});
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
} catch (err) {
|
|
2021
|
+
respondPreferencesError(options, err.message || 'Failed to list preferences', 'PREFERENCES_ERROR');
|
|
1869
2022
|
}
|
|
1870
2023
|
});
|
|
1871
2024
|
|
package/src/preferences.js
CHANGED
|
@@ -9,6 +9,17 @@ const storage = require('./storage');
|
|
|
9
9
|
const TEMPLATE_PATH = path.join(__dirname, 'templates', 'user-preferences-template.md');
|
|
10
10
|
const PREFERENCES_FILE = path.join(storage.CONFIG_DIR, 'user-preferences.md');
|
|
11
11
|
const MAX_LINES = 500;
|
|
12
|
+
const SECTION_ALIASES = new Map([
|
|
13
|
+
['about', 'About Me'],
|
|
14
|
+
['me', 'About Me'],
|
|
15
|
+
['projects', 'Important Projects'],
|
|
16
|
+
['important', 'Important Projects'],
|
|
17
|
+
['tags', 'Tag Meanings'],
|
|
18
|
+
['tag', 'Tag Meanings'],
|
|
19
|
+
['review', 'Review Preferences'],
|
|
20
|
+
['behavior', 'Behavioral Preferences'],
|
|
21
|
+
['behavioral', 'Behavioral Preferences']
|
|
22
|
+
]);
|
|
12
23
|
|
|
13
24
|
function ensureConfigDir() {
|
|
14
25
|
if (!fs.existsSync(storage.CONFIG_DIR)) {
|
|
@@ -79,7 +90,16 @@ function resetPreferences() {
|
|
|
79
90
|
return savePreferences(template);
|
|
80
91
|
}
|
|
81
92
|
|
|
93
|
+
function resolveSection(input) {
|
|
94
|
+
const target = parseSectionTarget(input);
|
|
95
|
+
const key = target.name.toLowerCase();
|
|
96
|
+
return SECTION_ALIASES.get(key) || target.name;
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
function parseSectionTarget(section) {
|
|
100
|
+
if (typeof section !== 'string') {
|
|
101
|
+
throw new Error('Section name is required');
|
|
102
|
+
}
|
|
83
103
|
const trimmed = section.trim();
|
|
84
104
|
if (!trimmed) {
|
|
85
105
|
throw new Error('Section name is required');
|
|
@@ -93,9 +113,22 @@ function parseSectionTarget(section) {
|
|
|
93
113
|
return { level: null, name: trimmed };
|
|
94
114
|
}
|
|
95
115
|
|
|
116
|
+
function normalizeEntry(entry) {
|
|
117
|
+
return entry.trim();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isCommentLine(line) {
|
|
121
|
+
const trimmed = line.trim();
|
|
122
|
+
return trimmed.startsWith('<!--');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isHeadingLine(line) {
|
|
126
|
+
return /^(#{1,6})\s+/.test(line.trim());
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
function findSection(lines, target) {
|
|
97
130
|
for (let i = 0; i < lines.length; i += 1) {
|
|
98
|
-
const match = lines[i].match(/^(#{1,6})\s
|
|
131
|
+
const match = lines[i].match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
99
132
|
if (!match) {
|
|
100
133
|
continue;
|
|
101
134
|
}
|
|
@@ -113,6 +146,148 @@ function findSection(lines, target) {
|
|
|
113
146
|
return null;
|
|
114
147
|
}
|
|
115
148
|
|
|
149
|
+
function getSectionRange(lines, match) {
|
|
150
|
+
if (!match) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let endIndex = lines.length;
|
|
155
|
+
for (let i = match.index + 1; i < lines.length; i += 1) {
|
|
156
|
+
const headingMatch = lines[i].match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
157
|
+
if (!headingMatch) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const level = headingMatch[1].length;
|
|
161
|
+
if (level <= match.level) {
|
|
162
|
+
endIndex = i;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { start: match.index, end: endIndex };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getEntriesInSection(section) {
|
|
171
|
+
const resolved = resolveSection(section);
|
|
172
|
+
const content = loadPreferences();
|
|
173
|
+
const lines = content.split(/\r?\n/);
|
|
174
|
+
const target = parseSectionTarget(resolved);
|
|
175
|
+
const match = findSection(lines, target);
|
|
176
|
+
|
|
177
|
+
if (!match) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const range = getSectionRange(lines, match);
|
|
182
|
+
const entries = [];
|
|
183
|
+
|
|
184
|
+
for (let i = match.index + 1; i < range.end; i += 1) {
|
|
185
|
+
const line = lines[i];
|
|
186
|
+
const trimmed = line.trim();
|
|
187
|
+
|
|
188
|
+
if (!trimmed || isCommentLine(trimmed) || isHeadingLine(trimmed)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
entries.push(trimmed);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return entries;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function setEntry(section, entry) {
|
|
199
|
+
if (typeof entry !== 'string' || !entry.trim()) {
|
|
200
|
+
throw new Error('Entry text is required');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const resolved = resolveSection(section);
|
|
204
|
+
const normalized = normalizeEntry(entry);
|
|
205
|
+
const entries = getEntriesInSection(resolved);
|
|
206
|
+
const existed = entries.some((existing) => existing === normalized);
|
|
207
|
+
|
|
208
|
+
if (existed) {
|
|
209
|
+
return {
|
|
210
|
+
added: false,
|
|
211
|
+
existed: true,
|
|
212
|
+
section: resolved,
|
|
213
|
+
entry: normalized
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
appendToSection(resolved, normalized);
|
|
218
|
+
return {
|
|
219
|
+
added: true,
|
|
220
|
+
existed: false,
|
|
221
|
+
section: resolved,
|
|
222
|
+
entry: normalized
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function removeFromSection(section, { match, entry } = {}) {
|
|
227
|
+
const hasMatch = typeof match === 'string' && match.trim();
|
|
228
|
+
const hasEntry = typeof entry === 'string' && entry.trim();
|
|
229
|
+
|
|
230
|
+
if (!hasMatch && !hasEntry) {
|
|
231
|
+
throw new Error('Match or entry is required');
|
|
232
|
+
}
|
|
233
|
+
if (hasMatch && hasEntry) {
|
|
234
|
+
throw new Error('Use either match or entry, not both');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const resolved = resolveSection(section);
|
|
238
|
+
const content = loadPreferences();
|
|
239
|
+
const lines = content.split(/\r?\n/);
|
|
240
|
+
const target = parseSectionTarget(resolved);
|
|
241
|
+
const sectionMatch = findSection(lines, target);
|
|
242
|
+
|
|
243
|
+
if (!sectionMatch) {
|
|
244
|
+
return { removed: false, count: 0, entries: [], section: resolved };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const range = getSectionRange(lines, sectionMatch);
|
|
248
|
+
const sectionLines = lines.slice(sectionMatch.index + 1, range.end);
|
|
249
|
+
const removedEntries = [];
|
|
250
|
+
const normalizedEntry = hasEntry ? normalizeEntry(entry) : null;
|
|
251
|
+
const matchNeedle = hasMatch ? match.trim().toLowerCase() : null;
|
|
252
|
+
|
|
253
|
+
const updatedSectionLines = sectionLines.filter((line) => {
|
|
254
|
+
const trimmed = line.trim();
|
|
255
|
+
|
|
256
|
+
if (!trimmed || isCommentLine(trimmed) || isHeadingLine(trimmed)) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const matches = normalizedEntry
|
|
261
|
+
? trimmed === normalizedEntry
|
|
262
|
+
: trimmed.toLowerCase().includes(matchNeedle);
|
|
263
|
+
|
|
264
|
+
if (matches) {
|
|
265
|
+
removedEntries.push(trimmed);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return true;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (removedEntries.length === 0) {
|
|
273
|
+
return { removed: false, count: 0, entries: [], section: resolved };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const updatedLines = [
|
|
277
|
+
...lines.slice(0, sectionMatch.index + 1),
|
|
278
|
+
...updatedSectionLines,
|
|
279
|
+
...lines.slice(range.end)
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
savePreferences(updatedLines.join('\n'));
|
|
283
|
+
return {
|
|
284
|
+
removed: true,
|
|
285
|
+
count: removedEntries.length,
|
|
286
|
+
entries: removedEntries,
|
|
287
|
+
section: resolved
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
116
291
|
function appendToSection(section, text) {
|
|
117
292
|
if (typeof text !== 'string' || !text.trim()) {
|
|
118
293
|
throw new Error('Append text is required');
|
|
@@ -140,7 +315,7 @@ function appendToSection(section, text) {
|
|
|
140
315
|
|
|
141
316
|
let insertIndex = lines.length;
|
|
142
317
|
for (let i = match.index + 1; i < lines.length; i += 1) {
|
|
143
|
-
const headingMatch = lines[i].match(/^(#{1,6})\s
|
|
318
|
+
const headingMatch = lines[i].match(/^(#{1,6})\s+(.+?)\s*$/);
|
|
144
319
|
if (!headingMatch) {
|
|
145
320
|
continue;
|
|
146
321
|
}
|
|
@@ -167,6 +342,10 @@ module.exports = {
|
|
|
167
342
|
getPreferencesPath,
|
|
168
343
|
loadPreferences,
|
|
169
344
|
savePreferences,
|
|
345
|
+
resolveSection,
|
|
346
|
+
setEntry,
|
|
347
|
+
removeFromSection,
|
|
348
|
+
getEntriesInSection,
|
|
170
349
|
appendToSection,
|
|
171
350
|
resetPreferences,
|
|
172
351
|
validatePreferences,
|