@aperdomoll90/ledger-ai 1.1.3 → 1.2.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/dist/cli.js +20 -2
- package/dist/commands/add.js +28 -4
- package/dist/commands/config.js +158 -27
- package/dist/commands/sync.js +72 -1
- package/dist/lib/config.js +6 -1
- package/dist/lib/notes.js +75 -20
- package/dist/mcp-server.js +10 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ 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';
|
|
@@ -133,7 +133,25 @@ configCmd
|
|
|
133
133
|
.command('set <key> <value>')
|
|
134
134
|
.description('Set a config value')
|
|
135
135
|
.action(async (key, value) => {
|
|
136
|
-
|
|
136
|
+
if (key.startsWith('types.')) {
|
|
137
|
+
const config = loadConfig();
|
|
138
|
+
await configSet(key, value, { supabase: config.supabase, openai: config.openai });
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
await configSet(key, value);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
configCmd
|
|
145
|
+
.command('unset <key>')
|
|
146
|
+
.description('Remove a config override (types.* keys only)')
|
|
147
|
+
.action(async (key) => {
|
|
148
|
+
if (key.startsWith('types.')) {
|
|
149
|
+
const config = loadConfig();
|
|
150
|
+
await configUnset(key, { supabase: config.supabase, openai: config.openai });
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
await configUnset(key);
|
|
154
|
+
}
|
|
137
155
|
});
|
|
138
156
|
configCmd
|
|
139
157
|
.command('list')
|
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']);
|
|
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/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'].includes(delivery)) {
|
|
159
|
+
console.error(`Invalid delivery tier: "${value}". Must be: persona, project, or knowledge.`);
|
|
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/sync.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync, readFileSync, mkdirSync, existsSync, unlinkSync, readdirSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { loadConfigFile, saveConfigFile } from '../lib/config.js';
|
|
4
|
+
import { fetchPersonaNotes, updateNoteContent, updateNoteHash, opAddNote } from '../lib/notes.js';
|
|
4
5
|
import { contentHash } from '../lib/hash.js';
|
|
5
6
|
import { generateClaudeMd, generateMemoryMd } from '../lib/generators.js';
|
|
6
7
|
import { confirm } from '../lib/prompt.js';
|
|
@@ -20,6 +21,8 @@ export async function sync(config, options) {
|
|
|
20
21
|
return result;
|
|
21
22
|
}
|
|
22
23
|
mkdirSync(config.memoryDir, { recursive: true });
|
|
24
|
+
// --- Phase 0: Sync type registry ---
|
|
25
|
+
await syncTypeRegistryPull(config, quiet, force, dryRun);
|
|
23
26
|
const notesByFile = new Map();
|
|
24
27
|
for (const note of notes) {
|
|
25
28
|
const localFile = note.metadata.local_file;
|
|
@@ -191,6 +194,8 @@ export async function sync(config, options) {
|
|
|
191
194
|
console.error(' wrote ~/CLAUDE.md');
|
|
192
195
|
}
|
|
193
196
|
}
|
|
197
|
+
// --- Phase 3.5: Push type registry ---
|
|
198
|
+
await syncTypeRegistryPush(config, quiet, dryRun);
|
|
194
199
|
// --- Summary ---
|
|
195
200
|
if (!quiet) {
|
|
196
201
|
const parts = [
|
|
@@ -204,3 +209,69 @@ export async function sync(config, options) {
|
|
|
204
209
|
}
|
|
205
210
|
return result;
|
|
206
211
|
}
|
|
212
|
+
export async function syncTypeRegistryPush(config, quiet, dryRun) {
|
|
213
|
+
const configFile = loadConfigFile();
|
|
214
|
+
const userTypes = configFile.types;
|
|
215
|
+
if (!userTypes || Object.keys(userTypes).length === 0) {
|
|
216
|
+
return; // Nothing to push
|
|
217
|
+
}
|
|
218
|
+
const content = JSON.stringify(userTypes, null, 2);
|
|
219
|
+
const clients = { supabase: config.supabase, openai: config.openai };
|
|
220
|
+
if (dryRun) {
|
|
221
|
+
if (!quiet)
|
|
222
|
+
console.error(' type-registry — would push to Ledger');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
await opAddNote(clients, content, 'system-rule', 'ledger-sync', {
|
|
226
|
+
upsert_key: 'system-rule-type-registry',
|
|
227
|
+
description: 'User-defined type registry overrides. Managed by ledger sync.',
|
|
228
|
+
delivery: 'persona',
|
|
229
|
+
scope: 'system',
|
|
230
|
+
interactive_skip: true,
|
|
231
|
+
}, true); // force: true to skip duplicate guard
|
|
232
|
+
if (!quiet)
|
|
233
|
+
console.error(' type-registry — pushed to Ledger');
|
|
234
|
+
}
|
|
235
|
+
export async function syncTypeRegistryPull(config, quiet, force, dryRun) {
|
|
236
|
+
const { data: note } = await config.supabase
|
|
237
|
+
.from('notes')
|
|
238
|
+
.select('content')
|
|
239
|
+
.eq('metadata->>upsert_key', 'system-rule-type-registry')
|
|
240
|
+
.limit(1)
|
|
241
|
+
.single();
|
|
242
|
+
if (!note)
|
|
243
|
+
return; // No remote type registry
|
|
244
|
+
let remoteTypes;
|
|
245
|
+
try {
|
|
246
|
+
remoteTypes = JSON.parse(note.content);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
if (!quiet)
|
|
250
|
+
console.error(' type-registry — invalid JSON in remote note, skipping');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const configFile = loadConfigFile();
|
|
254
|
+
const localTypes = configFile.types ?? {};
|
|
255
|
+
// Merge: local wins unless --force
|
|
256
|
+
const merged = force
|
|
257
|
+
? { ...localTypes, ...remoteTypes }
|
|
258
|
+
: { ...remoteTypes, ...localTypes };
|
|
259
|
+
// Check if anything changed
|
|
260
|
+
const localJson = JSON.stringify(localTypes, Object.keys(localTypes).sort());
|
|
261
|
+
const mergedJson = JSON.stringify(merged, Object.keys(merged).sort());
|
|
262
|
+
if (localJson === mergedJson)
|
|
263
|
+
return; // No changes
|
|
264
|
+
if (dryRun) {
|
|
265
|
+
const newKeys = Object.keys(merged).filter(k => !(k in localTypes));
|
|
266
|
+
if (newKeys.length > 0 && !quiet) {
|
|
267
|
+
console.error(` type-registry — would add: ${newKeys.map(k => `${k} (${merged[k]})`).join(', ')}`);
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
configFile.types = merged;
|
|
272
|
+
saveConfigFile(configFile);
|
|
273
|
+
const newKeys = Object.keys(merged).filter(k => !(k in localTypes));
|
|
274
|
+
if (newKeys.length > 0 && !quiet) {
|
|
275
|
+
console.error(` type-registry synced: added ${newKeys.map(k => `${k} (${merged[k]})`).join(', ')}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
package/dist/lib/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createClient } from '@supabase/supabase-js';
|
|
|
3
3
|
import OpenAI from 'openai';
|
|
4
4
|
import { resolve } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
|
-
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
7
|
import { fatal, ExitCode } from './errors.js';
|
|
8
8
|
// --- Defaults ---
|
|
9
9
|
const LEDGER_DIR = resolve(homedir(), '.ledger');
|
|
@@ -28,6 +28,11 @@ export function loadConfigFile() {
|
|
|
28
28
|
}
|
|
29
29
|
return {};
|
|
30
30
|
}
|
|
31
|
+
export function saveConfigFile(config) {
|
|
32
|
+
const configPath = resolve(getLedgerDir(), 'config.json');
|
|
33
|
+
mkdirSync(getLedgerDir(), { recursive: true });
|
|
34
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
35
|
+
}
|
|
31
36
|
export function getDefaultConfig() {
|
|
32
37
|
return {
|
|
33
38
|
memoryDir: DEFAULT_MEMORY_DIR,
|
package/dist/lib/notes.js
CHANGED
|
@@ -1,19 +1,60 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { fatal, ExitCode } from './errors.js';
|
|
3
3
|
import { contentHash } from './hash.js';
|
|
4
|
-
import { loadConfigFile } from './config.js';
|
|
5
|
-
|
|
4
|
+
import { loadConfigFile, saveConfigFile } from './config.js';
|
|
5
|
+
// --- Built-in Type Registry ---
|
|
6
|
+
export const BUILTIN_TYPES = {
|
|
6
7
|
'user-preference': 'persona',
|
|
7
|
-
'
|
|
8
|
+
'persona-rule': 'persona',
|
|
9
|
+
'system-rule': 'persona',
|
|
10
|
+
'code-craft': 'persona',
|
|
8
11
|
'architecture-decision': 'project',
|
|
9
12
|
'project-status': 'project',
|
|
10
|
-
'error': 'project',
|
|
11
13
|
'event': 'project',
|
|
14
|
+
'error': 'project',
|
|
12
15
|
'reference': 'knowledge',
|
|
16
|
+
'knowledge-guide': 'knowledge',
|
|
13
17
|
'general': 'knowledge',
|
|
14
18
|
};
|
|
19
|
+
const TYPE_ALIASES = {
|
|
20
|
+
'feedback': 'general',
|
|
21
|
+
};
|
|
22
|
+
function resolveTypeAlias(type) {
|
|
23
|
+
return TYPE_ALIASES[type] ?? type;
|
|
24
|
+
}
|
|
25
|
+
export function getTypeRegistry() {
|
|
26
|
+
const config = loadConfigFile();
|
|
27
|
+
return { ...BUILTIN_TYPES, ...(config.types ?? {}) };
|
|
28
|
+
}
|
|
15
29
|
export function inferDelivery(noteType) {
|
|
16
|
-
|
|
30
|
+
const resolved = resolveTypeAlias(noteType);
|
|
31
|
+
return getTypeRegistry()[resolved] ?? 'knowledge';
|
|
32
|
+
}
|
|
33
|
+
export function getRegisteredTypes() {
|
|
34
|
+
return Object.keys(getTypeRegistry());
|
|
35
|
+
}
|
|
36
|
+
export function isRegisteredType(noteType) {
|
|
37
|
+
const resolved = resolveTypeAlias(noteType);
|
|
38
|
+
return resolved in getTypeRegistry();
|
|
39
|
+
}
|
|
40
|
+
export function registerType(name, delivery) {
|
|
41
|
+
const config = loadConfigFile();
|
|
42
|
+
if (!config.types)
|
|
43
|
+
config.types = {};
|
|
44
|
+
config.types[name] = delivery;
|
|
45
|
+
saveConfigFile(config);
|
|
46
|
+
}
|
|
47
|
+
export function validateTypeName(name) {
|
|
48
|
+
if (!name || name.length < 2) {
|
|
49
|
+
return `Type name must be at least 2 characters. Got ${name.length}.`;
|
|
50
|
+
}
|
|
51
|
+
if (name.length > 50) {
|
|
52
|
+
return `Type name must be 50 characters or fewer. Got ${name.length}.`;
|
|
53
|
+
}
|
|
54
|
+
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name)) {
|
|
55
|
+
return `Invalid type name "${name}". Use lowercase alphanumeric + hyphens, starting with a letter (e.g., "wine-log").`;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
17
58
|
}
|
|
18
59
|
// --- Queries ---
|
|
19
60
|
export async function fetchPersonaNotes(supabase) {
|
|
@@ -164,12 +205,16 @@ function formatNotePreview(id, meta, content, maxLen = 300) {
|
|
|
164
205
|
const TYPE_PREFIXES = {
|
|
165
206
|
'feedback': ['feedback'],
|
|
166
207
|
'user-preference': ['user'],
|
|
208
|
+
'persona-rule': ['persona-rule'],
|
|
209
|
+
'system-rule': ['system-rule'],
|
|
210
|
+
'code-craft': ['code-craft'],
|
|
167
211
|
'architecture-decision': ['spec', 'architecture'],
|
|
168
212
|
'project-status': ['project-status'],
|
|
169
213
|
'reference': ['reference'],
|
|
170
214
|
'event': ['devlog', 'event'],
|
|
171
215
|
'error': ['errorlog', 'error'],
|
|
172
216
|
'general': ['general'],
|
|
217
|
+
'knowledge-guide': ['knowledge-guide'],
|
|
173
218
|
};
|
|
174
219
|
/**
|
|
175
220
|
* Validate upsert_key format: {prefix}-{topic} or {project}-{prefix}-{topic}
|
|
@@ -224,18 +269,6 @@ function isInteractive() {
|
|
|
224
269
|
const config = loadConfigFile();
|
|
225
270
|
return config.naming?.interactive !== false;
|
|
226
271
|
}
|
|
227
|
-
/** Valid note types for the interactive prompt. */
|
|
228
|
-
export const NOTE_TYPES = [
|
|
229
|
-
'user-preference',
|
|
230
|
-
'feedback',
|
|
231
|
-
'architecture-decision',
|
|
232
|
-
'project-status',
|
|
233
|
-
'reference',
|
|
234
|
-
'event',
|
|
235
|
-
'error',
|
|
236
|
-
'knowledge-guide',
|
|
237
|
-
'general',
|
|
238
|
-
];
|
|
239
272
|
/** Valid statuses for notes. */
|
|
240
273
|
export const NOTE_STATUSES = ['idea', 'planning', 'active', 'done'];
|
|
241
274
|
/**
|
|
@@ -251,8 +284,7 @@ export function checkMetadataCompleteness(metadata, type) {
|
|
|
251
284
|
missing.push('upsert_key');
|
|
252
285
|
}
|
|
253
286
|
// Only ask for status on project-scoped types
|
|
254
|
-
|
|
255
|
-
if (projectTypes.includes(type) && !metadata.status) {
|
|
287
|
+
if (inferDelivery(type) === 'project' && !metadata.status) {
|
|
256
288
|
missing.push('status');
|
|
257
289
|
}
|
|
258
290
|
if (missing.length === 0)
|
|
@@ -367,7 +399,7 @@ export async function opListNotes(clients, limit, type, project) {
|
|
|
367
399
|
}).join('\n\n---\n\n');
|
|
368
400
|
return { status: 'ok', message: formatted };
|
|
369
401
|
}
|
|
370
|
-
export async function opAddNote(clients, content, type, agent, metadata, force) {
|
|
402
|
+
export async function opAddNote(clients, content, type, agent, metadata, force, registerTypeFlag) {
|
|
371
403
|
const upsertKey = metadata.upsert_key;
|
|
372
404
|
const description = metadata.description;
|
|
373
405
|
const skippedInteractive = metadata.interactive_skip === true;
|
|
@@ -381,6 +413,29 @@ export async function opAddNote(clients, content, type, agent, metadata, force)
|
|
|
381
413
|
}
|
|
382
414
|
// Clean up the skip flag before saving
|
|
383
415
|
delete metadata.interactive_skip;
|
|
416
|
+
// --- Type resolution and registration ---
|
|
417
|
+
const resolvedType = resolveTypeAlias(type);
|
|
418
|
+
// If register_type flag is set and type is unknown, register it
|
|
419
|
+
if (registerTypeFlag && !isRegisteredType(resolvedType)) {
|
|
420
|
+
const nameError = validateTypeName(resolvedType);
|
|
421
|
+
if (nameError)
|
|
422
|
+
return { status: 'error', message: nameError };
|
|
423
|
+
const delivery = metadata.delivery || 'knowledge';
|
|
424
|
+
registerType(resolvedType, delivery);
|
|
425
|
+
}
|
|
426
|
+
// If type is still unknown after potential registration, prompt
|
|
427
|
+
if (!isRegisteredType(resolvedType)) {
|
|
428
|
+
const registry = getTypeRegistry();
|
|
429
|
+
const typeListStr = Object.entries(registry)
|
|
430
|
+
.map(([t, d]) => `${t} (${d})`)
|
|
431
|
+
.join(', ');
|
|
432
|
+
return {
|
|
433
|
+
status: 'confirm',
|
|
434
|
+
message: `Type "${type}" is not registered.\n\nOptions:\n1. Register with delivery "knowledge" (default) — re-call add_note with register_type: true\n2. Register with specific delivery — re-call add_note with register_type: true AND set metadata.delivery to "persona", "project", or "knowledge"\n3. Use an existing type instead — re-call add_note with one of: ${typeListStr}\n4. Cancel\n\nAsk the user which option they prefer.`,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
// Use resolved type for the rest of the flow
|
|
438
|
+
type = resolvedType;
|
|
384
439
|
// Naming enforcement (opt-in via config)
|
|
385
440
|
if (isNamingEnforced()) {
|
|
386
441
|
const namingError = validateNaming(upsertKey || '', type, description);
|
package/dist/mcp-server.js
CHANGED
|
@@ -4,7 +4,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import { createClient } from '@supabase/supabase-js';
|
|
5
5
|
import OpenAI from 'openai';
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import { opSearchNotes, opListNotes, opAddNote, opUpdateNote, opUpdateMetadata, opDeleteNote, } from './lib/notes.js';
|
|
7
|
+
import { opSearchNotes, opListNotes, opAddNote, opUpdateNote, opUpdateMetadata, opDeleteNote, getTypeRegistry, } from './lib/notes.js';
|
|
8
8
|
// --- Clients ---
|
|
9
9
|
const supabaseUrl = process.env.SUPABASE_URL;
|
|
10
10
|
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
@@ -21,6 +21,11 @@ const clients = {
|
|
|
21
21
|
supabase: createClient(supabaseUrl, supabaseKey),
|
|
22
22
|
openai: new OpenAI({ apiKey: openaiKey }),
|
|
23
23
|
};
|
|
24
|
+
// Build dynamic type description from registry
|
|
25
|
+
const typeRegistry = getTypeRegistry();
|
|
26
|
+
const typeList = Object.entries(typeRegistry)
|
|
27
|
+
.map(([t, d]) => `${t} (${d})`)
|
|
28
|
+
.join(', ');
|
|
24
29
|
// --- MCP Server ---
|
|
25
30
|
const server = new McpServer({
|
|
26
31
|
name: 'ledger',
|
|
@@ -40,12 +45,13 @@ server.tool('search_notes', 'Search memories by meaning using semantic similarit
|
|
|
40
45
|
// Tool: Add a new note
|
|
41
46
|
server.tool('add_note', 'Save a new memory/note to the knowledge base. Large notes are automatically chunked for embedding. Use upsert_key in metadata to update an existing note instead of creating a duplicate.', {
|
|
42
47
|
content: z.string().describe('The note content to save'),
|
|
43
|
-
type: z.
|
|
48
|
+
type: z.string().describe(`Note type. Registered: ${typeList}. Unknown types will prompt for registration.`),
|
|
44
49
|
agent: z.string().describe('Which agent is saving this note (e.g. claude-code, zhuli)'),
|
|
45
50
|
metadata: z.record(z.string(), z.unknown()).default({}).describe('Optional metadata (project, local_file, upsert_key, etc.)'),
|
|
46
51
|
force: z.boolean().default(false).describe('Skip duplicate check and force creation of a new note'),
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
register_type: z.boolean().default(false).describe('Set to true to register an unknown type before saving. Pass delivery in metadata if not using default (knowledge).'),
|
|
53
|
+
}, async ({ content, type, agent, metadata, force, register_type }) => {
|
|
54
|
+
const result = await opAddNote(clients, content, type, agent, metadata, force, register_type);
|
|
49
55
|
return { content: [{ type: 'text', text: result.message }] };
|
|
50
56
|
});
|
|
51
57
|
// Tool: Update an existing note by ID
|
package/package.json
CHANGED