remember-when-cli 1.0.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.
- package/README.md +66 -0
- package/cli.test.js +107 -0
- package/e2e.test.js +43 -0
- package/index.js +191 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Remember When - Storage CLI
|
|
2
|
+
|
|
3
|
+
The persistence engine for the **Remember When** digital memory system. This Node.js application handles local file organization, automatic folder structure, and the master timeline ledger.
|
|
4
|
+
|
|
5
|
+
## ๐ฆ Installation
|
|
6
|
+
|
|
7
|
+
Install globally to enable the `remember-when` command system-wide:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
cd remember-when-cli
|
|
11
|
+
npm install -g .
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## ๐ Command Reference
|
|
15
|
+
|
|
16
|
+
### `add`
|
|
17
|
+
Registers a new event and optionally moves a file to the permanent local archive.
|
|
18
|
+
```bash
|
|
19
|
+
remember-when add -g "Friends" -t "photo" -s "Juan" -r "Beach day" -f "/tmp/img.jpg"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### `set-group-info`
|
|
23
|
+
Defines the purpose and participants of a group.
|
|
24
|
+
```bash
|
|
25
|
+
remember-when set-group-info -g "Friends" -d "Local hangout crew" -p "Juan, Eric, Maria"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### `set-daily-summary`
|
|
29
|
+
Adds a high-level summary of what happened during a specific day.
|
|
30
|
+
```bash
|
|
31
|
+
remember-when set-daily-summary -g "Friends" -d "2026-04-10" -s "We planned the summer trip."
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### `inventory`
|
|
35
|
+
Audits the storage and highlights missing metadata or gaps in chronicles.
|
|
36
|
+
```bash
|
|
37
|
+
remember-when inventory
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## ๐ Storage Architecture
|
|
41
|
+
|
|
42
|
+
All data is stored in your home directory: `~/.remember-when/`
|
|
43
|
+
|
|
44
|
+
### 1. `timeline.json`
|
|
45
|
+
The master index of all your memories.
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"groups": {
|
|
49
|
+
"Friends": {
|
|
50
|
+
"info": { "description": "...", "participants": [] },
|
|
51
|
+
"daily_summaries": { "2026-04-10": "..." }
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"entries": [
|
|
55
|
+
{ "id": "...", "type": "...", "summary": "...", "file": "2026-04-10/123-img.jpg" }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Daily Folders
|
|
61
|
+
Media files are copied to `~/.remember-when/YYYY-MM-DD/` with unique timestamps to prevent name collisions.
|
|
62
|
+
|
|
63
|
+
## ๐งช Testing
|
|
64
|
+
```bash
|
|
65
|
+
npm test
|
|
66
|
+
```
|
package/cli.test.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
// Setup test dir before importing index.js logic
|
|
6
|
+
const testDir = path.join(os.tmpdir(), `remember-when-test-${Date.now()}`);
|
|
7
|
+
process.env.REMEMBER_WHEN_TEST_DIR = testDir;
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
addEntry,
|
|
11
|
+
setGroupInfo,
|
|
12
|
+
setDailySummary,
|
|
13
|
+
loadTimeline
|
|
14
|
+
} from './index.js';
|
|
15
|
+
|
|
16
|
+
describe('Remember When CLI - Logic Tests', () => {
|
|
17
|
+
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
if (!fs.existsSync(testDir)) {
|
|
20
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterAll(() => {
|
|
25
|
+
if (fs.existsSync(testDir)) {
|
|
26
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
// Clear timeline before each test
|
|
32
|
+
const timelinePath = path.join(testDir, 'timeline.json');
|
|
33
|
+
if (fs.existsSync(timelinePath)) {
|
|
34
|
+
fs.unlinkSync(timelinePath);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should add a text entry correctly', () => {
|
|
39
|
+
const options = {
|
|
40
|
+
group: 'TestGroup',
|
|
41
|
+
type: 'text',
|
|
42
|
+
sender: 'Jest',
|
|
43
|
+
summary: 'Unit test entry',
|
|
44
|
+
date: new Date().toISOString()
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const entry = addEntry(options);
|
|
48
|
+
const timeline = loadTimeline();
|
|
49
|
+
|
|
50
|
+
expect(Array.isArray(timeline.entries)).toBe(true);
|
|
51
|
+
expect(timeline.entries).toHaveLength(1);
|
|
52
|
+
expect(timeline.entries[0].group).toBe('TestGroup');
|
|
53
|
+
expect(timeline.entries[0].summary).toBe('Unit test entry');
|
|
54
|
+
expect(entry.id).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should set group info correctly', () => {
|
|
58
|
+
const options = {
|
|
59
|
+
group: 'TestGroup',
|
|
60
|
+
desc: 'A group for testing',
|
|
61
|
+
participants: 'User1, User2'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
setGroupInfo(options);
|
|
65
|
+
const timeline = loadTimeline();
|
|
66
|
+
|
|
67
|
+
expect(timeline.groups['TestGroup']).toBeDefined();
|
|
68
|
+
expect(timeline.groups['TestGroup'].info.description).toBe('A group for testing');
|
|
69
|
+
expect(timeline.groups['TestGroup'].info.participants).toEqual(['User1', 'User2']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should set daily summary correctly', () => {
|
|
73
|
+
const options = {
|
|
74
|
+
group: 'TestGroup',
|
|
75
|
+
date: '2026-04-10',
|
|
76
|
+
summary: 'Today nothing happened'
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
setDailySummary(options);
|
|
80
|
+
const timeline = loadTimeline();
|
|
81
|
+
|
|
82
|
+
expect(timeline.groups['TestGroup'].daily_summaries['2026-04-10']).toBe('Today nothing happened');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should copy file when provided in add', () => {
|
|
86
|
+
const tempFilePath = path.join(testDir, 'test-file.txt');
|
|
87
|
+
fs.writeFileSync(tempFilePath, 'hello world');
|
|
88
|
+
|
|
89
|
+
const options = {
|
|
90
|
+
group: 'FileGroup',
|
|
91
|
+
type: 'file',
|
|
92
|
+
sender: 'Jest',
|
|
93
|
+
summary: 'File test',
|
|
94
|
+
date: '2026-04-10T10:00:00.000Z',
|
|
95
|
+
file: tempFilePath
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const entry = addEntry(options);
|
|
99
|
+
|
|
100
|
+
// Check if file exists in the day folder
|
|
101
|
+
const dayFolder = '2026-04-10';
|
|
102
|
+
const storedPath = path.join(testDir, entry.file);
|
|
103
|
+
|
|
104
|
+
expect(fs.existsSync(storedPath)).toBe(true);
|
|
105
|
+
expect(fs.readFileSync(storedPath, 'utf8')).toBe('hello world');
|
|
106
|
+
});
|
|
107
|
+
});
|
package/e2e.test.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
describe('Remember When CLI - E2E Tests', () => {
|
|
7
|
+
const testDir = path.join(os.tmpdir(), `remember-when-e2e-${Date.now()}`);
|
|
8
|
+
const cliPath = path.resolve('./index.js');
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
if (!fs.existsSync(testDir)) {
|
|
12
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterAll(() => {
|
|
17
|
+
if (fs.existsSync(testDir)) {
|
|
18
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should run "add" command successfully', () => {
|
|
23
|
+
const cmd = `REMEMBER_WHEN_TEST_DIR=${testDir} node ${cliPath} add -g E2EGroup -t text -s Tester -r "E2E Test Summary"`;
|
|
24
|
+
const output = execSync(cmd).toString();
|
|
25
|
+
|
|
26
|
+
expect(output).toContain('[remember-when] Added entry to E2EGroup');
|
|
27
|
+
|
|
28
|
+
const timelinePath = path.join(testDir, 'timeline.json');
|
|
29
|
+
expect(fs.existsSync(timelinePath)).toBe(true);
|
|
30
|
+
|
|
31
|
+
const timeline = JSON.parse(fs.readFileSync(timelinePath, 'utf8'));
|
|
32
|
+
expect(timeline.entries[0].group).toBe('E2EGroup');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should run "inventory" command successfully', () => {
|
|
36
|
+
const cmd = `REMEMBER_WHEN_TEST_DIR=${testDir} node ${cliPath} inventory`;
|
|
37
|
+
const output = execSync(cmd).toString();
|
|
38
|
+
|
|
39
|
+
expect(output).toContain('--- REMEMBER-WHEN INVENTORY ---');
|
|
40
|
+
expect(output).toContain('Group: E2EGroup');
|
|
41
|
+
expect(output).toContain('[!] MISSING: Group description');
|
|
42
|
+
});
|
|
43
|
+
});
|
package/index.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { createRequire } from 'module';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const pkg = require('./package.json');
|
|
10
|
+
|
|
11
|
+
export function getStorageRoot() {
|
|
12
|
+
return process.env.REMEMBER_WHEN_TEST_DIR
|
|
13
|
+
? process.env.REMEMBER_WHEN_TEST_DIR
|
|
14
|
+
: path.join(os.homedir(), '.remember-when');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getTimelinePath() {
|
|
18
|
+
return path.join(getStorageRoot(), 'timeline.json');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ensureDir(dir) {
|
|
22
|
+
if (!fs.existsSync(dir)) {
|
|
23
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function loadTimeline() {
|
|
28
|
+
const root = getStorageRoot();
|
|
29
|
+
const timelinePath = getTimelinePath();
|
|
30
|
+
ensureDir(root);
|
|
31
|
+
if (!fs.existsSync(timelinePath)) {
|
|
32
|
+
return { groups: {}, entries: [] };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const data = fs.readFileSync(timelinePath, 'utf8');
|
|
36
|
+
const parsed = JSON.parse(data || '{}');
|
|
37
|
+
return {
|
|
38
|
+
groups: parsed.groups || {},
|
|
39
|
+
entries: Array.isArray(parsed.entries) ? parsed.entries : []
|
|
40
|
+
};
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { groups: {}, entries: [] };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function saveTimeline(timeline) {
|
|
47
|
+
const root = getStorageRoot();
|
|
48
|
+
const timelinePath = getTimelinePath();
|
|
49
|
+
ensureDir(root);
|
|
50
|
+
fs.writeFileSync(timelinePath, JSON.stringify(timeline, null, 2), 'utf8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function addEntry(options) {
|
|
54
|
+
const root = getStorageRoot();
|
|
55
|
+
const timeline = loadTimeline();
|
|
56
|
+
const date = new Date(options.date);
|
|
57
|
+
const dayFolderName = date.toISOString().split('T')[0];
|
|
58
|
+
let storedFilePath = null;
|
|
59
|
+
|
|
60
|
+
if (options.file && fs.existsSync(options.file)) {
|
|
61
|
+
const dayFolderPath = path.join(root, dayFolderName);
|
|
62
|
+
ensureDir(dayFolderPath);
|
|
63
|
+
const safeName = `${Date.now()}-${path.basename(options.file)}`;
|
|
64
|
+
storedFilePath = path.join(dayFolderName, safeName);
|
|
65
|
+
fs.copyFileSync(options.file, path.join(root, storedFilePath));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const entry = {
|
|
69
|
+
id: Math.random().toString(36).substring(2, 11),
|
|
70
|
+
timestamp: new Date().toISOString(),
|
|
71
|
+
originalDate: options.date,
|
|
72
|
+
group: options.group,
|
|
73
|
+
type: options.type,
|
|
74
|
+
sender: options.sender,
|
|
75
|
+
summary: options.summary,
|
|
76
|
+
file: storedFilePath
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
timeline.entries.push(entry);
|
|
80
|
+
saveTimeline(timeline);
|
|
81
|
+
return entry;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function setGroupInfo(options) {
|
|
85
|
+
const timeline = loadTimeline();
|
|
86
|
+
if (!timeline.groups[options.group]) timeline.groups[options.group] = { info: {}, daily_summaries: {} };
|
|
87
|
+
|
|
88
|
+
timeline.groups[options.group].info = {
|
|
89
|
+
description: options.desc,
|
|
90
|
+
participants: (options.participants || '').split(',').map(p => p.trim())
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
saveTimeline(timeline);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function setDailySummary(options) {
|
|
97
|
+
const timeline = loadTimeline();
|
|
98
|
+
if (!timeline.groups[options.group]) timeline.groups[options.group] = { info: {}, daily_summaries: {} };
|
|
99
|
+
|
|
100
|
+
timeline.groups[options.group].daily_summaries[options.date] = options.summary;
|
|
101
|
+
|
|
102
|
+
saveTimeline(timeline);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// CLI definition
|
|
106
|
+
const isMain = import.meta.url === `file://${fs.realpathSync(process.argv[1])}` || process.argv[1].endsWith('remember-when');
|
|
107
|
+
|
|
108
|
+
if (isMain) {
|
|
109
|
+
program
|
|
110
|
+
.name('remember-when')
|
|
111
|
+
.description('Local storage system for your digital memories')
|
|
112
|
+
.version(pkg.version);
|
|
113
|
+
|
|
114
|
+
program.command('add')
|
|
115
|
+
.description('Registers a new memory entry')
|
|
116
|
+
.requiredOption('-g, --group <group>', 'Group identifier')
|
|
117
|
+
.requiredOption('-t, --type <type>', 'Entry type (text, photo, etc.)')
|
|
118
|
+
.requiredOption('-s, --sender <sender>', 'Who sent it')
|
|
119
|
+
.requiredOption('-r, --summary <summary>', 'Entry description')
|
|
120
|
+
.option('-d, --date <date>', 'ISO Date', new Date().toISOString())
|
|
121
|
+
.option('-f, --file <path>', 'Local file path')
|
|
122
|
+
.action((options) => {
|
|
123
|
+
try {
|
|
124
|
+
const entry = addEntry(options);
|
|
125
|
+
console.log(`[remember-when] Added entry to ${options.group} | ID: ${entry.id}`);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
console.error(`[remember-when] Error: ${err.message}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
program.command('set-group-info')
|
|
133
|
+
.description('Updates context information for a group')
|
|
134
|
+
.requiredOption('-g, --group <group>', 'Group identifier')
|
|
135
|
+
.requiredOption('-d, --desc <description>', 'What is this group about?')
|
|
136
|
+
.requiredOption('-p, --participants <participants>', 'List of members (comma separated)')
|
|
137
|
+
.action((options) => {
|
|
138
|
+
setGroupInfo(options);
|
|
139
|
+
console.log(`[remember-when] Group info updated for: ${options.group}`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
program.command('set-daily-summary')
|
|
143
|
+
.description('Sets a brief summary for a specific day')
|
|
144
|
+
.requiredOption('-g, --group <group>', 'Group identifier')
|
|
145
|
+
.requiredOption('-s, --summary <summary>', 'What happened today?')
|
|
146
|
+
.option('-d, --date <date>', 'Date (YYYY-MM-DD)', new Date().toISOString().split('T')[0])
|
|
147
|
+
.action((options) => {
|
|
148
|
+
setDailySummary(options);
|
|
149
|
+
console.log(`[remember-when] Daily summary updated for ${options.group} on ${options.date}`);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
program.command('inventory')
|
|
153
|
+
.description('Shows missing information (context or summaries)')
|
|
154
|
+
.action(() => {
|
|
155
|
+
const timeline = loadTimeline();
|
|
156
|
+
console.log('\n--- REMEMBER-WHEN INVENTORY ---');
|
|
157
|
+
|
|
158
|
+
if (timeline.entries.length === 0) {
|
|
159
|
+
console.log('No entries found yet. Use "add" to begin.');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const allGroups = [...new Set(timeline.entries.map(e => e.group))];
|
|
164
|
+
|
|
165
|
+
allGroups.forEach(groupName => {
|
|
166
|
+
console.log(`\nGroup: ${groupName}`);
|
|
167
|
+
const group = timeline.groups[groupName];
|
|
168
|
+
|
|
169
|
+
if (!group || !group.info || !group.info.description) {
|
|
170
|
+
console.log(' [!] MISSING: Group description and participants. Use set-group-info.');
|
|
171
|
+
} else {
|
|
172
|
+
console.log(' [ok] Group info present.');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const daysWithEntries = [...new Set(timeline.entries
|
|
176
|
+
.filter(e => e.group === groupName)
|
|
177
|
+
.map(e => e.originalDate.split('T')[0]))];
|
|
178
|
+
|
|
179
|
+
const missingDays = daysWithEntries.filter(day => !group || !group.daily_summaries[day]);
|
|
180
|
+
|
|
181
|
+
if (missingDays.length > 0) {
|
|
182
|
+
console.log(` [!] MISSING: Daily summaries for: ${missingDays.join(', ')}`);
|
|
183
|
+
} else {
|
|
184
|
+
console.log(' [ok] All active days have summaries.');
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
console.log('\n');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
program.parse();
|
|
191
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "remember-when-cli",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Local storage CLI for digital memories",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"remember-when": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"commander": "^12.1.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"jest": "^29.7.0"
|
|
21
|
+
}
|
|
22
|
+
}
|