@aperdomoll90/ledger-ai 1.1.0 → 1.1.2
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 +68 -22
- package/dist/cli.js +10 -3
- package/dist/commands/add.js +58 -4
- package/dist/commands/onboard.js +14 -11
- package/dist/commands/wizard.js +2 -2
- package/dist/lib/notes.js +138 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -15,24 +15,26 @@ Requires Node.js 20+.
|
|
|
15
15
|
## Quick Start
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
#
|
|
18
|
+
# One command does everything:
|
|
19
19
|
ledger init
|
|
20
|
+
```
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
The init wizard walks you through:
|
|
23
|
+
1. **Credentials** — Supabase + OpenAI keys
|
|
24
|
+
2. **Database** — connect and set up schema
|
|
25
|
+
3. **Device** — name this machine (optional)
|
|
26
|
+
4. **Persona** — profile, communication style, rules
|
|
27
|
+
5. **Platforms** — install Claude Code, OpenClaw, or ChatGPT
|
|
28
|
+
6. **Sync** — pull everything down
|
|
29
|
+
7. **Migrate** — detect stray local files
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
ledger onboard
|
|
28
|
-
```
|
|
31
|
+
Smart step-skipping — re-run anytime, it only does what's needed.
|
|
29
32
|
|
|
30
|
-
Second device? Same Ledger, same persona:
|
|
33
|
+
### Second device? Same Ledger, same persona:
|
|
31
34
|
|
|
32
35
|
```bash
|
|
33
36
|
npm install -g @aperdomoll90/ledger-ai
|
|
34
|
-
ledger init
|
|
35
|
-
ledger setup claude # pull persona, install hooks
|
|
37
|
+
ledger init # connect to existing Supabase, pull persona, set up platform
|
|
36
38
|
```
|
|
37
39
|
|
|
38
40
|
## What It Does
|
|
@@ -47,19 +49,53 @@ ledger setup claude # pull persona, install hooks
|
|
|
47
49
|
|
|
48
50
|
| Command | Description |
|
|
49
51
|
|---|---|
|
|
50
|
-
| `ledger init` |
|
|
51
|
-
| `ledger setup <platform>` | Configure an agent (claude, openclaw, chatgpt) |
|
|
52
|
-
| `ledger onboard` | Create your persona (interactive
|
|
52
|
+
| `ledger init` | Guided setup wizard (credentials, persona, platforms, sync) |
|
|
53
|
+
| `ledger setup <platform>` | Configure an agent (claude-code, openclaw, chatgpt) |
|
|
54
|
+
| `ledger onboard` | Create your persona (interactive questionnaire) |
|
|
55
|
+
| `ledger sync` | Bidirectional sync between Ledger and local cache |
|
|
53
56
|
| `ledger pull` | Download notes from Ledger to local cache |
|
|
54
57
|
| `ledger push <file>` | Upload a local file to Ledger |
|
|
55
|
-
| `ledger check` | Compare local files vs Ledger |
|
|
58
|
+
| `ledger check` | Compare local files vs Ledger (dry-run sync) |
|
|
56
59
|
| `ledger show <query>` | Search by meaning, open matching note |
|
|
57
60
|
| `ledger export <query>` | Download a note to any path (untracked) |
|
|
58
61
|
| `ledger ingest [file]` | Add files to Ledger with duplicate detection |
|
|
62
|
+
| `ledger migrate` | Migrate local files to Ledger (backup, compare, merge) |
|
|
63
|
+
| `ledger add` | Add a new note (interactive metadata prompts) |
|
|
64
|
+
| `ledger update <id>` | Update a note by ID |
|
|
65
|
+
| `ledger delete <id>` | Delete a note by ID |
|
|
66
|
+
| `ledger list` | List recent notes |
|
|
67
|
+
| `ledger tag <id>` | Update metadata on a note |
|
|
59
68
|
| `ledger backup` | Backup all notes to ~/.ledger/backups/ |
|
|
60
69
|
| `ledger restore <file>` | Restore from backup |
|
|
61
70
|
| `ledger config list` | View settings |
|
|
62
|
-
|
|
71
|
+
|
|
72
|
+
## Note Metadata
|
|
73
|
+
|
|
74
|
+
Every note has structured metadata for organization and discovery:
|
|
75
|
+
|
|
76
|
+
| Field | Purpose | Example |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `type` | Categorization | `feedback`, `user-preference`, `architecture-decision`, `reference` |
|
|
79
|
+
| `upsert_key` | Unique identifier, dedup | `feedback-communication-style`, `ledger-spec-init` |
|
|
80
|
+
| `description` | What the note IS and what it's FOR | `"How to communicate with Adrian"` |
|
|
81
|
+
| `status` | Lifecycle stage | `idea`, `planning`, `active`, `done` |
|
|
82
|
+
| `project` | Which project | `ledger`, `ai-studio` |
|
|
83
|
+
| `delivery` | Sync tier | `persona` (everywhere), `project` (per-repo), `knowledge` (search only) |
|
|
84
|
+
|
|
85
|
+
### Interactive prompting
|
|
86
|
+
|
|
87
|
+
When creating notes, Ledger prompts for missing metadata by default — both via MCP (agents) and CLI. This helps keep notes organized without requiring users to memorize the schema.
|
|
88
|
+
|
|
89
|
+
Skip prompts with `--force` (CLI) or `interactive_skip: true` (MCP). Disable globally: `ledger config set naming.interactive false`.
|
|
90
|
+
|
|
91
|
+
### Naming enforcement (opt-in)
|
|
92
|
+
|
|
93
|
+
Enable strict naming validation: `ledger config set naming.enforce true`
|
|
94
|
+
|
|
95
|
+
Keys follow the pattern `{prefix}-{topic}` or `{project}-{prefix}-{topic}`:
|
|
96
|
+
- `feedback-communication-style`
|
|
97
|
+
- `ledger-spec-init`
|
|
98
|
+
- `user-profile`
|
|
63
99
|
|
|
64
100
|
## Stack
|
|
65
101
|
|
|
@@ -77,11 +113,11 @@ Your devices / agents
|
|
|
77
113
|
v
|
|
78
114
|
ledger CLI ←→ Supabase (Postgres + pgvector)
|
|
79
115
|
|
|
|
80
|
-
├──
|
|
81
|
-
├──
|
|
82
|
-
├── check: compare hashes, detect drift
|
|
116
|
+
├── init: guided wizard (credentials → persona → platforms → sync)
|
|
117
|
+
├── sync: bidirectional, hash-based conflict detection
|
|
83
118
|
├── show: semantic search → open in editor
|
|
84
|
-
|
|
119
|
+
├── add: interactive metadata prompts → duplicate guard → save
|
|
120
|
+
└── MCP server: Claude Code talks to Ledger natively (6 tools)
|
|
85
121
|
```
|
|
86
122
|
|
|
87
123
|
Notes are stored with content + metadata + vector embeddings. Search finds relevant notes by meaning. Sync uses SHA-256 content hashes to detect changes without markers.
|
|
@@ -94,6 +130,15 @@ Notes are stored with content + metadata + vector embeddings. Search finds relev
|
|
|
94
130
|
| OpenClaw | CLI | Bidirectional via `ledger` commands |
|
|
95
131
|
| ChatGPT | None | Static snapshot, re-run to update |
|
|
96
132
|
|
|
133
|
+
Platform management is built into the wizard — install, reinstall, or uninstall from `ledger init`.
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm run ship # typecheck → test → commit → push
|
|
139
|
+
npm run release # version bump → build → test → publish to npm
|
|
140
|
+
```
|
|
141
|
+
|
|
97
142
|
## Requirements
|
|
98
143
|
|
|
99
144
|
- Node.js 20+
|
|
@@ -102,9 +147,9 @@ Notes are stored with content + metadata + vector embeddings. Search finds relev
|
|
|
102
147
|
|
|
103
148
|
## Known Limitations
|
|
104
149
|
|
|
105
|
-
- **Embeddings require OpenAI** — currently uses `text-embedding-3-small` only.
|
|
150
|
+
- **Embeddings require OpenAI** — currently uses `text-embedding-3-small` only. Multi-provider support planned.
|
|
106
151
|
- **Anthropic has no embedding API** — Claude is for text generation. Even Claude users need an OpenAI key for embeddings.
|
|
107
|
-
- **English-optimized** — semantic search works best with English content.
|
|
152
|
+
- **English-optimized** — semantic search works best with English content.
|
|
108
153
|
|
|
109
154
|
## Roadmap
|
|
110
155
|
|
|
@@ -113,6 +158,7 @@ Notes are stored with content + metadata + vector embeddings. Search finds relev
|
|
|
113
158
|
- Soft delete (trash)
|
|
114
159
|
- Multi-format ingest (PDF, Excel, images, audio)
|
|
115
160
|
- Web dashboard
|
|
161
|
+
- Skills system (context-aware convention enforcement)
|
|
116
162
|
- VS Code extension
|
|
117
163
|
|
|
118
164
|
## License
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
+
import { createRequire } from 'module';
|
|
3
4
|
import { loadConfig } from './lib/config.js';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const { version } = require('../package.json');
|
|
4
7
|
import { pull } from './commands/pull.js';
|
|
5
8
|
import { push } from './commands/push.js';
|
|
6
9
|
import { check } from './commands/check.js';
|
|
@@ -29,7 +32,7 @@ const program = new Command();
|
|
|
29
32
|
program
|
|
30
33
|
.name('ledger')
|
|
31
34
|
.description('AI identity and memory system — sync knowledge across agents and devices')
|
|
32
|
-
.version(
|
|
35
|
+
.version(version);
|
|
33
36
|
program
|
|
34
37
|
.command('pull')
|
|
35
38
|
.description('Download notes from Ledger to local cache')
|
|
@@ -189,11 +192,13 @@ program
|
|
|
189
192
|
.command('add')
|
|
190
193
|
.description('Add a new note to Ledger (with duplicate detection)')
|
|
191
194
|
.requiredOption('-c, --content <content>', 'note content (or use stdin)')
|
|
192
|
-
.
|
|
195
|
+
.option('-t, --type <type>', 'note type (feedback, reference, event, etc.)')
|
|
193
196
|
.option('-a, --agent <agent>', 'agent name', 'cli')
|
|
194
197
|
.option('-p, --project <project>', 'project name')
|
|
195
198
|
.option('-k, --upsert-key <key>', 'upsert key for dedup')
|
|
196
|
-
.option('-
|
|
199
|
+
.option('-d, --description <text>', 'one-line description of the note')
|
|
200
|
+
.option('-s, --status <status>', 'note status (idea, planning, active, done)')
|
|
201
|
+
.option('-f, --force', 'skip duplicate check and interactive prompts')
|
|
197
202
|
.action(async (options) => {
|
|
198
203
|
const config = loadConfig();
|
|
199
204
|
await add(config, options.content, {
|
|
@@ -201,6 +206,8 @@ program
|
|
|
201
206
|
agent: options.agent,
|
|
202
207
|
project: options.project,
|
|
203
208
|
upsertKey: options.upsertKey,
|
|
209
|
+
description: options.description,
|
|
210
|
+
status: options.status,
|
|
204
211
|
force: options.force ?? false,
|
|
205
212
|
});
|
|
206
213
|
});
|
package/dist/commands/add.js
CHANGED
|
@@ -1,17 +1,71 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { loadConfigFile } from '../lib/config.js';
|
|
2
|
+
import { opAddNote, NOTE_TYPES, NOTE_STATUSES } from '../lib/notes.js';
|
|
3
|
+
import { ask, confirm, choose } from '../lib/prompt.js';
|
|
3
4
|
export async function add(config, content, options) {
|
|
5
|
+
const configFile = loadConfigFile();
|
|
6
|
+
const interactive = configFile.naming?.interactive !== false;
|
|
7
|
+
let type = options.type || '';
|
|
4
8
|
const metadata = {};
|
|
5
9
|
if (options.project)
|
|
6
10
|
metadata.project = options.project;
|
|
7
11
|
if (options.upsertKey)
|
|
8
12
|
metadata.upsert_key = options.upsertKey;
|
|
9
|
-
|
|
13
|
+
if (options.description)
|
|
14
|
+
metadata.description = options.description;
|
|
15
|
+
if (options.status)
|
|
16
|
+
metadata.status = options.status;
|
|
17
|
+
// Interactive prompting for missing fields (CLI only)
|
|
18
|
+
if (interactive && !options.force) {
|
|
19
|
+
// Type
|
|
20
|
+
if (!type) {
|
|
21
|
+
const typeChoice = await choose('What type of note is this?', [
|
|
22
|
+
...NOTE_TYPES,
|
|
23
|
+
'skip — use default (general)',
|
|
24
|
+
]);
|
|
25
|
+
type = typeChoice.startsWith('skip') ? 'general' : typeChoice;
|
|
26
|
+
}
|
|
27
|
+
// Description
|
|
28
|
+
if (!metadata.description) {
|
|
29
|
+
const desc = await ask('One-line description (what is this note for?): ');
|
|
30
|
+
if (desc)
|
|
31
|
+
metadata.description = desc;
|
|
32
|
+
}
|
|
33
|
+
// upsert_key
|
|
34
|
+
if (!metadata.upsert_key) {
|
|
35
|
+
const key = await ask('Unique key for this note (lowercase-hyphenated, or Enter to auto-generate): ');
|
|
36
|
+
if (key)
|
|
37
|
+
metadata.upsert_key = key;
|
|
38
|
+
}
|
|
39
|
+
// Project
|
|
40
|
+
if (!metadata.project) {
|
|
41
|
+
const proj = await ask('Project name (or Enter to skip): ');
|
|
42
|
+
if (proj)
|
|
43
|
+
metadata.project = proj;
|
|
44
|
+
}
|
|
45
|
+
// Status (only for project-scoped types)
|
|
46
|
+
const projectTypes = ['architecture-decision', 'project-status', 'event', 'error'];
|
|
47
|
+
if (projectTypes.includes(type) && !metadata.status) {
|
|
48
|
+
const statusChoice = await choose('What stage is this?', [
|
|
49
|
+
...NOTE_STATUSES,
|
|
50
|
+
'skip — no status',
|
|
51
|
+
]);
|
|
52
|
+
if (!statusChoice.startsWith('skip')) {
|
|
53
|
+
metadata.status = statusChoice;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Default type if still empty
|
|
58
|
+
if (!type)
|
|
59
|
+
type = 'general';
|
|
60
|
+
// Mark as having gone through interactive (or skipped it)
|
|
61
|
+
// so opAddNote doesn't re-prompt via MCP confirm flow
|
|
62
|
+
metadata.interactive_skip = true;
|
|
63
|
+
const result = await opAddNote({ supabase: config.supabase, openai: config.openai }, content, type, options.agent || 'cli', metadata, options.force ?? false);
|
|
10
64
|
if (result.status === 'confirm') {
|
|
11
65
|
console.error(result.message);
|
|
12
66
|
const proceed = await confirm('\nCreate new note anyway?');
|
|
13
67
|
if (proceed) {
|
|
14
|
-
const forced = await opAddNote({ supabase: config.supabase, openai: config.openai }, content,
|
|
68
|
+
const forced = await opAddNote({ supabase: config.supabase, openai: config.openai }, content, type, options.agent || 'cli', { ...metadata, interactive_skip: true }, true);
|
|
15
69
|
console.error(forced.message);
|
|
16
70
|
}
|
|
17
71
|
else {
|
package/dist/commands/onboard.js
CHANGED
|
@@ -34,22 +34,25 @@ const DEFAULT_SECURITY_RULES = `- Never read .env files or any files containing
|
|
|
34
34
|
const DEFAULT_KNOWLEDGE_RULES = `- Ledger is the source of truth for all knowledge
|
|
35
35
|
- Local files are cache — update Ledger first, then local
|
|
36
36
|
- Use ledger CLI for syncing between Ledger and local files`;
|
|
37
|
-
|
|
38
|
-
export async function onboard(config) {
|
|
37
|
+
export async function onboard(config, options = {}) {
|
|
39
38
|
const envPath = resolve(getLedgerDir(), '.env');
|
|
40
39
|
if (!existsSync(envPath)) {
|
|
41
40
|
console.error('Ledger not initialized. Run `ledger init` first.');
|
|
42
41
|
process.exit(1);
|
|
43
42
|
}
|
|
44
|
-
// Check if persona already exists
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
console.error('
|
|
52
|
-
|
|
43
|
+
// Check if persona already exists (skip when called from wizard, which handles this itself)
|
|
44
|
+
if (!options.skipExistingCheck) {
|
|
45
|
+
const existing = await fetchPersonaNotes(config.supabase);
|
|
46
|
+
const hasProfile = existing.some(n => n.metadata.type === 'user-preference');
|
|
47
|
+
const hasFeedback = existing.some(n => n.metadata.type === 'feedback');
|
|
48
|
+
if (hasProfile || hasFeedback) {
|
|
49
|
+
console.error('You already have persona notes in Ledger (profile, communication style, etc.).');
|
|
50
|
+
console.error('Running onboarding again will create new notes for any missing fields — existing ones are kept as-is.\n');
|
|
51
|
+
const proceed = await confirm('Continue?');
|
|
52
|
+
if (!proceed) {
|
|
53
|
+
console.error('Cancelled.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
console.error('\nLet\'s set up your AI persona.\n');
|
package/dist/commands/wizard.js
CHANGED
|
@@ -91,12 +91,12 @@ export async function wizard() {
|
|
|
91
91
|
console.error(`Step 4: Persona: found\n`);
|
|
92
92
|
const update = await confirm(' Update persona?');
|
|
93
93
|
if (update) {
|
|
94
|
-
await onboard(config);
|
|
94
|
+
await onboard(config, { skipExistingCheck: true });
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
else {
|
|
98
98
|
console.error('Step 4: Build persona\n');
|
|
99
|
-
await runSkippable('Persona', () => onboard(config));
|
|
99
|
+
await runSkippable('Persona', () => onboard(config, { skipExistingCheck: true }));
|
|
100
100
|
}
|
|
101
101
|
// Step 5: Platforms
|
|
102
102
|
console.error('Step 5: Platform setup\n');
|
package/dist/lib/notes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
4
5
|
const DELIVERY_BY_TYPE = {
|
|
5
6
|
'user-preference': 'persona',
|
|
6
7
|
'feedback': 'persona',
|
|
@@ -158,6 +159,116 @@ function formatNotePreview(id, meta, content, maxLen = 300) {
|
|
|
158
159
|
const descLine = desc ? `\nDescription: ${desc}` : '';
|
|
159
160
|
return `"${label}" (id: ${id}) | type: ${noteType || '-'} | project: ${project || '-'}${descLine}\n ${preview}${truncated}`;
|
|
160
161
|
}
|
|
162
|
+
// --- Naming Conventions ---
|
|
163
|
+
/** Valid type prefixes for upsert_key naming. */
|
|
164
|
+
const TYPE_PREFIXES = {
|
|
165
|
+
'feedback': ['feedback'],
|
|
166
|
+
'user-preference': ['user'],
|
|
167
|
+
'architecture-decision': ['spec', 'architecture'],
|
|
168
|
+
'project-status': ['project-status'],
|
|
169
|
+
'reference': ['reference'],
|
|
170
|
+
'event': ['devlog', 'event'],
|
|
171
|
+
'error': ['errorlog', 'error'],
|
|
172
|
+
'general': ['general'],
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Validate upsert_key format: {prefix}-{topic} or {project}-{prefix}-{topic}
|
|
176
|
+
* Returns null if valid, error message if invalid.
|
|
177
|
+
*/
|
|
178
|
+
export function validateNaming(upsertKey, type, description) {
|
|
179
|
+
if (!upsertKey) {
|
|
180
|
+
return 'upsert_key is required when naming enforcement is enabled.';
|
|
181
|
+
}
|
|
182
|
+
if (!description) {
|
|
183
|
+
return 'description is required when naming enforcement is enabled. Add a one-line description of what this note IS and what it\'s FOR.';
|
|
184
|
+
}
|
|
185
|
+
// Check format: lowercase, hyphens, no underscores or special chars
|
|
186
|
+
if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(upsertKey)) {
|
|
187
|
+
return `Invalid upsert_key format "${upsertKey}". Use lowercase-hyphenated (e.g., "feedback-communication-style").`;
|
|
188
|
+
}
|
|
189
|
+
// Check that the key contains a valid prefix for this type
|
|
190
|
+
const validPrefixes = TYPE_PREFIXES[type];
|
|
191
|
+
if (validPrefixes) {
|
|
192
|
+
const parts = upsertKey.split('-');
|
|
193
|
+
// Match prefix at start, or after a project name segment
|
|
194
|
+
const hasValidPrefix = validPrefixes.some(prefix => {
|
|
195
|
+
const prefixParts = prefix.split('-');
|
|
196
|
+
// Direct match: prefix-topic
|
|
197
|
+
if (parts.slice(0, prefixParts.length).join('-') === prefix)
|
|
198
|
+
return true;
|
|
199
|
+
// Project-scoped: project-prefix or project-prefix-topic
|
|
200
|
+
if (parts.length >= prefixParts.length + 1) {
|
|
201
|
+
const afterProject = parts.slice(1, 1 + prefixParts.length).join('-');
|
|
202
|
+
if (afterProject === prefix)
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
});
|
|
207
|
+
if (!hasValidPrefix) {
|
|
208
|
+
return `upsert_key "${upsertKey}" doesn't match type "${type}". Expected prefix: ${validPrefixes.join(' or ')}. Examples: "${validPrefixes[0]}-my-topic" or "myproject-${validPrefixes[0]}-my-topic".`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
/** Derive local_file from upsert_key: feedback-style → feedback_style.md */
|
|
214
|
+
export function deriveLocalFile(upsertKey) {
|
|
215
|
+
return upsertKey.replace(/-/g, '_') + '.md';
|
|
216
|
+
}
|
|
217
|
+
/** Check if naming enforcement is enabled in config. */
|
|
218
|
+
function isNamingEnforced() {
|
|
219
|
+
const config = loadConfigFile();
|
|
220
|
+
return config.naming?.enforce === true;
|
|
221
|
+
}
|
|
222
|
+
/** Check if interactive metadata prompting is enabled (default: true). */
|
|
223
|
+
function isInteractive() {
|
|
224
|
+
const config = loadConfigFile();
|
|
225
|
+
return config.naming?.interactive !== false;
|
|
226
|
+
}
|
|
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
|
+
/** Valid statuses for notes. */
|
|
240
|
+
export const NOTE_STATUSES = ['idea', 'planning', 'active', 'done'];
|
|
241
|
+
/**
|
|
242
|
+
* Check if metadata is complete enough to skip interactive prompting.
|
|
243
|
+
* Returns null if complete, or a structured prompt message if fields are missing.
|
|
244
|
+
*/
|
|
245
|
+
export function checkMetadataCompleteness(metadata, type) {
|
|
246
|
+
const missing = [];
|
|
247
|
+
if (!metadata.description) {
|
|
248
|
+
missing.push('description');
|
|
249
|
+
}
|
|
250
|
+
if (!metadata.upsert_key) {
|
|
251
|
+
missing.push('upsert_key');
|
|
252
|
+
}
|
|
253
|
+
// Only ask for status on project-scoped types
|
|
254
|
+
const projectTypes = ['architecture-decision', 'project-status', 'event', 'error'];
|
|
255
|
+
if (projectTypes.includes(type) && !metadata.status) {
|
|
256
|
+
missing.push('status');
|
|
257
|
+
}
|
|
258
|
+
if (missing.length === 0)
|
|
259
|
+
return null;
|
|
260
|
+
const fields = [];
|
|
261
|
+
if (missing.includes('description')) {
|
|
262
|
+
fields.push('- **description**: One line explaining what this note IS and what it\'s FOR');
|
|
263
|
+
}
|
|
264
|
+
if (missing.includes('upsert_key')) {
|
|
265
|
+
fields.push('- **upsert_key**: A unique identifier for this note (lowercase-hyphenated, e.g., "feedback-my-rule" or "myproject-spec-feature")');
|
|
266
|
+
}
|
|
267
|
+
if (missing.includes('status')) {
|
|
268
|
+
fields.push('- **status**: What stage is this? Options: idea, planning, active, done');
|
|
269
|
+
}
|
|
270
|
+
return `METADATA NEEDED — ask the user for these fields before saving:\n\n${fields.join('\n')}\n\nIf the user wants to skip, re-call add_note with metadata field \`interactive_skip: true\` to use defaults.`;
|
|
271
|
+
}
|
|
161
272
|
// --- Shared Operations (called by both MCP and CLI) ---
|
|
162
273
|
export async function opSearchNotes(clients, query, threshold, limit, type, project) {
|
|
163
274
|
const embedding = await getEmbedding(clients.openai, query);
|
|
@@ -257,8 +368,34 @@ export async function opListNotes(clients, limit, type, project) {
|
|
|
257
368
|
return { status: 'ok', message: formatted };
|
|
258
369
|
}
|
|
259
370
|
export async function opAddNote(clients, content, type, agent, metadata, force) {
|
|
260
|
-
const fullMetadata = { ...metadata, type, agent, content_hash: contentHash(content) };
|
|
261
371
|
const upsertKey = metadata.upsert_key;
|
|
372
|
+
const description = metadata.description;
|
|
373
|
+
const skippedInteractive = metadata.interactive_skip === true;
|
|
374
|
+
// Interactive metadata prompting (default: on, opt-out via config)
|
|
375
|
+
// Skip if user explicitly opted out via interactive_skip flag
|
|
376
|
+
if (!skippedInteractive && isInteractive()) {
|
|
377
|
+
const prompt = checkMetadataCompleteness(metadata, type);
|
|
378
|
+
if (prompt) {
|
|
379
|
+
return { status: 'confirm', message: prompt };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Clean up the skip flag before saving
|
|
383
|
+
delete metadata.interactive_skip;
|
|
384
|
+
// Naming enforcement (opt-in via config)
|
|
385
|
+
if (isNamingEnforced()) {
|
|
386
|
+
const namingError = validateNaming(upsertKey || '', type, description);
|
|
387
|
+
if (namingError) {
|
|
388
|
+
return { status: 'error', message: `Naming violation: ${namingError}` };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Auto-derive local_file from upsert_key for persona notes
|
|
392
|
+
if (upsertKey && !metadata.local_file) {
|
|
393
|
+
const delivery = metadata.delivery || inferDelivery(type);
|
|
394
|
+
if (delivery === 'persona') {
|
|
395
|
+
metadata.local_file = deriveLocalFile(upsertKey);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const fullMetadata = { ...metadata, type, agent, content_hash: contentHash(content) };
|
|
262
399
|
// Duplicate guard: if no upsert_key and not forced, check for similar notes
|
|
263
400
|
if (!upsertKey && !force) {
|
|
264
401
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aperdomoll90/ledger-ai",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "AI identity and memory system — portable persona, knowledge sync, semantic search across agents and devices",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,10 @@
|
|
|
18
18
|
"prepublishOnly": "npm run build && npm test",
|
|
19
19
|
"test": "vitest run",
|
|
20
20
|
"test:watch": "vitest",
|
|
21
|
-
"typecheck": "tsc --noEmit"
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"preversion": "npm run typecheck && npm test",
|
|
23
|
+
"ship": "bash scripts/ship.sh",
|
|
24
|
+
"release": "npm version patch && npm publish --access public"
|
|
22
25
|
},
|
|
23
26
|
"keywords": [
|
|
24
27
|
"ai",
|