@aperdomoll90/ledger-ai 1.0.1 → 1.1.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 +93 -4
- package/dist/commands/add.js +25 -0
- package/dist/commands/delete.js +22 -0
- package/dist/commands/ingest.js +22 -9
- package/dist/commands/init.js +80 -26
- package/dist/commands/list.js +10 -0
- package/dist/commands/migrate.js +9 -8
- package/dist/commands/onboard.js +3 -3
- package/dist/commands/pull.js +2 -2
- package/dist/commands/setup.js +88 -4
- package/dist/commands/sync.js +206 -0
- package/dist/commands/tag.js +20 -0
- package/dist/commands/update.js +22 -0
- package/dist/commands/wizard.js +430 -0
- package/dist/hooks/hooks/session-end-check.sh +3 -3
- package/dist/hooks/hooks/strip-ai-coauthor.sh +22 -0
- package/dist/lib/notes.js +430 -18
- package/dist/mcp-server.js +33 -293
- package/package.json +1 -1
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import { writeFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { createClient } from '@supabase/supabase-js';
|
|
5
|
+
import OpenAI from 'openai';
|
|
6
|
+
import { ask, confirm, choose } from '../lib/prompt.js';
|
|
7
|
+
import { getLedgerDir, loadConfigFile } from '../lib/config.js';
|
|
8
|
+
import { fetchPersonaNotes } from '../lib/notes.js';
|
|
9
|
+
import { gatherCredentials, connectAndMigrate, hasCredentials, readCredentials } from './init.js';
|
|
10
|
+
import { onboard } from './onboard.js';
|
|
11
|
+
import { setupClaudeCode, setupOpenclaw, setupChatgpt, detectPlatform, uninstallClaudeCode, uninstallOpenclaw, } from './setup.js';
|
|
12
|
+
import { sync } from './sync.js';
|
|
13
|
+
import { getMemoryFiles } from './migrate.js';
|
|
14
|
+
function ok() { return { ran: true, skipped: false }; }
|
|
15
|
+
function skipped() { return { ran: false, skipped: true }; }
|
|
16
|
+
function failed(msg) { return { ran: true, skipped: false, error: msg }; }
|
|
17
|
+
// --- Wizard ---
|
|
18
|
+
export async function wizard() {
|
|
19
|
+
console.error('Ledger Init Wizard\n');
|
|
20
|
+
// Detect what's already done
|
|
21
|
+
const checks = detectAllSteps();
|
|
22
|
+
if (checks.allDone) {
|
|
23
|
+
await showAlreadySetUp(checks);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Run steps sequentially
|
|
27
|
+
let creds = null;
|
|
28
|
+
let connectResult = null;
|
|
29
|
+
let config = null;
|
|
30
|
+
// Step 1: Credentials
|
|
31
|
+
if (checks.credentials) {
|
|
32
|
+
console.error('Step 1: Credentials: found (Supabase + OpenAI)\n');
|
|
33
|
+
creds = readCredentials();
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
console.error('Step 1: Credentials\n');
|
|
37
|
+
const result = await runNonSkippable('Credentials', async () => {
|
|
38
|
+
creds = await gatherCredentials();
|
|
39
|
+
});
|
|
40
|
+
if (result.error)
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Step 2: Database
|
|
44
|
+
if (!creds) {
|
|
45
|
+
creds = readCredentials();
|
|
46
|
+
if (!creds) {
|
|
47
|
+
console.error('Cannot proceed without credentials.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (checks.database) {
|
|
52
|
+
console.error(`Step 2: Database: connected (${checks.noteCount} notes)\n`);
|
|
53
|
+
// Construct clients from existing creds
|
|
54
|
+
const supabase = createClient(creds.supabaseUrl, creds.supabaseKey);
|
|
55
|
+
const openai = new OpenAI({ apiKey: creds.openaiKey });
|
|
56
|
+
connectResult = { supabase, openai, noteCount: checks.noteCount };
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.error('Step 2: Connect to database\n');
|
|
60
|
+
const result = await runNonSkippable('Database', async () => {
|
|
61
|
+
connectResult = await connectAndMigrate(creds);
|
|
62
|
+
});
|
|
63
|
+
if (result.error)
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Build LedgerConfig for remaining steps
|
|
67
|
+
if (connectResult) {
|
|
68
|
+
const configFile = loadConfigFile();
|
|
69
|
+
const HOME_PROJECT_DIR = homedir().replace(/\//g, '-');
|
|
70
|
+
config = {
|
|
71
|
+
memoryDir: configFile.memoryDir || resolve(homedir(), `.claude/projects/${HOME_PROJECT_DIR}/memory`),
|
|
72
|
+
claudeMdPath: configFile.claudeMdPath || resolve(homedir(), 'CLAUDE.md'),
|
|
73
|
+
supabase: connectResult.supabase,
|
|
74
|
+
openai: connectResult.openai,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (!config) {
|
|
78
|
+
console.error('Cannot proceed without database connection.');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Step 3: Device alias
|
|
82
|
+
if (checks.device) {
|
|
83
|
+
console.error(`Step 3: Device: ${checks.deviceAlias}\n`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.error('Step 3: Device alias (optional)\n');
|
|
87
|
+
await runSkippable('Device alias', () => stepDeviceAlias(config));
|
|
88
|
+
}
|
|
89
|
+
// Step 4: Persona
|
|
90
|
+
if (checks.persona) {
|
|
91
|
+
console.error(`Step 4: Persona: found\n`);
|
|
92
|
+
const update = await confirm(' Update persona?');
|
|
93
|
+
if (update) {
|
|
94
|
+
await onboard(config);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.error('Step 4: Build persona\n');
|
|
99
|
+
await runSkippable('Persona', () => onboard(config));
|
|
100
|
+
}
|
|
101
|
+
// Step 5: Platforms
|
|
102
|
+
console.error('Step 5: Platform setup\n');
|
|
103
|
+
await runSkippable('Platform setup', () => stepPlatforms(config));
|
|
104
|
+
// Step 6: Sync (always runs)
|
|
105
|
+
console.error('Step 6: Sync\n');
|
|
106
|
+
const syncResult = await sync(config, { quiet: false, force: false, dryRun: false });
|
|
107
|
+
// Step 7: Migrate local files
|
|
108
|
+
const unknownFiles = getMemoryFiles(config);
|
|
109
|
+
const personaNotes = await fetchPersonaNotes(config.supabase);
|
|
110
|
+
const knownFiles = new Set(personaNotes.map(n => n.metadata.local_file).filter(Boolean));
|
|
111
|
+
const unknowns = unknownFiles.filter(f => !knownFiles.has(f));
|
|
112
|
+
if (unknowns.length === 0) {
|
|
113
|
+
console.error('Step 7: Migration: no unknown files\n');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.error(`Step 7: Migration: ${unknowns.length} unknown file(s) found\n`);
|
|
117
|
+
console.error(' Run `ledger migrate` to process these files.\n');
|
|
118
|
+
}
|
|
119
|
+
// Summary
|
|
120
|
+
console.error('='.repeat(40));
|
|
121
|
+
console.error('Wizard complete.\n');
|
|
122
|
+
const parts = [
|
|
123
|
+
syncResult.downloaded.length > 0 ? `${syncResult.downloaded.length} downloaded` : null,
|
|
124
|
+
syncResult.uploaded.length > 0 ? `${syncResult.uploaded.length} uploaded` : null,
|
|
125
|
+
syncResult.conflicts.length > 0 ? `${syncResult.conflicts.length} conflicts` : null,
|
|
126
|
+
].filter(Boolean);
|
|
127
|
+
if (parts.length > 0) {
|
|
128
|
+
console.error(` Sync: ${parts.join(', ')}`);
|
|
129
|
+
}
|
|
130
|
+
console.error(' Run `ledger show <query>` to search your knowledge.');
|
|
131
|
+
}
|
|
132
|
+
function detectAllSteps() {
|
|
133
|
+
const credentials = hasCredentials();
|
|
134
|
+
let database = false;
|
|
135
|
+
let noteCount = 0;
|
|
136
|
+
let device = false;
|
|
137
|
+
let deviceAlias = '';
|
|
138
|
+
let persona = false;
|
|
139
|
+
if (credentials) {
|
|
140
|
+
// We can't check database without connecting, so we'll trust config
|
|
141
|
+
// The actual connection test happens in step 2
|
|
142
|
+
const creds = readCredentials();
|
|
143
|
+
database = creds !== null; // If we can read creds, assume DB was set up before
|
|
144
|
+
}
|
|
145
|
+
const configFile = loadConfigFile();
|
|
146
|
+
if (configFile.device?.alias) {
|
|
147
|
+
device = true;
|
|
148
|
+
deviceAlias = configFile.device.alias;
|
|
149
|
+
}
|
|
150
|
+
// We can't check persona or noteCount without connecting — these will be
|
|
151
|
+
// checked at runtime if credentials exist. For the allDone check,
|
|
152
|
+
// we conservatively say not all done if we can't verify.
|
|
153
|
+
return {
|
|
154
|
+
credentials,
|
|
155
|
+
database,
|
|
156
|
+
noteCount,
|
|
157
|
+
device,
|
|
158
|
+
deviceAlias,
|
|
159
|
+
persona,
|
|
160
|
+
allDone: false, // Full allDone check requires DB connection, done in showAlreadySetUp
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
async function showAlreadySetUp(checks) {
|
|
164
|
+
// If we got here, credentials exist. Connect to verify everything.
|
|
165
|
+
const creds = readCredentials();
|
|
166
|
+
if (!creds)
|
|
167
|
+
return;
|
|
168
|
+
let connectResult;
|
|
169
|
+
try {
|
|
170
|
+
const supabase = createClient(creds.supabaseUrl, creds.supabaseKey);
|
|
171
|
+
const { count } = await supabase
|
|
172
|
+
.from('notes')
|
|
173
|
+
.select('*', { count: 'exact', head: true });
|
|
174
|
+
const noteCount = count ?? 0;
|
|
175
|
+
const openai = new OpenAI({ apiKey: creds.openaiKey });
|
|
176
|
+
// Check persona
|
|
177
|
+
const { data: personaData } = await supabase
|
|
178
|
+
.from('notes')
|
|
179
|
+
.select('id')
|
|
180
|
+
.eq('metadata->>delivery', 'persona')
|
|
181
|
+
.limit(1);
|
|
182
|
+
const hasPersona = personaData !== null && personaData.length > 0;
|
|
183
|
+
// Check platforms
|
|
184
|
+
const claudeCode = detectPlatform('claude-code');
|
|
185
|
+
const openclaw = detectPlatform('openclaw');
|
|
186
|
+
const platforms = [
|
|
187
|
+
claudeCode.installed ? 'Claude Code' : null,
|
|
188
|
+
openclaw.installed ? 'OpenClaw' : null,
|
|
189
|
+
].filter(Boolean);
|
|
190
|
+
const configFile = loadConfigFile();
|
|
191
|
+
// Now check if truly everything is set up
|
|
192
|
+
const allDone = hasPersona && checks.credentials;
|
|
193
|
+
if (!allDone) {
|
|
194
|
+
// Not everything is done — let the main flow handle it
|
|
195
|
+
// Reset and run wizard normally
|
|
196
|
+
checks.noteCount = noteCount;
|
|
197
|
+
checks.persona = hasPersona;
|
|
198
|
+
checks.database = true;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
console.error('Ledger is already set up.');
|
|
202
|
+
console.error(` Credentials: found (Supabase + OpenAI)`);
|
|
203
|
+
console.error(` Database: connected (${noteCount} notes)`);
|
|
204
|
+
console.error(` Device: ${configFile.device?.alias || '(not set)'}`);
|
|
205
|
+
console.error(` Persona: found`);
|
|
206
|
+
console.error(` Platforms: ${platforms.length > 0 ? platforms.join(', ') : '(none)'}`);
|
|
207
|
+
console.error('');
|
|
208
|
+
const rerun = await ask('Re-run a step? [1-7 or Enter to skip] ');
|
|
209
|
+
if (!rerun)
|
|
210
|
+
return;
|
|
211
|
+
const step = parseInt(rerun, 10);
|
|
212
|
+
if (step < 1 || step > 7)
|
|
213
|
+
return;
|
|
214
|
+
// Build config for re-running
|
|
215
|
+
const config = {
|
|
216
|
+
memoryDir: configFile.memoryDir || resolve(homedir(), `.claude/projects/${homedir().replace(/\//g, '-')}/memory`),
|
|
217
|
+
claudeMdPath: configFile.claudeMdPath || resolve(homedir(), 'CLAUDE.md'),
|
|
218
|
+
supabase,
|
|
219
|
+
openai,
|
|
220
|
+
};
|
|
221
|
+
// Re-running steps 1-2 re-runs all subsequent steps
|
|
222
|
+
if (step <= 2) {
|
|
223
|
+
// Re-run from the beginning by falling through
|
|
224
|
+
console.error('Re-running from step 1 will re-run all subsequent steps.\n');
|
|
225
|
+
const confirmRerun = await confirm('Continue?');
|
|
226
|
+
if (!confirmRerun)
|
|
227
|
+
return;
|
|
228
|
+
// Recursively call wizard (which won't hit allDone since we're forcing re-run)
|
|
229
|
+
// For simplicity, just call the individual steps
|
|
230
|
+
if (step === 1) {
|
|
231
|
+
await gatherCredentials();
|
|
232
|
+
const newCreds = readCredentials();
|
|
233
|
+
await connectAndMigrate(newCreds);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
await connectAndMigrate(creds);
|
|
237
|
+
}
|
|
238
|
+
await stepDeviceAlias(config);
|
|
239
|
+
await onboard(config);
|
|
240
|
+
await stepPlatforms(config);
|
|
241
|
+
await sync(config, { quiet: false, force: false, dryRun: false });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
switch (step) {
|
|
245
|
+
case 3:
|
|
246
|
+
await stepDeviceAlias(config);
|
|
247
|
+
break;
|
|
248
|
+
case 4:
|
|
249
|
+
await onboard(config);
|
|
250
|
+
break;
|
|
251
|
+
case 5:
|
|
252
|
+
await stepPlatforms(config);
|
|
253
|
+
break;
|
|
254
|
+
case 6:
|
|
255
|
+
await sync(config, { quiet: false, force: false, dryRun: false });
|
|
256
|
+
break;
|
|
257
|
+
case 7: {
|
|
258
|
+
const { migrate } = await import('./migrate.js');
|
|
259
|
+
await migrate(config);
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// Connection failed — re-run wizard from scratch
|
|
266
|
+
console.error('Could not verify setup. Running wizard...\n');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// --- Step implementations ---
|
|
270
|
+
async function stepDeviceAlias(config) {
|
|
271
|
+
const configFile = loadConfigFile();
|
|
272
|
+
const current = configFile.device?.alias;
|
|
273
|
+
if (current) {
|
|
274
|
+
console.error(` Current device alias: ${current}`);
|
|
275
|
+
const change = await confirm(' Change it?');
|
|
276
|
+
if (!change)
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const alias = await ask(' Name this device? (optional, press Enter to skip) ');
|
|
280
|
+
if (!alias)
|
|
281
|
+
return;
|
|
282
|
+
// Save to config.json
|
|
283
|
+
const ledgerDir = getLedgerDir();
|
|
284
|
+
const configPath = resolve(ledgerDir, 'config.json');
|
|
285
|
+
const updated = { ...configFile, device: { alias } };
|
|
286
|
+
writeFileSync(configPath, JSON.stringify(updated, null, 2) + '\n');
|
|
287
|
+
console.error(` Device alias set to "${alias}"\n`);
|
|
288
|
+
// Update user-devices note in Ledger
|
|
289
|
+
const today = new Date().toISOString().split('T')[0];
|
|
290
|
+
const { data: existing } = await config.supabase
|
|
291
|
+
.from('notes')
|
|
292
|
+
.select('id, content, metadata')
|
|
293
|
+
.eq('metadata->>upsert_key', 'user-devices')
|
|
294
|
+
.limit(1)
|
|
295
|
+
.single();
|
|
296
|
+
if (existing) {
|
|
297
|
+
// Check if device already listed
|
|
298
|
+
if (!existing.content.includes(alias)) {
|
|
299
|
+
const newContent = `${existing.content}\n- ${alias} (registered ${today})`;
|
|
300
|
+
await config.supabase
|
|
301
|
+
.from('notes')
|
|
302
|
+
.update({ content: newContent, updated_at: new Date().toISOString() })
|
|
303
|
+
.eq('id', existing.id);
|
|
304
|
+
console.error(` Added "${alias}" to device registry.\n`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// Create device registry note
|
|
309
|
+
const content = `## Devices\n- ${alias} (registered ${today})`;
|
|
310
|
+
const openai = config.openai;
|
|
311
|
+
const embeddingResponse = await openai.embeddings.create({
|
|
312
|
+
model: 'text-embedding-3-small',
|
|
313
|
+
input: content,
|
|
314
|
+
});
|
|
315
|
+
await config.supabase
|
|
316
|
+
.from('notes')
|
|
317
|
+
.insert({
|
|
318
|
+
content,
|
|
319
|
+
metadata: {
|
|
320
|
+
type: 'reference',
|
|
321
|
+
delivery: 'knowledge',
|
|
322
|
+
agent: 'ledger-wizard',
|
|
323
|
+
scope: 'user',
|
|
324
|
+
upsert_key: 'user-devices',
|
|
325
|
+
description: 'Registry of all devices connected to this Ledger instance.',
|
|
326
|
+
},
|
|
327
|
+
embedding: embeddingResponse.data[0].embedding,
|
|
328
|
+
});
|
|
329
|
+
console.error(` Created device registry with "${alias}".\n`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function stepPlatforms(_config) {
|
|
333
|
+
const platforms = ['claude-code', 'openclaw', 'chatgpt'];
|
|
334
|
+
for (const name of platforms) {
|
|
335
|
+
const status = detectPlatform(name);
|
|
336
|
+
const label = name === 'claude-code' ? 'Claude Code' : name === 'openclaw' ? 'OpenClaw' : 'ChatGPT';
|
|
337
|
+
if (status.installed) {
|
|
338
|
+
console.error(` ${label} (installed)`);
|
|
339
|
+
const action = await choose(` Action for ${label}:`, ['Keep', 'Reinstall', 'Uninstall']);
|
|
340
|
+
if (action === 'Reinstall') {
|
|
341
|
+
console.error(` Reinstalling ${label}...\n`);
|
|
342
|
+
if (name === 'claude-code') {
|
|
343
|
+
uninstallClaudeCode();
|
|
344
|
+
await setupClaudeCode();
|
|
345
|
+
}
|
|
346
|
+
else if (name === 'openclaw') {
|
|
347
|
+
uninstallOpenclaw();
|
|
348
|
+
await setupOpenclaw();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else if (action === 'Uninstall') {
|
|
352
|
+
console.error(` Uninstalling ${label}...\n`);
|
|
353
|
+
if (name === 'claude-code') {
|
|
354
|
+
uninstallClaudeCode();
|
|
355
|
+
}
|
|
356
|
+
else if (name === 'openclaw') {
|
|
357
|
+
uninstallOpenclaw();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
console.error(` Keeping ${label}.\n`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
// ChatGPT never shows as installed but is always available to install
|
|
366
|
+
if (name === 'chatgpt') {
|
|
367
|
+
console.error(` ${label} (static snapshot)`);
|
|
368
|
+
}
|
|
369
|
+
else if (status.detail === 'Claude Code CLI not found') {
|
|
370
|
+
console.error(` ${label}: CLI not found — skipping. Install it and run 'ledger setup claude-code' later.\n`);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
console.error(` ${label} (not installed)`);
|
|
375
|
+
}
|
|
376
|
+
const action = await choose(` Action for ${label}:`, ['Install', 'Skip']);
|
|
377
|
+
if (action === 'Install') {
|
|
378
|
+
console.error(` Installing ${label}...\n`);
|
|
379
|
+
if (name === 'claude-code') {
|
|
380
|
+
await setupClaudeCode();
|
|
381
|
+
}
|
|
382
|
+
else if (name === 'openclaw') {
|
|
383
|
+
await setupOpenclaw();
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
await setupChatgpt();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
console.error(` Skipped.\n`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// --- Error handling wrappers ---
|
|
396
|
+
async function runNonSkippable(label, fn) {
|
|
397
|
+
while (true) {
|
|
398
|
+
try {
|
|
399
|
+
await fn();
|
|
400
|
+
return ok();
|
|
401
|
+
}
|
|
402
|
+
catch (e) {
|
|
403
|
+
console.error(` ${label} failed: ${e.message}\n`);
|
|
404
|
+
const action = await choose(' What to do?', ['Retry', 'Quit']);
|
|
405
|
+
if (action === 'Quit') {
|
|
406
|
+
console.error('Wizard cancelled.');
|
|
407
|
+
return failed(e.message);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async function runSkippable(label, fn) {
|
|
413
|
+
try {
|
|
414
|
+
await fn();
|
|
415
|
+
return ok();
|
|
416
|
+
}
|
|
417
|
+
catch (e) {
|
|
418
|
+
console.error(` ${label} failed: ${e.message}\n`);
|
|
419
|
+
const action = await choose(' What to do?', ['Retry', 'Skip', 'Quit']);
|
|
420
|
+
if (action === 'Quit') {
|
|
421
|
+
console.error('Wizard cancelled.');
|
|
422
|
+
return failed(e.message);
|
|
423
|
+
}
|
|
424
|
+
if (action === 'Skip') {
|
|
425
|
+
return skipped();
|
|
426
|
+
}
|
|
427
|
+
// Retry
|
|
428
|
+
return runSkippable(label, fn);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Session end: run hash-based sync check + temp file alert
|
|
3
3
|
|
|
4
|
-
OUTPUT=$(ledger
|
|
4
|
+
OUTPUT=$(ledger sync --dry-run 2>&1)
|
|
5
5
|
|
|
6
|
-
# Only show output if there are
|
|
7
|
-
if ! echo "$OUTPUT" | grep -q "
|
|
6
|
+
# Only show output if there are changes to sync
|
|
7
|
+
if ! echo "$OUTPUT" | grep -q "nothing to do"; then
|
|
8
8
|
echo "$OUTPUT"
|
|
9
9
|
fi
|
|
10
10
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Block git commits that contain AI co-author attribution
|
|
3
|
+
# PreToolUse hook on Bash — intercepts before execution
|
|
4
|
+
INPUT=$(cat)
|
|
5
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
|
6
|
+
|
|
7
|
+
if [[ -z "$COMMAND" ]]; then
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
# Only check git commit commands
|
|
12
|
+
if ! echo "$COMMAND" | grep -qE 'git\s+commit'; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Check for Co-Authored-By with AI/Claude/bot patterns
|
|
17
|
+
if echo "$COMMAND" | grep -qiE 'Co-Authored-By:.*\b(Claude|Anthropic|AI|GPT|OpenAI|Copilot|bot)\b'; then
|
|
18
|
+
echo "BLOCKED: Do not include AI Co-Authored-By lines in commits. Remove the Co-Authored-By line and retry." >&2
|
|
19
|
+
exit 2
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
exit 0
|