@our2ndbrain/cli 1.1.3

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 (47) hide show
  1. package/.obsidian/.2ndbrain-manifest.json +8 -0
  2. package/.obsidian/app.json +6 -0
  3. package/.obsidian/appearance.json +1 -0
  4. package/.obsidian/community-plugins.json +4 -0
  5. package/.obsidian/core-plugins.json +33 -0
  6. package/.obsidian/graph.json +22 -0
  7. package/.obsidian/plugins/calendar/data.json +10 -0
  8. package/.obsidian/plugins/calendar/main.js +4459 -0
  9. package/.obsidian/plugins/calendar/manifest.json +10 -0
  10. package/.obsidian/plugins/obsidian-custom-attachment-location/data.json +32 -0
  11. package/.obsidian/plugins/obsidian-custom-attachment-location/main.js +575 -0
  12. package/.obsidian/plugins/obsidian-custom-attachment-location/manifest.json +11 -0
  13. package/.obsidian/plugins/obsidian-custom-attachment-location/styles.css +1 -0
  14. package/.obsidian/plugins/obsidian-git/data.json +62 -0
  15. package/.obsidian/plugins/obsidian-git/main.js +426 -0
  16. package/.obsidian/plugins/obsidian-git/manifest.json +10 -0
  17. package/.obsidian/plugins/obsidian-git/obsidian_askpass.sh +23 -0
  18. package/.obsidian/plugins/obsidian-git/styles.css +629 -0
  19. package/.obsidian/plugins/obsidian-tasks-plugin/main.js +504 -0
  20. package/.obsidian/plugins/obsidian-tasks-plugin/manifest.json +12 -0
  21. package/.obsidian/plugins/obsidian-tasks-plugin/styles.css +1 -0
  22. package/.obsidian/types.json +28 -0
  23. package/00_Dashboard/01_All_Tasks.md +118 -0
  24. package/00_Dashboard/09_All_Done.md +42 -0
  25. package/10_Inbox/Agents/Journal.md +1 -0
  26. package/99_System/Scripts/init_member.sh +108 -0
  27. package/99_System/Templates/tpl_daily_note.md +13 -0
  28. package/99_System/Templates/tpl_member_done.md +32 -0
  29. package/99_System/Templates/tpl_member_tasks.md +97 -0
  30. package/AGENTS.md +193 -0
  31. package/CHANGELOG.md +67 -0
  32. package/CLAUDE.md +153 -0
  33. package/LICENSE +201 -0
  34. package/README.md +636 -0
  35. package/bin/2ndbrain.js +117 -0
  36. package/package.json +56 -0
  37. package/src/commands/completion.js +198 -0
  38. package/src/commands/init.js +308 -0
  39. package/src/commands/member.js +123 -0
  40. package/src/commands/remove.js +88 -0
  41. package/src/commands/update.js +507 -0
  42. package/src/index.js +17 -0
  43. package/src/lib/config.js +112 -0
  44. package/src/lib/diff.js +222 -0
  45. package/src/lib/files.js +340 -0
  46. package/src/lib/obsidian.js +366 -0
  47. package/src/lib/prompt.js +182 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * 2ndBrain CLI - Diff Operations
3
+ *
4
+ * Provides file comparison and diff generation utilities.
5
+ */
6
+
7
+ const fs = require('fs-extra');
8
+ const { diffLines, diffWords } = require('diff');
9
+
10
+ /**
11
+ * Binary file extensions that should be detected and skipped
12
+ */
13
+ const BINARY_EXTENSIONS = new Set([
14
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp',
15
+ '.pdf', '.zip', '.tar', '.gz', '.rar', '.7z',
16
+ '.mp3', '.mp4', '.avi', '.mov', '.wav',
17
+ '.ttf', '.otf', '.woff', '.woff2', '.eot',
18
+ '.psd', '.ai', '.sketch',
19
+ ]);
20
+
21
+ /**
22
+ * Large file threshold in bytes (100KB)
23
+ */
24
+ const LARGE_FILE_THRESHOLD = 100 * 1024;
25
+
26
+ /**
27
+ * Check if a file is likely binary based on its extension
28
+ * @param {string} filePath - File path
29
+ * @returns {boolean} True if file is likely binary
30
+ */
31
+ function isBinaryFile(filePath) {
32
+ const ext = filePath.toLowerCase().split('.').pop();
33
+ return BINARY_EXTENSIONS.has(`.${ext}`);
34
+ }
35
+
36
+ /**
37
+ * Check if a file is too large for diff display
38
+ * @param {string} filePath - File path
39
+ * @returns {Promise<boolean>} True if file exceeds threshold
40
+ */
41
+ async function isLargeFile(filePath) {
42
+ try {
43
+ const stats = await fs.stat(filePath);
44
+ return stats.size > LARGE_FILE_THRESHOLD;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Compare two file contents for equality
52
+ * @param {string} content1 - First content
53
+ * @param {string} content2 - Second content
54
+ * @returns {boolean} True if contents are equal
55
+ */
56
+ function areContentsEqual(content1, content2) {
57
+ if (content1 === content2) return true;
58
+
59
+ // Normalize line endings for comparison
60
+ const normalize = (str) => str.replace(/\r\n/g, '\n');
61
+ return normalize(content1) === normalize(content2);
62
+ }
63
+
64
+ /**
65
+ * Read file content safely
66
+ * @param {string} filePath - File path
67
+ * @returns {Promise<string|null>} File content or null if error
68
+ */
69
+ async function readFileContent(filePath) {
70
+ try {
71
+ return await fs.readFile(filePath, 'utf8');
72
+ } catch (err) {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Compare source and destination files
79
+ * @param {string} src - Source file path
80
+ * @param {string} dest - Destination file path
81
+ * @returns {Promise<{equal: boolean, error?: string, binary?: boolean, large?: boolean}>}
82
+ */
83
+ async function compareFiles(src, dest) {
84
+ // Check for binary file
85
+ if (isBinaryFile(src) || isBinaryFile(dest)) {
86
+ return { equal: false, binary: true };
87
+ }
88
+
89
+ // Check for large file
90
+ if (await isLargeFile(src) || await isLargeFile(dest)) {
91
+ return { equal: false, large: true };
92
+ }
93
+
94
+ try {
95
+ const [srcContent, destContent] = await Promise.all([
96
+ readFileContent(src),
97
+ readFileContent(dest),
98
+ ]);
99
+
100
+ // If destination doesn't exist, files are not equal
101
+ if (destContent === null) {
102
+ return { equal: false };
103
+ }
104
+
105
+ // If source doesn't exist, that's an error
106
+ if (srcContent === null) {
107
+ return { equal: false, error: 'source file not found' };
108
+ }
109
+
110
+ return { equal: areContentsEqual(srcContent, destContent) };
111
+ } catch (err) {
112
+ return { equal: false, error: err.message };
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Generate a unified diff between two contents
118
+ * @param {string} oldContent - Old content
119
+ * @param {string} newContent - New content
120
+ * @param {string} oldPath - Old file path (for display)
121
+ * @param {string} newPath - New file path (for display)
122
+ * @returns {string} Unified diff string
123
+ */
124
+ function generateDiff(oldContent, newContent, oldPath = 'old', newPath = 'new') {
125
+ const changes = diffLines(oldContent, newContent);
126
+
127
+ if (changes.length === 0) {
128
+ return 'No differences found.';
129
+ }
130
+
131
+ let diffText = '';
132
+ let oldLineNum = 1;
133
+ let newLineNum = 1;
134
+
135
+ // Count changes
136
+ const addedLines = changes.filter(c => c.added).reduce((sum, c) => sum + c.count, 0);
137
+ const removedLines = changes.filter(c => c.removed).reduce((sum, c) => sum + c.count, 0);
138
+
139
+ diffText += `--- ${oldPath}\n`;
140
+ diffText += `+++ ${newPath}\n`;
141
+ diffText += `@@ -1 +${addedLines > 0 || removedLines === 0 ? '' : ''},${removedLines > 0 ? '?' : '1'} `;
142
+ diffText += `+1 ${removedLines > 0 || addedLines === 0 ? '' : ''},${addedLines > 0 ? '?' : '1'} @@\n`;
143
+
144
+ for (const change of changes) {
145
+ const lines = change.value.split('\n');
146
+ // Remove trailing empty line from split
147
+ if (lines[lines.length - 1] === '') {
148
+ lines.pop();
149
+ }
150
+
151
+ for (const line of lines) {
152
+ if (change.added) {
153
+ diffText += `+${line}\n`;
154
+ newLineNum++;
155
+ } else if (change.removed) {
156
+ diffText += `-${line}\n`;
157
+ oldLineNum++;
158
+ } else {
159
+ diffText += ` ${line}\n`;
160
+ oldLineNum++;
161
+ newLineNum++;
162
+ }
163
+ }
164
+ }
165
+
166
+ return diffText;
167
+ }
168
+
169
+ /**
170
+ * Format diff text with colors for terminal display
171
+ * @param {string} diffText - Raw diff text
172
+ * @param {Object} chalk - Chalk module for colors
173
+ * @returns {string} Formatted diff with ANSI colors
174
+ */
175
+ function formatDiffForTerminal(diffText, chalk) {
176
+ const lines = diffText.split('\n');
177
+ const formatted = lines.map(line => {
178
+ if (line.startsWith('+') && !line.startsWith('+++')) {
179
+ return chalk.green(line);
180
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
181
+ return chalk.red(line);
182
+ } else if (line.startsWith('@@')) {
183
+ return chalk.cyan(line);
184
+ } else if (line.startsWith('---')) {
185
+ return chalk.yellow(line);
186
+ } else if (line.startsWith('+++')) {
187
+ return chalk.yellow(line);
188
+ }
189
+ return line;
190
+ });
191
+
192
+ return formatted.join('\n');
193
+ }
194
+
195
+ /**
196
+ * Generate a summary of changes between two contents
197
+ * @param {string} oldContent - Old content
198
+ * @param {string} newContent - New content
199
+ * @returns {{added: number, removed: number, changed: boolean}}
200
+ */
201
+ function summarizeChanges(oldContent, newContent) {
202
+ const changes = diffLines(oldContent, newContent);
203
+ const added = changes.filter(c => c.added).reduce((sum, c) => sum + c.count, 0);
204
+ const removed = changes.filter(c => c.removed).reduce((sum, c) => sum + c.count, 0);
205
+
206
+ return {
207
+ added,
208
+ removed,
209
+ changed: added > 0 || removed > 0,
210
+ };
211
+ }
212
+
213
+ module.exports = {
214
+ areContentsEqual,
215
+ compareFiles,
216
+ generateDiff,
217
+ formatDiffForTerminal,
218
+ summarizeChanges,
219
+ isBinaryFile,
220
+ isLargeFile,
221
+ LARGE_FILE_THRESHOLD,
222
+ };
@@ -0,0 +1,340 @@
1
+ /**
2
+ * 2ndBrain CLI - File Operations
3
+ *
4
+ * Provides file copy, remove, and directory operations.
5
+ */
6
+
7
+ const fs = require('fs-extra');
8
+ const path = require('path');
9
+ const { compareFiles, summarizeChanges, isBinaryFile, isLargeFile } = require('./diff');
10
+
11
+ /**
12
+ * Copy a file from source to destination
13
+ * @param {string} src - Source file path
14
+ * @param {string} dest - Destination file path
15
+ * @returns {Promise<void>}
16
+ */
17
+ async function copyFile(src, dest) {
18
+ await fs.ensureDir(path.dirname(dest));
19
+ await fs.copy(src, dest);
20
+ }
21
+
22
+ /**
23
+ * Copy multiple files from template to target
24
+ * @param {string[]} files - Array of relative file paths
25
+ * @param {string} templateRoot - Template root directory
26
+ * @param {string} targetRoot - Target root directory
27
+ * @param {Function} [onFile] - Callback for each file (relativePath, action)
28
+ * @returns {Promise<{copied: string[], skipped: string[], errors: string[]}>}
29
+ */
30
+ async function copyFiles(files, templateRoot, targetRoot, onFile) {
31
+ const result = { copied: [], skipped: [], errors: [] };
32
+
33
+ for (const file of files) {
34
+ const src = path.join(templateRoot, file);
35
+ const dest = path.join(targetRoot, file);
36
+
37
+ try {
38
+ // Check if source exists
39
+ if (!await fs.pathExists(src)) {
40
+ result.skipped.push(file);
41
+ if (onFile) onFile(file, 'skip', 'source not found');
42
+ continue;
43
+ }
44
+
45
+ await copyFile(src, dest);
46
+ result.copied.push(file);
47
+ if (onFile) onFile(file, 'copy');
48
+ } catch (err) {
49
+ result.errors.push(`${file}: ${err.message}`);
50
+ if (onFile) onFile(file, 'error', err.message);
51
+ }
52
+ }
53
+
54
+ return result;
55
+ }
56
+
57
+ /**
58
+ * Remove a file if it exists
59
+ * @param {string} filePath - File path to remove
60
+ * @returns {Promise<boolean>} True if removed, false if not found
61
+ */
62
+ async function removeFile(filePath) {
63
+ if (await fs.pathExists(filePath)) {
64
+ await fs.remove(filePath);
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+
70
+ /**
71
+ * Remove multiple files from target directory
72
+ * @param {string[]} files - Array of relative file paths
73
+ * @param {string} targetRoot - Target root directory
74
+ * @param {Function} [onFile] - Callback for each file (relativePath, action)
75
+ * @returns {Promise<{removed: string[], skipped: string[], errors: string[]}>}
76
+ */
77
+ async function removeFiles(files, targetRoot, onFile) {
78
+ const result = { removed: [], skipped: [], errors: [] };
79
+
80
+ for (const file of files) {
81
+ const filePath = path.join(targetRoot, file);
82
+
83
+ try {
84
+ if (await removeFile(filePath)) {
85
+ result.removed.push(file);
86
+ if (onFile) onFile(file, 'remove');
87
+ } else {
88
+ result.skipped.push(file);
89
+ if (onFile) onFile(file, 'skip', 'not found');
90
+ }
91
+ } catch (err) {
92
+ result.errors.push(`${file}: ${err.message}`);
93
+ if (onFile) onFile(file, 'error', err.message);
94
+ }
95
+ }
96
+
97
+ return result;
98
+ }
99
+
100
+ /**
101
+ * Ensure directories exist
102
+ * @param {string[]} dirs - Array of relative directory paths
103
+ * @param {string} targetRoot - Target root directory
104
+ * @returns {Promise<void>}
105
+ */
106
+ async function ensureDirs(dirs, targetRoot) {
107
+ for (const dir of dirs) {
108
+ await fs.ensureDir(path.join(targetRoot, dir));
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Remove empty directories (bottom-up)
114
+ * @param {string[]} dirs - Array of relative directory paths
115
+ * @param {string} targetRoot - Target root directory
116
+ * @param {Function} [onDir] - Callback for each directory (relativePath, action)
117
+ * @returns {Promise<string[]>} Array of removed directories
118
+ */
119
+ async function removeEmptyDirs(dirs, targetRoot, onDir) {
120
+ const removed = [];
121
+
122
+ // Sort by depth (deepest first)
123
+ const sortedDirs = [...dirs].sort((a, b) => {
124
+ return b.split('/').length - a.split('/').length;
125
+ });
126
+
127
+ for (const dir of sortedDirs) {
128
+ const dirPath = path.join(targetRoot, dir);
129
+
130
+ try {
131
+ if (await fs.pathExists(dirPath)) {
132
+ const contents = await fs.readdir(dirPath);
133
+ if (contents.length === 0) {
134
+ await fs.remove(dirPath);
135
+ removed.push(dir);
136
+ if (onDir) onDir(dir, 'remove');
137
+ } else {
138
+ if (onDir) onDir(dir, 'skip', 'not empty');
139
+ }
140
+ }
141
+ } catch (err) {
142
+ // Ignore errors for directory removal
143
+ }
144
+ }
145
+
146
+ // Also try to remove parent directories if empty
147
+ const parentDirs = new Set();
148
+ for (const dir of removed) {
149
+ const parent = path.dirname(dir);
150
+ if (parent !== '.') {
151
+ parentDirs.add(parent);
152
+ }
153
+ }
154
+
155
+ for (const parent of parentDirs) {
156
+ const parentPath = path.join(targetRoot, parent);
157
+ try {
158
+ if (await fs.pathExists(parentPath)) {
159
+ const contents = await fs.readdir(parentPath);
160
+ if (contents.length === 0) {
161
+ await fs.remove(parentPath);
162
+ removed.push(parent);
163
+ if (onDir) onDir(parent, 'remove');
164
+ }
165
+ }
166
+ } catch {
167
+ // Ignore errors
168
+ }
169
+ }
170
+
171
+ return removed;
172
+ }
173
+
174
+ /**
175
+ * Create a file with content
176
+ * @param {string} filePath - File path
177
+ * @param {string} content - File content
178
+ * @returns {Promise<void>}
179
+ */
180
+ async function createFile(filePath, content) {
181
+ await fs.ensureDir(path.dirname(filePath));
182
+ await fs.writeFile(filePath, content, 'utf8');
183
+ }
184
+
185
+ /**
186
+ * Check if directory is empty or doesn't exist
187
+ * @param {string} dirPath - Directory path
188
+ * @returns {Promise<boolean>}
189
+ */
190
+ async function isDirEmpty(dirPath) {
191
+ if (!await fs.pathExists(dirPath)) {
192
+ return true;
193
+ }
194
+ const contents = await fs.readdir(dirPath);
195
+ return contents.length === 0;
196
+ }
197
+
198
+ /**
199
+ * Compare source and destination files
200
+ * @param {string} src - Source file path
201
+ * @param {string} dest - Destination file path
202
+ * @returns {Promise<{equal: boolean, exists: boolean, error?: string, binary?: boolean, large?: boolean}>}
203
+ */
204
+ async function compareFilesWrapper(src, dest) {
205
+ const destExists = await fs.pathExists(dest);
206
+
207
+ if (!destExists) {
208
+ return { equal: false, exists: false };
209
+ }
210
+
211
+ const result = await compareFiles(src, dest);
212
+ return { ...result, exists: true };
213
+ }
214
+
215
+ /**
216
+ * Copy a single file with comparison
217
+ * @param {string} src - Source file path
218
+ * @param {string} dest - Destination file path
219
+ * @param {Object} options - Copy options
220
+ * @param {boolean} [options.force] - Force copy even if equal
221
+ * @returns {Promise<{copied: boolean, unchanged: boolean, error?: string, change?: any}>}
222
+ */
223
+ async function copyFileWithCompare(src, dest, options = {}) {
224
+ try {
225
+ // Check if source exists
226
+ if (!await fs.pathExists(src)) {
227
+ return { copied: false, unchanged: false, error: 'source not found' };
228
+ }
229
+
230
+ // Compare files if destination exists
231
+ const comparison = await compareFilesWrapper(src, dest);
232
+
233
+ if (comparison.exists && comparison.equal && !options.force) {
234
+ return { copied: false, unchanged: true };
235
+ }
236
+
237
+ // Perform the copy
238
+ await fs.ensureDir(path.dirname(dest));
239
+ await fs.copy(src, dest);
240
+
241
+ // Generate change summary
242
+ let change = null;
243
+ if (!comparison.equal) {
244
+ if (comparison.binary) {
245
+ change = { binary: true };
246
+ } else if (comparison.large) {
247
+ change = { large: true };
248
+ } else {
249
+ try {
250
+ const [srcContent, destContent] = await Promise.all([
251
+ fs.readFile(src, 'utf8'),
252
+ comparison.exists ? fs.readFile(dest, 'utf8') : '',
253
+ ]);
254
+ const summary = summarizeChanges(destContent, srcContent);
255
+ change = { added: summary.added, removed: summary.removed };
256
+ } catch {
257
+ // If we can't read as text, mark as binary
258
+ change = { binary: true };
259
+ }
260
+ }
261
+ }
262
+
263
+ return { copied: true, unchanged: false, change };
264
+ } catch (err) {
265
+ return { copied: false, unchanged: false, error: err.message };
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Copy multiple files with smart comparison
271
+ * @param {string[]} files - Array of relative file paths
272
+ * @param {string} templateRoot - Template root directory
273
+ * @param {string} targetRoot - Target root directory
274
+ * @param {Object} options - Copy options
275
+ * @param {boolean} [options.force] - Force copy even if equal
276
+ * @param {boolean} [options.dryRun] - Dry run mode
277
+ * @param {Function} [onFile] - Callback for each file (relativePath, action, detail)
278
+ * @returns {Promise<{copied: string[], skipped: string[], unchanged: string[], errors: string[], changes: Array}>}
279
+ */
280
+ async function copyFilesSmart(files, templateRoot, targetRoot, options = {}, onFile) {
281
+ const result = {
282
+ copied: [],
283
+ skipped: [],
284
+ unchanged: [],
285
+ errors: [],
286
+ changes: [],
287
+ };
288
+
289
+ for (const file of files) {
290
+ const src = path.join(templateRoot, file);
291
+ const dest = path.join(targetRoot, file);
292
+
293
+ try {
294
+ const fileResult = await copyFileWithCompare(src, dest, options);
295
+
296
+ if (fileResult.error) {
297
+ result.errors.push(`${file}: ${fileResult.error}`);
298
+ if (onFile) onFile(file, 'error', fileResult.error);
299
+ continue;
300
+ }
301
+
302
+ if (fileResult.unchanged) {
303
+ result.unchanged.push(file);
304
+ if (onFile) onFile(file, 'unchanged');
305
+ continue;
306
+ }
307
+
308
+ if (options.dryRun) {
309
+ result.skipped.push(file);
310
+ if (onFile) onFile(file, 'dryrun', fileResult.change);
311
+ continue;
312
+ }
313
+
314
+ result.copied.push(file);
315
+ if (fileResult.change) {
316
+ result.changes.push({ file, ...fileResult.change });
317
+ }
318
+ if (onFile) onFile(file, 'copy', fileResult.change);
319
+ } catch (err) {
320
+ result.errors.push(`${file}: ${err.message}`);
321
+ if (onFile) onFile(file, 'error', err.message);
322
+ }
323
+ }
324
+
325
+ return result;
326
+ }
327
+
328
+ module.exports = {
329
+ copyFile,
330
+ copyFiles,
331
+ removeFile,
332
+ removeFiles,
333
+ ensureDirs,
334
+ removeEmptyDirs,
335
+ createFile,
336
+ isDirEmpty,
337
+ compareFilesWrapper,
338
+ copyFileWithCompare,
339
+ copyFilesSmart,
340
+ };