@aperdomoll90/ledger-ai 1.1.3 → 1.3.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/README.md +9 -3
- package/dist/cli.js +39 -6
- package/dist/commands/add.js +28 -4
- package/dist/commands/check.js +13 -1
- package/dist/commands/config.js +158 -27
- package/dist/commands/ingest.js +1 -1
- package/dist/commands/lint.js +179 -0
- package/dist/commands/sync.js +72 -1
- package/dist/lib/config.js +6 -1
- package/dist/lib/lint-configs.js +163 -0
- package/dist/lib/notes.js +133 -24
- package/dist/lib/op-add-note-types.test.js +159 -0
- package/dist/lib/type-registry.test.js +258 -0
- package/dist/mcp-server.js +15 -8
- package/dist/migrations/migrations/004-upsert-key-unique.sql +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ ledger init # connect to existing Supabase, pull persona, set up platform
|
|
|
41
41
|
|
|
42
42
|
**For you:** Define who you are, how you want AI to behave, your coding conventions, communication style. Do it once. Every AI agent on every device follows the same rules.
|
|
43
43
|
|
|
44
|
-
**For your agents:**
|
|
44
|
+
**For your agents:** A full RAG (Retrieval-Augmented Generation) system. Notes are embedded with OpenAI and stored in Postgres with pgvector. Agents find relevant context by semantic meaning, not keywords. Retrieved context is injected into agent prompts automatically via MCP.
|
|
45
45
|
|
|
46
46
|
**For your workflow:** Automatic sync at session start, conflict detection, session-end checks. Hooks enforce rules (block credential file access, auto-ingest notes to Ledger).
|
|
47
47
|
|
|
@@ -153,10 +153,16 @@ npm run release # version bump → build → test → publish to npm
|
|
|
153
153
|
|
|
154
154
|
## Roadmap
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
### RAG Enhancements
|
|
157
|
+
- Hybrid search (vector + BM25 keyword via PostgreSQL full-text search)
|
|
158
|
+
- Re-ranking — score retrieved chunks before feeding to LLM
|
|
159
|
+
- Chunking strategies for large documents (auto-split on ingest)
|
|
160
|
+
- Multi-format ingest (PDF, Excel, images, audio)
|
|
161
|
+
- Multi-provider embeddings (Supabase built-in, OpenAI, Ollama, Cohere)
|
|
162
|
+
|
|
163
|
+
### Platform
|
|
157
164
|
- Note versioning / history
|
|
158
165
|
- Soft delete (trash)
|
|
159
|
-
- Multi-format ingest (PDF, Excel, images, audio)
|
|
160
166
|
- Web dashboard
|
|
161
167
|
- Skills system (context-aware convention enforcement)
|
|
162
168
|
- VS Code extension
|
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ const require = createRequire(import.meta.url);
|
|
|
6
6
|
const { version } = require('../package.json');
|
|
7
7
|
import { pull } from './commands/pull.js';
|
|
8
8
|
import { push } from './commands/push.js';
|
|
9
|
-
import { check } from './commands/check.js';
|
|
9
|
+
import { check, checkChunks } from './commands/check.js';
|
|
10
10
|
import { sync } from './commands/sync.js';
|
|
11
11
|
import { show } from './commands/show.js';
|
|
12
12
|
import { exportNote } from './commands/export.js';
|
|
@@ -17,13 +17,14 @@ import { setupClaudeCode, setupOpenclaw, setupChatgpt } from './commands/setup.j
|
|
|
17
17
|
import { backup, enableBackupCron, disableBackupCron } from './commands/backup.js';
|
|
18
18
|
import { restore } from './commands/restore.js';
|
|
19
19
|
import { onboard } from './commands/onboard.js';
|
|
20
|
-
import { configGet, configSet, configList } from './commands/config.js';
|
|
20
|
+
import { configGet, configSet, configUnset, configList } from './commands/config.js';
|
|
21
21
|
import { migrate } from './commands/migrate.js';
|
|
22
22
|
import { add } from './commands/add.js';
|
|
23
23
|
import { update } from './commands/update.js';
|
|
24
24
|
import { deleteNote } from './commands/delete.js';
|
|
25
25
|
import { list } from './commands/list.js';
|
|
26
26
|
import { tag } from './commands/tag.js';
|
|
27
|
+
import { lint } from './commands/lint.js';
|
|
27
28
|
process.on('unhandledRejection', (err) => {
|
|
28
29
|
console.error(err instanceof Error ? err.message : String(err));
|
|
29
30
|
process.exit(1);
|
|
@@ -51,10 +52,16 @@ program
|
|
|
51
52
|
});
|
|
52
53
|
program
|
|
53
54
|
.command('check')
|
|
54
|
-
.description('Compare local files vs Ledger, report sync status
|
|
55
|
-
.
|
|
55
|
+
.description('Compare local files vs Ledger, report sync status')
|
|
56
|
+
.option('--chunks', 'Check chunk group integrity')
|
|
57
|
+
.action(async (opts) => {
|
|
56
58
|
const config = loadConfig();
|
|
57
|
-
|
|
59
|
+
if (opts.chunks) {
|
|
60
|
+
await checkChunks(config);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
await check(config);
|
|
64
|
+
}
|
|
58
65
|
});
|
|
59
66
|
program
|
|
60
67
|
.command('sync')
|
|
@@ -133,7 +140,25 @@ configCmd
|
|
|
133
140
|
.command('set <key> <value>')
|
|
134
141
|
.description('Set a config value')
|
|
135
142
|
.action(async (key, value) => {
|
|
136
|
-
|
|
143
|
+
if (key.startsWith('types.')) {
|
|
144
|
+
const config = loadConfig();
|
|
145
|
+
await configSet(key, value, { supabase: config.supabase, openai: config.openai });
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
await configSet(key, value);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
configCmd
|
|
152
|
+
.command('unset <key>')
|
|
153
|
+
.description('Remove a config override (types.* keys only)')
|
|
154
|
+
.action(async (key) => {
|
|
155
|
+
if (key.startsWith('types.')) {
|
|
156
|
+
const config = loadConfig();
|
|
157
|
+
await configUnset(key, { supabase: config.supabase, openai: config.openai });
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
await configUnset(key);
|
|
161
|
+
}
|
|
137
162
|
});
|
|
138
163
|
configCmd
|
|
139
164
|
.command('list')
|
|
@@ -254,4 +279,12 @@ program
|
|
|
254
279
|
scope: options.scope,
|
|
255
280
|
});
|
|
256
281
|
});
|
|
282
|
+
program
|
|
283
|
+
.command('lint')
|
|
284
|
+
.description('Add lint configs to the current project based on detected stack')
|
|
285
|
+
.option('--personal', 'include personal conventions (BEM, SCSS patterns)')
|
|
286
|
+
.option('--diff', 'compare local configs against Ledger versions')
|
|
287
|
+
.action(async (options) => {
|
|
288
|
+
await lint({ personal: options.personal ?? false, diff: options.diff ?? false });
|
|
289
|
+
});
|
|
257
290
|
program.parse();
|
package/dist/commands/add.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { loadConfigFile } from '../lib/config.js';
|
|
2
|
-
import { opAddNote,
|
|
2
|
+
import { opAddNote, getRegisteredTypes, isRegisteredType, registerType, validateTypeName, inferDelivery, NOTE_STATUSES } from '../lib/notes.js';
|
|
3
3
|
import { ask, confirm, choose } from '../lib/prompt.js';
|
|
4
4
|
export async function add(config, content, options) {
|
|
5
5
|
const configFile = loadConfigFile();
|
|
@@ -18,12 +18,37 @@ export async function add(config, content, options) {
|
|
|
18
18
|
if (interactive && !options.force) {
|
|
19
19
|
// Type
|
|
20
20
|
if (!type) {
|
|
21
|
+
const registeredTypes = getRegisteredTypes();
|
|
21
22
|
const typeChoice = await choose('What type of note is this?', [
|
|
22
|
-
...
|
|
23
|
+
...registeredTypes,
|
|
23
24
|
'skip — use default (general)',
|
|
24
25
|
]);
|
|
25
26
|
type = typeChoice.startsWith('skip') ? 'general' : typeChoice;
|
|
26
27
|
}
|
|
28
|
+
// Handle unknown type from --type flag
|
|
29
|
+
if (type && !isRegisteredType(type)) {
|
|
30
|
+
console.error(`\nType "${type}" is not registered.`);
|
|
31
|
+
const action = await choose('What would you like to do?', [
|
|
32
|
+
'register — register it now (pick a delivery tier)',
|
|
33
|
+
'existing — use an existing type instead',
|
|
34
|
+
'proceed — save anyway (defaults to "knowledge" delivery)',
|
|
35
|
+
]);
|
|
36
|
+
if (action.startsWith('register')) {
|
|
37
|
+
const nameError = validateTypeName(type);
|
|
38
|
+
if (nameError) {
|
|
39
|
+
console.error(nameError);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const deliveryChoice = await choose('Delivery tier?', ['persona', 'project', 'knowledge', 'protected']);
|
|
43
|
+
registerType(type, deliveryChoice);
|
|
44
|
+
console.error(`Registered type "${type}" with delivery "${deliveryChoice}".`);
|
|
45
|
+
}
|
|
46
|
+
else if (action.startsWith('existing')) {
|
|
47
|
+
const registeredTypes = getRegisteredTypes();
|
|
48
|
+
type = await choose('Choose a type:', registeredTypes);
|
|
49
|
+
}
|
|
50
|
+
// 'proceed' — use the type as-is, will default to knowledge delivery
|
|
51
|
+
}
|
|
27
52
|
// Description
|
|
28
53
|
if (!metadata.description) {
|
|
29
54
|
const desc = await ask('One-line description (what is this note for?): ');
|
|
@@ -43,8 +68,7 @@ export async function add(config, content, options) {
|
|
|
43
68
|
metadata.project = proj;
|
|
44
69
|
}
|
|
45
70
|
// Status (only for project-scoped types)
|
|
46
|
-
|
|
47
|
-
if (projectTypes.includes(type) && !metadata.status) {
|
|
71
|
+
if (inferDelivery(type) === 'project' && !metadata.status) {
|
|
48
72
|
const statusChoice = await choose('What stage is this?', [
|
|
49
73
|
...NOTE_STATUSES,
|
|
50
74
|
'skip — no status',
|
package/dist/commands/check.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, existsSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
-
import { fetchNoteHashes } from '../lib/notes.js';
|
|
3
|
+
import { fetchNoteHashes, checkChunkIntegrity } from '../lib/notes.js';
|
|
4
4
|
import { contentHash } from '../lib/hash.js';
|
|
5
5
|
export async function check(config) {
|
|
6
6
|
const result = {
|
|
@@ -77,3 +77,15 @@ export async function check(config) {
|
|
|
77
77
|
}
|
|
78
78
|
return result;
|
|
79
79
|
}
|
|
80
|
+
export async function checkChunks(config) {
|
|
81
|
+
console.error('Checking chunk integrity...');
|
|
82
|
+
const result = await checkChunkIntegrity(config.supabase);
|
|
83
|
+
if (result.incompleteGroups.length === 0) {
|
|
84
|
+
console.log('All chunk groups are complete.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.error(`Found ${result.incompleteGroups.length} incomplete chunk group(s):`);
|
|
88
|
+
for (const group of result.incompleteGroups) {
|
|
89
|
+
console.error(` group ${group.groupId}: expected ${group.expected} chunks, found ${group.found}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
package/dist/commands/config.js
CHANGED
|
@@ -1,22 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getLedgerDir, saveConfigFile, loadConfigFile } from '../lib/config.js';
|
|
2
|
+
import { BUILTIN_TYPES, getTypeRegistry, opUpdateMetadata, validateTypeName } from '../lib/notes.js';
|
|
3
|
+
import { choose, confirm } from '../lib/prompt.js';
|
|
2
4
|
import { resolve } from 'path';
|
|
3
|
-
import { getLedgerDir } from '../lib/config.js';
|
|
4
|
-
import { confirm } from '../lib/prompt.js';
|
|
5
5
|
const CONFIG_PATH = resolve(getLedgerDir(), 'config.json');
|
|
6
|
-
function loadFullConfig() {
|
|
7
|
-
if (existsSync(CONFIG_PATH)) {
|
|
8
|
-
try {
|
|
9
|
-
return JSON.parse(readFileSync(CONFIG_PATH, 'utf-8'));
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
return {};
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return {};
|
|
16
|
-
}
|
|
17
|
-
function saveConfig(config) {
|
|
18
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
19
|
-
}
|
|
20
6
|
const SECURITY_WARNINGS = {
|
|
21
7
|
envBlocking: 'This disables .env file protection.\nYour API keys and credentials will be readable by the AI agent.',
|
|
22
8
|
mcpJsonBlocking: 'This allows direct editing of mcp.json.\nMCP servers should be registered via `claude mcp add`, not by editing config files.',
|
|
@@ -35,7 +21,7 @@ const DEVICE_DESCRIPTIONS = {
|
|
|
35
21
|
'device.alias': 'Name for this device',
|
|
36
22
|
};
|
|
37
23
|
export async function configGet(key) {
|
|
38
|
-
const config =
|
|
24
|
+
const config = loadConfigFile();
|
|
39
25
|
if (key === 'all') {
|
|
40
26
|
console.log(JSON.stringify(config, null, 2));
|
|
41
27
|
return;
|
|
@@ -76,13 +62,42 @@ export async function configGet(key) {
|
|
|
76
62
|
console.log(`${key}: ${defaults[key]} (default)`);
|
|
77
63
|
return;
|
|
78
64
|
}
|
|
79
|
-
|
|
65
|
+
// Handle type registry
|
|
66
|
+
if (key === 'types') {
|
|
67
|
+
const registry = getTypeRegistry();
|
|
68
|
+
const userTypes = config.types ?? {};
|
|
69
|
+
console.error('Type registry:');
|
|
70
|
+
for (const [typeName, delivery] of Object.entries(registry)) {
|
|
71
|
+
const isBuiltin = typeName in BUILTIN_TYPES;
|
|
72
|
+
const isOverridden = isBuiltin && typeName in userTypes;
|
|
73
|
+
const isCustom = !isBuiltin;
|
|
74
|
+
let annotation = isCustom ? '(custom)' : '(built-in)';
|
|
75
|
+
if (isOverridden) {
|
|
76
|
+
annotation = `(built-in, overridden — default: ${BUILTIN_TYPES[typeName]})`;
|
|
77
|
+
}
|
|
78
|
+
console.error(` ${typeName}: ${delivery} ${annotation}`);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (key.startsWith('types.')) {
|
|
83
|
+
const typeName = key.slice(6);
|
|
84
|
+
const registry = getTypeRegistry();
|
|
85
|
+
if (typeName in registry) {
|
|
86
|
+
const isBuiltin = typeName in BUILTIN_TYPES;
|
|
87
|
+
console.log(`${typeName}: ${registry[typeName]} (${isBuiltin ? 'built-in' : 'custom'})`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log(`${typeName}: not registered`);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const allKeys = [...Object.keys(DESCRIPTIONS), ...Object.keys(NAMING_DESCRIPTIONS), ...Object.keys(DEVICE_DESCRIPTIONS), 'memoryDir', 'claudeMdPath', 'types', 'all'];
|
|
80
95
|
console.error(`Unknown config key: ${key}`);
|
|
81
96
|
console.error(`Available: ${allKeys.join(', ')}`);
|
|
82
97
|
process.exit(1);
|
|
83
98
|
}
|
|
84
|
-
export async function configSet(key, value) {
|
|
85
|
-
const config =
|
|
99
|
+
export async function configSet(key, value, clients) {
|
|
100
|
+
const config = loadConfigFile();
|
|
86
101
|
// Handle hook settings
|
|
87
102
|
const hookKeys = ['envBlocking', 'mcpJsonBlocking', 'writeInterception', 'sessionEndCheck'];
|
|
88
103
|
if (hookKeys.includes(key)) {
|
|
@@ -104,7 +119,7 @@ export async function configSet(key, value) {
|
|
|
104
119
|
if (!config.hooks)
|
|
105
120
|
config.hooks = {};
|
|
106
121
|
config.hooks[key] = boolValue;
|
|
107
|
-
|
|
122
|
+
saveConfigFile(config);
|
|
108
123
|
const state = boolValue ? 'enabled' : 'disabled';
|
|
109
124
|
console.error(`${key}: ${state}`);
|
|
110
125
|
console.error('Run `ledger setup claude-code` to apply hook changes.');
|
|
@@ -118,31 +133,132 @@ export async function configSet(key, value) {
|
|
|
118
133
|
config.naming = {};
|
|
119
134
|
const field = key.split('.')[1];
|
|
120
135
|
config.naming[field] = boolValue;
|
|
121
|
-
|
|
136
|
+
saveConfigFile(config);
|
|
122
137
|
console.error(`${key}: ${boolValue ? 'enabled' : 'disabled'}`);
|
|
123
138
|
return;
|
|
124
139
|
}
|
|
125
140
|
// Handle device alias
|
|
126
141
|
if (key === 'device.alias') {
|
|
127
142
|
config.device = { alias: value };
|
|
128
|
-
|
|
143
|
+
saveConfigFile(config);
|
|
129
144
|
console.error(`device.alias: ${value}`);
|
|
130
145
|
return;
|
|
131
146
|
}
|
|
132
147
|
// Handle path settings
|
|
133
148
|
if (key === 'memoryDir' || key === 'claudeMdPath') {
|
|
134
149
|
config[key] = value;
|
|
135
|
-
|
|
150
|
+
saveConfigFile(config);
|
|
136
151
|
console.error(`${key}: ${value}`);
|
|
137
152
|
return;
|
|
138
153
|
}
|
|
139
|
-
|
|
154
|
+
// Handle type registry
|
|
155
|
+
if (key.startsWith('types.')) {
|
|
156
|
+
const typeName = key.slice(6);
|
|
157
|
+
const delivery = value;
|
|
158
|
+
if (!['persona', 'project', 'knowledge', 'protected'].includes(delivery)) {
|
|
159
|
+
console.error(`Invalid delivery tier: "${value}". Must be: persona, project, knowledge, or protected.`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
const nameError = validateTypeName(typeName);
|
|
163
|
+
if (nameError) {
|
|
164
|
+
console.error(nameError);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const oldDelivery = config.types?.[typeName] ?? BUILTIN_TYPES[typeName];
|
|
168
|
+
if (!config.types)
|
|
169
|
+
config.types = {};
|
|
170
|
+
config.types[typeName] = delivery;
|
|
171
|
+
saveConfigFile(config);
|
|
172
|
+
const isBuiltin = typeName in BUILTIN_TYPES;
|
|
173
|
+
const action = isBuiltin ? 'overridden' : 'registered';
|
|
174
|
+
console.error(`types.${typeName}: ${delivery} (${action})`);
|
|
175
|
+
// Delivery change propagation — only if we have DB access and delivery actually changed
|
|
176
|
+
if (clients && oldDelivery && oldDelivery !== delivery) {
|
|
177
|
+
const { data: notes } = await clients.supabase
|
|
178
|
+
.from('notes')
|
|
179
|
+
.select('id, metadata')
|
|
180
|
+
.eq('metadata->>type', typeName);
|
|
181
|
+
const affected = (notes ?? []).filter((n) => n.metadata.delivery !== delivery);
|
|
182
|
+
if (affected.length > 0) {
|
|
183
|
+
console.error(`\n${affected.length} note(s) currently have a different delivery:`);
|
|
184
|
+
for (const note of affected) {
|
|
185
|
+
const meta = note.metadata;
|
|
186
|
+
const uKey = meta.upsert_key || `id-${note.id}`;
|
|
187
|
+
console.error(` [${note.id}] ${uKey} — delivery: ${meta.delivery}`);
|
|
188
|
+
}
|
|
189
|
+
const action = await choose('\nUpdate delivery on these notes?', [
|
|
190
|
+
'all — update all notes',
|
|
191
|
+
'select — choose individually',
|
|
192
|
+
'none — only affect new notes',
|
|
193
|
+
]);
|
|
194
|
+
if (action.startsWith('all')) {
|
|
195
|
+
for (const note of affected) {
|
|
196
|
+
await opUpdateMetadata(clients, note.id, { delivery });
|
|
197
|
+
}
|
|
198
|
+
console.error(`Updated delivery to "${delivery}" on ${affected.length} note(s).`);
|
|
199
|
+
}
|
|
200
|
+
else if (action.startsWith('select')) {
|
|
201
|
+
let updated = 0;
|
|
202
|
+
for (const note of affected) {
|
|
203
|
+
const meta = note.metadata;
|
|
204
|
+
const uKey = meta.upsert_key || `id-${note.id}`;
|
|
205
|
+
const yes = await confirm(` Update [${note.id}] ${uKey}?`);
|
|
206
|
+
if (yes) {
|
|
207
|
+
await opUpdateMetadata(clients, note.id, { delivery });
|
|
208
|
+
updated++;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
console.error(`Updated delivery on ${updated} note(s).`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const allKeys = [...hookKeys, ...namingKeys, 'device.alias', 'memoryDir', 'claudeMdPath', 'types.*'];
|
|
140
218
|
console.error(`Unknown config key: ${key}`);
|
|
141
219
|
console.error(`Available: ${allKeys.join(', ')}`);
|
|
142
220
|
process.exit(1);
|
|
143
221
|
}
|
|
222
|
+
export async function configUnset(key, clients) {
|
|
223
|
+
if (!key.startsWith('types.')) {
|
|
224
|
+
console.error(`Unset is only supported for types.* keys. Got: ${key}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
const typeName = key.slice(6);
|
|
228
|
+
const config = loadConfigFile();
|
|
229
|
+
const userTypes = config.types ?? {};
|
|
230
|
+
if (!(typeName in userTypes)) {
|
|
231
|
+
console.error(`No user override for "${typeName}".`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const isBuiltin = typeName in BUILTIN_TYPES;
|
|
235
|
+
if (!isBuiltin && clients) {
|
|
236
|
+
const { data: notes } = await clients.supabase
|
|
237
|
+
.from('notes')
|
|
238
|
+
.select('id')
|
|
239
|
+
.eq('metadata->>type', typeName);
|
|
240
|
+
if (notes && notes.length > 0) {
|
|
241
|
+
console.error(`\n${notes.length} note(s) use type "${typeName}". They will become unregistered (delivery defaults to "knowledge").`);
|
|
242
|
+
const proceed = await confirm('Proceed?');
|
|
243
|
+
if (!proceed) {
|
|
244
|
+
console.error('Cancelled.');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
delete config.types[typeName];
|
|
250
|
+
if (config.types && Object.keys(config.types).length === 0)
|
|
251
|
+
delete config.types;
|
|
252
|
+
saveConfigFile(config);
|
|
253
|
+
if (isBuiltin) {
|
|
254
|
+
console.error(`Reverted "${typeName}" to built-in default: ${BUILTIN_TYPES[typeName]}`);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.error(`Removed custom type "${typeName}".`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
144
260
|
export async function configList() {
|
|
145
|
-
const config =
|
|
261
|
+
const config = loadConfigFile();
|
|
146
262
|
const hooks = config.hooks || {};
|
|
147
263
|
console.error('Hook settings:');
|
|
148
264
|
for (const [key, desc] of Object.entries(DESCRIPTIONS)) {
|
|
@@ -163,5 +279,20 @@ export async function configList() {
|
|
|
163
279
|
console.error('\nPaths:');
|
|
164
280
|
console.error(` memoryDir: ${config.memoryDir || '(default)'}`);
|
|
165
281
|
console.error(` claudeMdPath: ${config.claudeMdPath || '(default)'}`);
|
|
282
|
+
const registry = getTypeRegistry();
|
|
283
|
+
const userTypes = config.types ?? {};
|
|
284
|
+
const customTypes = Object.keys(userTypes).filter(t => !(t in BUILTIN_TYPES));
|
|
285
|
+
const overrides = Object.keys(userTypes).filter(t => t in BUILTIN_TYPES);
|
|
286
|
+
console.error('\nType registry:');
|
|
287
|
+
if (customTypes.length > 0) {
|
|
288
|
+
console.error(` Custom types: ${customTypes.map(t => `${t} (${userTypes[t]})`).join(', ')}`);
|
|
289
|
+
}
|
|
290
|
+
if (overrides.length > 0) {
|
|
291
|
+
console.error(` Overrides: ${overrides.map(t => `${t}: ${userTypes[t]} (default: ${BUILTIN_TYPES[t]})`).join(', ')}`);
|
|
292
|
+
}
|
|
293
|
+
if (customTypes.length === 0 && overrides.length === 0) {
|
|
294
|
+
console.error(' No custom types or overrides. Using built-in defaults.');
|
|
295
|
+
}
|
|
296
|
+
console.error(` Built-in types: ${Object.keys(BUILTIN_TYPES).length}`);
|
|
166
297
|
console.error('\nConfig file: ' + CONFIG_PATH);
|
|
167
298
|
}
|
package/dist/commands/ingest.js
CHANGED
|
@@ -108,7 +108,7 @@ async function ingestFile(config, filePath, existingNotes) {
|
|
|
108
108
|
const defaultDelivery = inferDelivery(noteType);
|
|
109
109
|
const deliveryChoice = await choose(`Delivery tier (default: ${defaultDelivery}):`, [
|
|
110
110
|
`${defaultDelivery} (default)`,
|
|
111
|
-
...['persona', 'project', 'knowledge'].filter(d => d !== defaultDelivery),
|
|
111
|
+
...['persona', 'project', 'knowledge', 'protected'].filter(d => d !== defaultDelivery),
|
|
112
112
|
]);
|
|
113
113
|
const delivery = deliveryChoice.replace(' (default)', '');
|
|
114
114
|
const { openai } = config;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
5
|
+
import { detectStack, getConfigsForStack, ESLINT_TS, ESLINT_TS_REACT, STYLELINT_UNIVERSAL, } from '../lib/lint-configs.js';
|
|
6
|
+
// --- Helpers ---
|
|
7
|
+
async function confirm(question) {
|
|
8
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
rl.question(`${question} [y/N] `, (answer) => {
|
|
11
|
+
rl.close();
|
|
12
|
+
resolve(answer.trim() === 'y' || answer.trim() === 'yes');
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
// --- writeConfig ---
|
|
17
|
+
export async function writeConfig(projectDir, config) {
|
|
18
|
+
const filePath = resolve(projectDir, config.filename);
|
|
19
|
+
if (existsSync(filePath)) {
|
|
20
|
+
const existing = readFileSync(filePath, 'utf-8');
|
|
21
|
+
if (existing === config.content) {
|
|
22
|
+
console.error(` ${config.filename} — already up to date`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const overwrite = await confirm(` ${config.filename} already exists. Overwrite?`);
|
|
26
|
+
if (!overwrite) {
|
|
27
|
+
console.error(` ${config.filename} — skipped`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
writeFileSync(filePath, config.content, 'utf-8');
|
|
32
|
+
console.error(` ${config.filename} — written`);
|
|
33
|
+
}
|
|
34
|
+
function getUniversalConfigs(hasReact) {
|
|
35
|
+
return {
|
|
36
|
+
eslint: { filename: 'eslint.config.js', content: hasReact ? ESLINT_TS_REACT : ESLINT_TS },
|
|
37
|
+
stylelint: { filename: '.stylelintrc.json', content: STYLELINT_UNIVERSAL },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function handleDiff(projectDir, configs, universalConfigs) {
|
|
41
|
+
const toCheck = [];
|
|
42
|
+
if (configs.eslint) {
|
|
43
|
+
toCheck.push({ filename: configs.eslint.filename, template: configs.eslint.content });
|
|
44
|
+
// Also check universal if writing personal
|
|
45
|
+
if (configs.eslint.filename !== universalConfigs.eslint.filename) {
|
|
46
|
+
toCheck.push({ filename: universalConfigs.eslint.filename, template: universalConfigs.eslint.content });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (configs.stylelint) {
|
|
50
|
+
toCheck.push({ filename: configs.stylelint.filename, template: configs.stylelint.content });
|
|
51
|
+
if (configs.stylelint.filename !== universalConfigs.stylelint.filename) {
|
|
52
|
+
toCheck.push({ filename: universalConfigs.stylelint.filename, template: universalConfigs.stylelint.content });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (toCheck.length === 0) {
|
|
56
|
+
console.error('No lint configs applicable for this project stack.');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let anyDiff = false;
|
|
60
|
+
for (const { filename, template } of toCheck) {
|
|
61
|
+
const filePath = resolve(projectDir, filename);
|
|
62
|
+
if (!existsSync(filePath)) {
|
|
63
|
+
console.error(` ${filename} — not found (run without --diff to create)`);
|
|
64
|
+
anyDiff = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const existing = readFileSync(filePath, 'utf-8');
|
|
68
|
+
if (existing === template) {
|
|
69
|
+
console.error(` ${filename} — up to date`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.error(` ${filename} — differs from Ledger template`);
|
|
73
|
+
anyDiff = true;
|
|
74
|
+
const update = await confirm(` Update ${filename} to Ledger version?`);
|
|
75
|
+
if (update) {
|
|
76
|
+
writeFileSync(filePath, template, 'utf-8');
|
|
77
|
+
console.error(` ${filename} — updated`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!anyDiff) {
|
|
82
|
+
console.error('\nAll configs are up to date.');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// --- checkDependencies ---
|
|
86
|
+
const ESLINT_TS_DEPS = ['typescript-eslint', 'eslint-plugin-import'];
|
|
87
|
+
const ESLINT_REACT_DEPS = ['eslint-plugin-react', 'eslint-plugin-jsx-a11y'];
|
|
88
|
+
const STYLELINT_DEPS = ['stylelint', 'stylelint-config-standard-scss'];
|
|
89
|
+
export async function checkDependencies(projectDir, stack, personal) {
|
|
90
|
+
const packageJsonPath = resolve(projectDir, 'package.json');
|
|
91
|
+
if (!existsSync(packageJsonPath))
|
|
92
|
+
return;
|
|
93
|
+
let pkg;
|
|
94
|
+
try {
|
|
95
|
+
pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const installed = new Set([
|
|
101
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
102
|
+
...Object.keys(pkg.devDependencies ?? {}),
|
|
103
|
+
]);
|
|
104
|
+
const needed = [];
|
|
105
|
+
const needsEslint = stack.hasTypeScript || stack.hasReact;
|
|
106
|
+
const needsStylelint = stack.hasScss;
|
|
107
|
+
if (needsEslint) {
|
|
108
|
+
for (const dep of ESLINT_TS_DEPS) {
|
|
109
|
+
if (!installed.has(dep))
|
|
110
|
+
needed.push(dep);
|
|
111
|
+
}
|
|
112
|
+
if (stack.hasReact) {
|
|
113
|
+
for (const dep of ESLINT_REACT_DEPS) {
|
|
114
|
+
if (!installed.has(dep))
|
|
115
|
+
needed.push(dep);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (needsStylelint) {
|
|
120
|
+
for (const dep of STYLELINT_DEPS) {
|
|
121
|
+
if (!installed.has(dep))
|
|
122
|
+
needed.push(dep);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (personal) {
|
|
126
|
+
// no extra deps for personal configs
|
|
127
|
+
}
|
|
128
|
+
if (needed.length === 0)
|
|
129
|
+
return;
|
|
130
|
+
console.error('\nMissing devDependencies:');
|
|
131
|
+
for (const dep of needed) {
|
|
132
|
+
console.error(` - ${dep}`);
|
|
133
|
+
}
|
|
134
|
+
const install = await confirm('\nInstall missing dependencies now?');
|
|
135
|
+
if (install) {
|
|
136
|
+
execSync(`npm install --save-dev ${needed.join(' ')}`, { stdio: 'inherit', cwd: projectDir });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// --- Main lint command ---
|
|
140
|
+
export async function lint(options) {
|
|
141
|
+
const projectDir = process.cwd();
|
|
142
|
+
console.error('Detecting project stack...');
|
|
143
|
+
const stack = detectStack(projectDir);
|
|
144
|
+
console.error(` package.json: ${stack.hasPackageJson}`);
|
|
145
|
+
console.error(` TypeScript: ${stack.hasTypeScript}`);
|
|
146
|
+
console.error(` React: ${stack.hasReact}`);
|
|
147
|
+
console.error(` SCSS: ${stack.hasScss}`);
|
|
148
|
+
console.error('');
|
|
149
|
+
const configs = getConfigsForStack(stack, options.personal);
|
|
150
|
+
const universalConfigs = getUniversalConfigs(stack.hasReact);
|
|
151
|
+
if (!configs.eslint && !configs.stylelint) {
|
|
152
|
+
console.error('No lint configs applicable for this stack (needs TypeScript, React, or SCSS).');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (options.diff) {
|
|
156
|
+
console.error('Comparing local configs against Ledger templates...\n');
|
|
157
|
+
await handleDiff(projectDir, configs, universalConfigs);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
console.error('Writing lint configs...\n');
|
|
161
|
+
// When personal, also write the universal configs that personal ones extend
|
|
162
|
+
if (options.personal) {
|
|
163
|
+
if (configs.eslint) {
|
|
164
|
+
await writeConfig(projectDir, universalConfigs.eslint);
|
|
165
|
+
}
|
|
166
|
+
if (configs.stylelint) {
|
|
167
|
+
await writeConfig(projectDir, universalConfigs.stylelint);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (configs.eslint) {
|
|
171
|
+
await writeConfig(projectDir, configs.eslint);
|
|
172
|
+
}
|
|
173
|
+
if (configs.stylelint) {
|
|
174
|
+
await writeConfig(projectDir, configs.stylelint);
|
|
175
|
+
}
|
|
176
|
+
console.error('');
|
|
177
|
+
await checkDependencies(projectDir, stack, options.personal);
|
|
178
|
+
console.error('\nDone.');
|
|
179
|
+
}
|