@aperdomoll90/ledger-ai 1.3.0 → 1.4.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.
Files changed (84) hide show
  1. package/dist/cli.js +177 -221
  2. package/dist/commands/add.js +51 -100
  3. package/dist/commands/backfill.js +55 -0
  4. package/dist/commands/backup.js +10 -10
  5. package/dist/commands/check.js +21 -29
  6. package/dist/commands/config.js +13 -12
  7. package/dist/commands/delete.js +22 -17
  8. package/dist/commands/eval-judge.js +11 -0
  9. package/dist/commands/eval.js +321 -0
  10. package/dist/commands/export.js +8 -10
  11. package/dist/commands/get.js +9 -0
  12. package/dist/commands/hunt.js +206 -0
  13. package/dist/commands/ingest.js +15 -14
  14. package/dist/commands/init.js +18 -20
  15. package/dist/commands/list.js +21 -7
  16. package/dist/commands/migrate.js +11 -11
  17. package/dist/commands/onboard.js +2 -2
  18. package/dist/commands/pull.js +3 -2
  19. package/dist/commands/push.js +8 -8
  20. package/dist/commands/restore.js +38 -38
  21. package/dist/commands/show.js +13 -16
  22. package/dist/commands/sync.js +58 -19
  23. package/dist/commands/tag.js +20 -14
  24. package/dist/commands/update.js +50 -18
  25. package/dist/commands/wizard.js +3 -3
  26. package/dist/lib/ai-search.js +163 -0
  27. package/dist/lib/audit.js +19 -0
  28. package/dist/lib/backfill.js +60 -0
  29. package/dist/lib/config.js +19 -2
  30. package/dist/lib/document-classification.js +5 -0
  31. package/dist/lib/document-fetching.js +77 -0
  32. package/dist/lib/document-operations.js +150 -0
  33. package/dist/lib/documents/classification.js +5 -0
  34. package/dist/lib/documents/fetching.js +89 -0
  35. package/dist/lib/documents/operations.js +304 -0
  36. package/dist/lib/domains.js +116 -0
  37. package/dist/lib/embeddings.js +190 -0
  38. package/dist/lib/errors.js +3 -1
  39. package/dist/lib/eval/eval-advanced.js +289 -0
  40. package/dist/lib/eval/eval-judge-session.js +233 -0
  41. package/dist/lib/eval/eval-store.js +105 -0
  42. package/dist/lib/eval/eval.js +303 -0
  43. package/dist/lib/file-writer.js +23 -0
  44. package/dist/lib/generators.js +44 -45
  45. package/dist/lib/hunter-db.js +235 -0
  46. package/dist/lib/hunter-rss.js +30 -0
  47. package/dist/lib/hunter-scoring.js +55 -0
  48. package/dist/lib/hunter-types.js +36 -0
  49. package/dist/lib/lint-configs.js +20 -0
  50. package/dist/lib/migrate.js +2 -2
  51. package/dist/lib/notes.js +173 -59
  52. package/dist/lib/observability.js +296 -0
  53. package/dist/lib/op-add-note-types.test.js +7 -6
  54. package/dist/lib/prompt.js +8 -8
  55. package/dist/lib/rate-limiter.js +103 -0
  56. package/dist/lib/search/ai-search.js +396 -0
  57. package/dist/lib/search/chunk-context-enrichment.js +155 -0
  58. package/dist/lib/search/embeddings.js +293 -0
  59. package/dist/lib/search/reranker.js +120 -0
  60. package/dist/lib/search/semantic-cache.js +53 -0
  61. package/dist/lib/type-registry.test.js +6 -6
  62. package/dist/mcp-server.js +553 -66
  63. package/dist/migrations/migrations/005-audit-log.sql +22 -0
  64. package/dist/migrations/migrations/005_opportunities.sql +48 -0
  65. package/dist/migrations/migrations/006-audited-operations.sql +235 -0
  66. package/dist/migrations/migrations/006_hunt_analytics.sql +38 -0
  67. package/dist/migrations/migrations/007-eval-golden-judgments.sql +119 -0
  68. package/dist/migrations/migrations/008-drop-expected-doc-ids.sql +9 -0
  69. package/dist/migrations/migrations/008-judge-helpers.sql +21 -0
  70. package/dist/migrations/migrations/009-semantic-cache.sql +216 -0
  71. package/dist/scripts/batch-grade.js +344 -0
  72. package/dist/scripts/benchmark-ingestion.js +376 -0
  73. package/dist/scripts/convert-judgments-to-graded.js +88 -0
  74. package/dist/scripts/diagnose-first-result.js +333 -0
  75. package/dist/scripts/drop-golden-query.js +53 -0
  76. package/dist/scripts/eval-search.js +115 -0
  77. package/dist/scripts/grade-unjudged-top1.js +138 -0
  78. package/dist/scripts/hunter-analytics.js +38 -0
  79. package/dist/scripts/hunter-cron.js +63 -0
  80. package/dist/scripts/hunter-purge.js +25 -0
  81. package/dist/scripts/migrate-v2.js +140 -0
  82. package/dist/scripts/reindex.js +74 -0
  83. package/dist/scripts/sync-local-docs.js +153 -0
  84. package/package.json +7 -1
package/dist/cli.js CHANGED
@@ -1,110 +1,209 @@
1
1
  #!/usr/bin/env node
2
+ // cli.ts — Ledger CLI entry point.
3
+ // 14 commands for managing the RAG knowledge base.
2
4
  import { Command } from 'commander';
3
5
  import { createRequire } from 'module';
6
+ import { randomUUID } from 'node:crypto';
4
7
  import { loadConfig } from './lib/config.js';
8
+ import { shutdownObservability } from './lib/observability.js';
9
+ // Per-invocation session ID so all searches from this CLI run group together
10
+ // in the Langfuse dashboard.
11
+ const CLI_SESSION_ID = `cli-${randomUUID()}`;
12
+ const CLI_ENVIRONMENT = process.env.NODE_ENV ?? 'development';
13
+ const CLI_CONTEXT = { sessionId: CLI_SESSION_ID, observabilityEnvironment: CLI_ENVIRONMENT };
5
14
  const require = createRequire(import.meta.url);
6
15
  const { version } = require('../package.json');
7
- import { pull } from './commands/pull.js';
8
- import { push } from './commands/push.js';
9
- import { check, checkChunks } from './commands/check.js';
10
- import { sync } from './commands/sync.js';
11
- import { show } from './commands/show.js';
12
- import { exportNote } from './commands/export.js';
13
- import { ingest } from './commands/ingest.js';
16
+ // Commands
14
17
  import { init } from './commands/init.js';
15
- import { wizard } from './commands/wizard.js';
16
- import { setupClaudeCode, setupOpenclaw, setupChatgpt } from './commands/setup.js';
17
- import { backup, enableBackupCron, disableBackupCron } from './commands/backup.js';
18
- import { restore } from './commands/restore.js';
19
- import { onboard } from './commands/onboard.js';
20
- import { configGet, configSet, configUnset, configList } from './commands/config.js';
21
- import { migrate } from './commands/migrate.js';
22
- import { add } from './commands/add.js';
23
- import { update } from './commands/update.js';
24
- import { deleteNote } from './commands/delete.js';
18
+ import { addDocumentFromFile } from './commands/add.js';
25
19
  import { list } from './commands/list.js';
20
+ import { show } from './commands/show.js';
21
+ import { get } from './commands/get.js';
22
+ import { exportDocument } from './commands/export.js';
23
+ import { push } from './commands/push.js';
24
+ import { updateFromFile } from './commands/update.js';
25
+ import { removeDocument } from './commands/delete.js';
26
26
  import { tag } from './commands/tag.js';
27
+ import { check } from './commands/check.js';
28
+ import { backup, enableBackupCron, disableBackupCron } from './commands/backup.js';
29
+ import { restore } from './commands/restore.js';
27
30
  import { lint } from './commands/lint.js';
28
- process.on('unhandledRejection', (err) => {
29
- console.error(err instanceof Error ? err.message : String(err));
31
+ import { evalSearch, sweepThreshold, showEvalRun } from './commands/eval.js';
32
+ import { evalJudge } from './commands/eval-judge.js';
33
+ process.on('unhandledRejection', (rejection) => {
34
+ console.error(rejection instanceof Error ? rejection.message : String(rejection));
30
35
  process.exit(1);
31
36
  });
37
+ // Flush pending Langfuse traces before exit
38
+ const shutdown = async () => {
39
+ await shutdownObservability();
40
+ process.exit(0);
41
+ };
42
+ process.on('SIGINT', shutdown);
43
+ process.on('SIGTERM', shutdown);
32
44
  const program = new Command();
33
45
  program
34
46
  .name('ledger')
35
- .description('AI identity and memory system — sync knowledge across agents and devices')
47
+ .description('AI identity and memory system — RAG-powered knowledge base for agents')
36
48
  .version(version);
49
+ // =============================================================================
50
+ // Document management
51
+ // =============================================================================
37
52
  program
38
- .command('pull')
39
- .description('Download notes from Ledger to local cache')
40
- .option('-q, --quiet', 'suppress non-conflict output')
41
- .option('-f, --force', 'overwrite local changes without conflict check')
53
+ .command('add')
54
+ .description('Add a new document to Ledger by reading content from a file on disk (auto-verified after create)')
55
+ .requiredOption('-f, --file <file>', 'absolute or relative path to the file containing the document content')
56
+ .requiredOption('-n, --name <name>', 'unique document name (lowercase, hyphens)')
57
+ .option('-d, --domain <domain>', 'domain: system, persona, workspace, project, general', 'general')
58
+ .option('-t, --type <type>', 'document type (architecture, reference, knowledge, etc.)', 'knowledge')
59
+ .option('-p, --project <project>', 'project name')
60
+ .option('--description <text>', 'one-line description')
61
+ .option('-a, --agent <agent>', 'agent name', 'cli')
62
+ .option('-s, --status <status>', 'status: idea, planning, active, done')
63
+ .option('--protection <level>', 'protection: open, guarded, protected, immutable')
42
64
  .action(async (options) => {
43
- const config = loadConfig();
44
- await pull(config, { quiet: options.quiet ?? false, force: options.force ?? false });
45
- });
46
- program
47
- .command('push <file>')
48
- .description('Upload a local file to Ledger')
49
- .action(async (file) => {
50
- const config = loadConfig();
51
- await push(config, file);
52
- });
53
- program
54
- .command('check')
55
- .description('Compare local files vs Ledger, report sync status')
56
- .option('--chunks', 'Check chunk group integrity')
57
- .action(async (opts) => {
58
- const config = loadConfig();
59
- if (opts.chunks) {
60
- await checkChunks(config);
61
- }
62
- else {
63
- await check(config);
64
- }
65
+ const config = loadConfig(CLI_CONTEXT);
66
+ await addDocumentFromFile(config, {
67
+ filePath: options.file,
68
+ name: options.name,
69
+ domain: options.domain,
70
+ documentType: options.type,
71
+ project: options.project,
72
+ description: options.description,
73
+ agent: options.agent,
74
+ status: options.status,
75
+ protection: options.protection,
76
+ });
65
77
  });
66
78
  program
67
- .command('sync')
68
- .description('Bidirectional sync of persona files between Ledger and local cache')
69
- .option('-q, --quiet', 'suppress output unless conflicts (for hooks)')
70
- .option('-f, --force', 'overwrite local with Ledger version')
71
- .option('-n, --dry-run', 'show what would happen without doing it')
79
+ .command('list')
80
+ .description('List recent documents from Ledger')
81
+ .option('-n, --limit <number>', 'number of documents', '20')
82
+ .option('-t, --type <type>', 'filter by document type')
83
+ .option('-p, --project <project>', 'filter by project name')
72
84
  .action(async (options) => {
73
- const config = loadConfig();
74
- await sync(config, {
75
- quiet: options.quiet ?? false,
76
- force: options.force ?? false,
77
- dryRun: options.dryRun ?? false,
85
+ const config = loadConfig(CLI_CONTEXT);
86
+ await list(config, {
87
+ limit: parseInt(options.limit, 10),
88
+ type: options.type,
89
+ project: options.project,
78
90
  });
79
91
  });
92
+ program
93
+ .command('get <name>')
94
+ .description('Fetch a document by exact name and print its content')
95
+ .action(async (name) => {
96
+ const config = loadConfig(CLI_CONTEXT);
97
+ await get(config, name);
98
+ });
80
99
  program
81
100
  .command('show <query...>')
82
- .description('Search Ledger by meaning, open matching note')
83
- .option('-t, --type <type>', 'filter by note type (e.g. feedback, reference)')
101
+ .description('Search Ledger by meaning, open matching document')
102
+ .option('-t, --type <type>', 'filter by document type')
84
103
  .option('-p, --project <project>', 'filter by project name')
85
104
  .action(async (queryParts, options) => {
86
- const config = loadConfig();
105
+ const config = loadConfig(CLI_CONTEXT);
87
106
  await show(config, queryParts.join(' '), { type: options.type, project: options.project });
88
107
  });
89
108
  program
90
109
  .command('export <query...>')
91
- .description('Download a note to a custom location (untracked)')
110
+ .description('Download a document to a file')
92
111
  .option('-o, --output <path>', 'output directory (default: current directory)')
93
112
  .action(async (queryParts, options) => {
94
- const config = loadConfig();
95
- await exportNote(config, queryParts.join(' '), options.output);
113
+ const config = loadConfig(CLI_CONTEXT);
114
+ await exportDocument(config, queryParts.join(' '), options.output);
115
+ });
116
+ program
117
+ .command('push <file>')
118
+ .description('Upload a local file to Ledger')
119
+ .action(async (file) => {
120
+ const config = loadConfig(CLI_CONTEXT);
121
+ await push(config, file);
122
+ });
123
+ program
124
+ .command('update <id>')
125
+ .description('Update a document by ID by reading new content from a file on disk (auto-verified after push)')
126
+ .requiredOption('-f, --file <file>', 'absolute or relative path to the file containing the new content')
127
+ .option('-y, --yes', 'skip the interactive confirmation prompt (for non-interactive scripts)')
128
+ .action(async (documentId, options) => {
129
+ const config = loadConfig(CLI_CONTEXT);
130
+ await updateFromFile(config, parseInt(documentId, 10), options.file, { yes: options.yes });
131
+ });
132
+ program
133
+ .command('delete <id>')
134
+ .description('Soft-delete a document by ID (restorable within 30 days)')
135
+ .option('-y, --yes', 'skip the interactive confirmation prompt (for non-interactive scripts)')
136
+ .action(async (documentId, options) => {
137
+ const config = loadConfig(CLI_CONTEXT);
138
+ await removeDocument(config, parseInt(documentId, 10), { yes: options.yes });
139
+ });
140
+ program
141
+ .command('tag <id>')
142
+ .description('Update metadata on a document (description, project, domain)')
143
+ .option('-d, --description <text>', 'document description')
144
+ .option('-p, --project <name>', 'project name')
145
+ .option('--domain <domain>', 'domain: system, persona, workspace, project, general')
146
+ .action(async (documentId, options) => {
147
+ const config = loadConfig(CLI_CONTEXT);
148
+ await tag(config, parseInt(documentId, 10), {
149
+ description: options.description,
150
+ project: options.project,
151
+ domain: options.domain,
152
+ });
153
+ });
154
+ // =============================================================================
155
+ // Search quality
156
+ // =============================================================================
157
+ program
158
+ .command('eval')
159
+ .description('Run search quality evaluation against golden dataset')
160
+ .option('--dry-run', 'print report without saving to database')
161
+ .action(async (options) => {
162
+ const config = loadConfig(CLI_CONTEXT);
163
+ await evalSearch(config, { dryRun: options.dryRun ?? false });
164
+ });
165
+ program
166
+ .command('eval:sweep')
167
+ .description('Test multiple similarity thresholds to find optimal value')
168
+ .option('--thresholds <values>', 'comma-separated thresholds to test', '0.15,0.20,0.25,0.30,0.35,0.40')
169
+ .action(async (options) => {
170
+ const config = loadConfig(CLI_CONTEXT);
171
+ await sweepThreshold(config, { thresholds: options.thresholds });
172
+ });
173
+ program
174
+ .command('eval:show <runId>')
175
+ .description('Inspect a saved eval run — summary metrics and missed queries with doc names')
176
+ .option('--full', 'also dump per-query results as JSON', false)
177
+ .action(async (runIdArg, options) => {
178
+ const config = loadConfig(CLI_CONTEXT);
179
+ const runId = parseInt(runIdArg, 10);
180
+ if (isNaN(runId)) {
181
+ process.stderr.write(`Invalid run id: ${runIdArg}\n`);
182
+ process.exit(1);
183
+ }
184
+ await showEvalRun(config, runId, { full: options.full ?? false });
185
+ });
186
+ program
187
+ .command('eval:judge')
188
+ .description('Resumable rejudging walkthrough — grade top-10 search results per query using TREC 4-level scale')
189
+ .option('--query <id>', 'start at a specific golden query id', (value) => parseInt(value, 10))
190
+ .action(async (options) => {
191
+ const config = loadConfig(CLI_CONTEXT);
192
+ await evalJudge(config, { query: options.query });
96
193
  });
194
+ // =============================================================================
195
+ // Sync and maintenance
196
+ // =============================================================================
97
197
  program
98
- .command('ingest [file]')
99
- .description('Scan for unknown files and add them to Ledger with duplicate detection')
100
- .option('-a, --auto', 'auto-ingest without prompts (for hooks)')
101
- .action(async (file, options) => {
102
- const config = loadConfig();
103
- await ingest(config, { file, auto: options.auto ?? false });
198
+ .command('check')
199
+ .description('Compare local files vs Ledger, report sync status')
200
+ .action(async () => {
201
+ const config = loadConfig(CLI_CONTEXT);
202
+ await check(config);
104
203
  });
105
204
  program
106
205
  .command('backup')
107
- .description('Backup all notes to ~/.ledger/backups/')
206
+ .description('Backup all documents to ~/.ledger/backups/')
108
207
  .option('-q, --quiet', 'suppress output unless error')
109
208
  .option('--enable-cron', 'enable daily backup at 1am')
110
209
  .option('--disable-cron', 'disable daily backup cron')
@@ -117,167 +216,24 @@ program
117
216
  disableBackupCron();
118
217
  return;
119
218
  }
120
- const config = loadConfig();
219
+ const config = loadConfig(CLI_CONTEXT);
121
220
  await backup(config, { quiet: options.quiet ?? false });
122
221
  });
123
222
  program
124
223
  .command('restore <file>')
125
- .description('Restore notes from a backup JSON file')
224
+ .description('Restore documents from a backup JSON file')
126
225
  .action(async (file) => {
127
- const config = loadConfig();
226
+ const config = loadConfig(CLI_CONTEXT);
128
227
  await restore(config, file);
129
228
  });
130
- const configCmd = program
131
- .command('config')
132
- .description('View or change Ledger settings');
133
- configCmd
134
- .command('get <key>')
135
- .description('Get a config value (or "all" for full config)')
136
- .action(async (key) => {
137
- await configGet(key);
138
- });
139
- configCmd
140
- .command('set <key> <value>')
141
- .description('Set a config value')
142
- .action(async (key, value) => {
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
- }
162
- });
163
- configCmd
164
- .command('list')
165
- .description('Show all settings')
166
- .action(async () => {
167
- await configList();
168
- });
169
- program
170
- .command('onboard')
171
- .description('Create your AI persona (profile, communication style, rules)')
172
- .action(async () => {
173
- const config = loadConfig();
174
- await onboard(config);
175
- });
229
+ // =============================================================================
230
+ // Setup and configuration
231
+ // =============================================================================
176
232
  program
177
233
  .command('init')
178
- .description('Guided setup wizard (credentials, database, persona, platforms, sync)')
179
- .option('--legacy', 'run legacy init (credentials + database only)')
180
- .action(async (options) => {
181
- if (options.legacy) {
182
- await init();
183
- }
184
- else {
185
- await wizard();
186
- }
187
- });
188
- const setupCmd = program
189
- .command('setup')
190
- .description('Configure an agent platform to use Ledger');
191
- setupCmd
192
- .command('claude-code')
193
- .description('Register MCP, install hooks, pull cache (live sync)')
194
- .action(async () => {
195
- await setupClaudeCode();
196
- });
197
- setupCmd
198
- .command('openclaw [path]')
199
- .description('Generate persona files for OpenClaw (live sync via CLI)')
200
- .action(async (path) => {
201
- await setupOpenclaw(path);
202
- });
203
- setupCmd
204
- .command('chatgpt')
205
- .description('Generate system prompt for ChatGPT (static snapshot)')
206
- .action(async () => {
207
- await setupChatgpt();
208
- });
209
- program
210
- .command('migrate')
211
- .description('Safely migrate local files to Ledger (backup, compare, upload)')
234
+ .description('Set up Ledger credentials and database')
212
235
  .action(async () => {
213
- const config = loadConfig();
214
- await migrate(config);
215
- });
216
- program
217
- .command('add')
218
- .description('Add a new note to Ledger (with duplicate detection)')
219
- .requiredOption('-c, --content <content>', 'note content (or use stdin)')
220
- .option('-t, --type <type>', 'note type (feedback, reference, event, etc.)')
221
- .option('-a, --agent <agent>', 'agent name', 'cli')
222
- .option('-p, --project <project>', 'project name')
223
- .option('-k, --upsert-key <key>', 'upsert key for dedup')
224
- .option('-d, --description <text>', 'one-line description of the note')
225
- .option('-s, --status <status>', 'note status (idea, planning, active, done)')
226
- .option('-f, --force', 'skip duplicate check and interactive prompts')
227
- .action(async (options) => {
228
- const config = loadConfig();
229
- await add(config, options.content, {
230
- type: options.type,
231
- agent: options.agent,
232
- project: options.project,
233
- upsertKey: options.upsertKey,
234
- description: options.description,
235
- status: options.status,
236
- force: options.force ?? false,
237
- });
238
- });
239
- program
240
- .command('update <id>')
241
- .description('Update an existing note by ID (with confirmation)')
242
- .requiredOption('-c, --content <content>', 'new content')
243
- .action(async (id, options) => {
244
- const config = loadConfig();
245
- await update(config, parseInt(id, 10), options.content, {});
246
- });
247
- program
248
- .command('delete <id>')
249
- .description('Delete a note by ID (with confirmation)')
250
- .action(async (id) => {
251
- const config = loadConfig();
252
- await deleteNote(config, parseInt(id, 10));
253
- });
254
- program
255
- .command('list')
256
- .description('List recent notes from Ledger')
257
- .option('-n, --limit <number>', 'number of notes', '20')
258
- .option('-t, --type <type>', 'filter by note type')
259
- .option('-p, --project <project>', 'filter by project name')
260
- .action(async (options) => {
261
- const config = loadConfig();
262
- await list(config, {
263
- limit: parseInt(options.limit, 10),
264
- type: options.type,
265
- project: options.project,
266
- });
267
- });
268
- program
269
- .command('tag <id>')
270
- .description('Update metadata on a note (description, project, scope)')
271
- .option('-d, --description <text>', 'note description/purpose')
272
- .option('-p, --project <name>', 'project name')
273
- .option('-s, --scope <scope>', 'scope (user, system, general)')
274
- .action(async (id, options) => {
275
- const config = loadConfig();
276
- await tag(config, parseInt(id, 10), {
277
- description: options.description,
278
- project: options.project,
279
- scope: options.scope,
280
- });
236
+ await init();
281
237
  });
282
238
  program
283
239
  .command('lint')
@@ -287,4 +243,4 @@ program
287
243
  .action(async (options) => {
288
244
  await lint({ personal: options.personal ?? false, diff: options.diff ?? false });
289
245
  });
290
- program.parse();
246
+ program.parseAsync().then(() => shutdownObservability());
@@ -1,103 +1,54 @@
1
- import { loadConfigFile } from '../lib/config.js';
2
- import { opAddNote, getRegisteredTypes, isRegisteredType, registerType, validateTypeName, inferDelivery, NOTE_STATUSES } from '../lib/notes.js';
3
- import { ask, confirm, choose } from '../lib/prompt.js';
4
- export async function add(config, content, options) {
5
- const configFile = loadConfigFile();
6
- const interactive = configFile.naming?.interactive !== false;
7
- let type = options.type || '';
8
- const metadata = {};
9
- if (options.project)
10
- metadata.project = options.project;
11
- if (options.upsertKey)
12
- metadata.upsert_key = options.upsertKey;
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 registeredTypes = getRegisteredTypes();
22
- const typeChoice = await choose('What type of note is this?', [
23
- ...registeredTypes,
24
- 'skip — use default (general)',
25
- ]);
26
- type = typeChoice.startsWith('skip') ? 'general' : typeChoice;
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
- }
52
- // Description
53
- if (!metadata.description) {
54
- const desc = await ask('One-line description (what is this note for?): ');
55
- if (desc)
56
- metadata.description = desc;
57
- }
58
- // upsert_key
59
- if (!metadata.upsert_key) {
60
- const key = await ask('Unique key for this note (lowercase-hyphenated, or Enter to auto-generate): ');
61
- if (key)
62
- metadata.upsert_key = key;
63
- }
64
- // Project
65
- if (!metadata.project) {
66
- const proj = await ask('Project name (or Enter to skip): ');
67
- if (proj)
68
- metadata.project = proj;
69
- }
70
- // Status (only for project-scoped types)
71
- if (inferDelivery(type) === 'project' && !metadata.status) {
72
- const statusChoice = await choose('What stage is this?', [
73
- ...NOTE_STATUSES,
74
- 'skip — no status',
75
- ]);
76
- if (!statusChoice.startsWith('skip')) {
77
- metadata.status = statusChoice;
78
- }
79
- }
1
+ import { resolve } from 'path';
2
+ import { existsSync } from 'fs';
3
+ import { createDocumentFromFile, VerifyMismatchError } from '../lib/documents/operations.js';
4
+ import { fatal, ExitCode } from '../lib/errors.js';
5
+ // =============================================================================
6
+ // Command
7
+ // =============================================================================
8
+ /**
9
+ * Add a new document by reading content from a file on disk. Auto-verified after create.
10
+ *
11
+ * Bytes flow file -> createDocumentFromFile() -> Postgres without retyping.
12
+ * The composed-string path (`-c`) was removed in Phase 4 of the
13
+ * file-based-write-api rollout to close the drift class of bug.
14
+ *
15
+ * Drift surfaces as VerifyMismatchError on stderr with exit code VERIFY_MISMATCH.
16
+ * The document still exists on verify failure (audit_log preserves the create
17
+ * event for manual cleanup if needed).
18
+ */
19
+ export async function addDocumentFromFile(config, options) {
20
+ const absPath = resolve(options.filePath);
21
+ if (!existsSync(absPath)) {
22
+ fatal(`File not found: ${absPath}`, ExitCode.FILE_NOT_FOUND);
80
23
  }
81
- // Default type if still empty
82
- if (!type)
83
- type = 'general';
84
- // Mark as having gone through interactive (or skipped it)
85
- // so opAddNote doesn't re-prompt via MCP confirm flow
86
- metadata.interactive_skip = true;
87
- const result = await opAddNote({ supabase: config.supabase, openai: config.openai }, content, type, options.agent || 'cli', metadata, options.force ?? false);
88
- if (result.status === 'confirm') {
89
- console.error(result.message);
90
- const proceed = await confirm('\nCreate new note anyway?');
91
- if (proceed) {
92
- const forced = await opAddNote({ supabase: config.supabase, openai: config.openai }, content, type, options.agent || 'cli', { ...metadata, interactive_skip: true }, true);
93
- console.error(forced.message);
94
- }
95
- else {
96
- console.error('Cancelled.');
97
- }
98
- return;
24
+ const clients = {
25
+ supabase: config.supabase,
26
+ openai: config.openai,
27
+ };
28
+ process.stderr.write(`Adding document "${options.name}" from ${absPath} (${options.domain}/${options.documentType})...\n`);
29
+ try {
30
+ const result = await createDocumentFromFile(clients, {
31
+ filePath: absPath,
32
+ name: options.name,
33
+ domain: options.domain,
34
+ document_type: options.documentType,
35
+ description: options.description,
36
+ project: options.project,
37
+ agent: options.agent,
38
+ status: options.status,
39
+ protection: options.protection,
40
+ });
41
+ process.stdout.write(`${result.id}\n`);
42
+ process.stderr.write(`Document created (id: ${result.id}, ${result.bytes} bytes, verified)\n`);
43
+ }
44
+ catch (error) {
45
+ if (error instanceof VerifyMismatchError) {
46
+ process.stderr.write(`\nVerify failed on document ${error.id}.\n`);
47
+ process.stderr.write(`Pushed ${error.expectedLength} bytes, pulled ${error.actualLength} bytes.\n`);
48
+ process.stderr.write(`${error.diffPreview}\n`);
49
+ process.stderr.write(`The document was created but the round-trip diff caught drift. Inspect the doc and re-run if needed.\n`);
50
+ process.exit(ExitCode.VERIFY_MISMATCH);
51
+ }
52
+ throw error;
99
53
  }
100
- console.error(result.message);
101
- if (result.status === 'error')
102
- process.exit(1);
103
54
  }