@omnitype-code/journal 2.0.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.
Files changed (69) hide show
  1. package/dist/blame/legacy.d.ts +24 -0
  2. package/dist/blame/legacy.d.ts.map +1 -0
  3. package/dist/blame/legacy.js +219 -0
  4. package/dist/blame/legacy.js.map +1 -0
  5. package/dist/blame/merge.d.ts +17 -0
  6. package/dist/blame/merge.d.ts.map +1 -0
  7. package/dist/blame/merge.js +32 -0
  8. package/dist/blame/merge.js.map +1 -0
  9. package/dist/cli.d.ts +7 -0
  10. package/dist/cli.d.ts.map +1 -0
  11. package/dist/cli.js +638 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/cloud/anchor.d.ts +78 -0
  14. package/dist/cloud/anchor.d.ts.map +1 -0
  15. package/dist/cloud/anchor.js +220 -0
  16. package/dist/cloud/anchor.js.map +1 -0
  17. package/dist/cloud/pending.d.ts +29 -0
  18. package/dist/cloud/pending.d.ts.map +1 -0
  19. package/dist/cloud/pending.js +115 -0
  20. package/dist/cloud/pending.js.map +1 -0
  21. package/dist/cloud/shipper.d.ts +67 -0
  22. package/dist/cloud/shipper.d.ts.map +1 -0
  23. package/dist/cloud/shipper.js +177 -0
  24. package/dist/cloud/shipper.js.map +1 -0
  25. package/dist/crypto/chain.d.ts +19 -0
  26. package/dist/crypto/chain.d.ts.map +1 -0
  27. package/dist/crypto/chain.js +123 -0
  28. package/dist/crypto/chain.js.map +1 -0
  29. package/dist/daemon/journal.d.ts +92 -0
  30. package/dist/daemon/journal.d.ts.map +1 -0
  31. package/dist/daemon/journal.js +370 -0
  32. package/dist/daemon/journal.js.map +1 -0
  33. package/dist/daemon/server.d.ts +89 -0
  34. package/dist/daemon/server.d.ts.map +1 -0
  35. package/dist/daemon/server.js +323 -0
  36. package/dist/daemon/server.js.map +1 -0
  37. package/dist/index.d.ts +8 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +9 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/log/segment.d.ts +43 -0
  42. package/dist/log/segment.d.ts.map +1 -0
  43. package/dist/log/segment.js +180 -0
  44. package/dist/log/segment.js.map +1 -0
  45. package/dist/materializer/db.d.ts +47 -0
  46. package/dist/materializer/db.d.ts.map +1 -0
  47. package/dist/materializer/db.js +385 -0
  48. package/dist/materializer/db.js.map +1 -0
  49. package/dist/notes/git-notes.d.ts +50 -0
  50. package/dist/notes/git-notes.d.ts.map +1 -0
  51. package/dist/notes/git-notes.js +94 -0
  52. package/dist/notes/git-notes.js.map +1 -0
  53. package/dist/schema/events.d.ts +224 -0
  54. package/dist/schema/events.d.ts.map +1 -0
  55. package/dist/schema/events.js +10 -0
  56. package/dist/schema/events.js.map +1 -0
  57. package/dist/security/developer-identity.d.ts +35 -0
  58. package/dist/security/developer-identity.d.ts.map +1 -0
  59. package/dist/security/developer-identity.js +105 -0
  60. package/dist/security/developer-identity.js.map +1 -0
  61. package/dist/security/keychain.d.ts +20 -0
  62. package/dist/security/keychain.d.ts.map +1 -0
  63. package/dist/security/keychain.js +167 -0
  64. package/dist/security/keychain.js.map +1 -0
  65. package/dist/verify/chain-verify.d.ts +43 -0
  66. package/dist/verify/chain-verify.d.ts.map +1 -0
  67. package/dist/verify/chain-verify.js +119 -0
  68. package/dist/verify/chain-verify.js.map +1 -0
  69. package/package.json +47 -0
package/dist/cli.js ADDED
@@ -0,0 +1,638 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * omnitype-daemon CLI
4
+ * Commands: start, stop, status, health, doctor, blame
5
+ */
6
+ import { program } from 'commander';
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
8
+ import { join, resolve, relative } from 'node:path';
9
+ import { execSync, spawn, execFileSync } from 'node:child_process';
10
+ import { join as joinPath } from 'node:path';
11
+ import { Journal, resolvePaths } from './daemon/journal.js';
12
+ import { DaemonServer, JournalClient, getSocketPath } from './daemon/server.js';
13
+ import { loadOrCreateInstallKeySecure } from './security/keychain.js';
14
+ import { CloudShipper } from './cloud/shipper.js';
15
+ import { CloudAnchor, AnchorQueue } from './cloud/anchor.js';
16
+ import { resolveLegacyLineMap, parseGitBlamePorcelain } from './blame/legacy.js';
17
+ import { mergeLineAttribution } from './blame/merge.js';
18
+ // Lazy import adapter-sdk so the daemon CLI works without it installed
19
+ async function loadAdapterSdk() {
20
+ try {
21
+ return await import('@omnitype-code/adapter-sdk');
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ function getWorkspaceRoot() {
28
+ try {
29
+ return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
30
+ }
31
+ catch {
32
+ return process.cwd();
33
+ }
34
+ }
35
+ function getJournalRoot(workspace) {
36
+ return join(workspace, '.omnitype', 'journal');
37
+ }
38
+ function getPidFile(journalRoot) {
39
+ return join(journalRoot, 'daemon.pid');
40
+ }
41
+ // ─── start ────────────────────────────────────────────────────────────────────
42
+ program
43
+ .command('start')
44
+ .description('Start the journal daemon in the background')
45
+ .option('--foreground', 'Run in foreground (for debugging)')
46
+ .action(async (opts) => {
47
+ const workspace = getWorkspaceRoot();
48
+ const journalRoot = getJournalRoot(workspace);
49
+ mkdirSync(journalRoot, { recursive: true });
50
+ const socketPath = getSocketPath(journalRoot);
51
+ if (await JournalClient.isRunning(socketPath)) {
52
+ console.log('Daemon already running.');
53
+ return;
54
+ }
55
+ if (opts.foreground) {
56
+ runDaemon(workspace, journalRoot, socketPath);
57
+ return;
58
+ }
59
+ // Spawn detached background process
60
+ const child = spawn(process.execPath, [process.argv[1], 'start', '--foreground'], {
61
+ detached: true,
62
+ stdio: 'ignore',
63
+ env: { ...process.env, OMNITYPE_WORKSPACE: workspace },
64
+ });
65
+ child.unref();
66
+ writeFileSync(getPidFile(journalRoot), String(child.pid), 'utf-8');
67
+ console.log(`Daemon started (pid ${child.pid})`);
68
+ });
69
+ // ─── stop ─────────────────────────────────────────────────────────────────────
70
+ program
71
+ .command('stop')
72
+ .description('Stop the journal daemon')
73
+ .action(async () => {
74
+ const workspace = getWorkspaceRoot();
75
+ const journalRoot = getJournalRoot(workspace);
76
+ const pidFile = getPidFile(journalRoot);
77
+ if (existsSync(pidFile)) {
78
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
79
+ try {
80
+ process.kill(pid, 'SIGTERM');
81
+ console.log(`Sent SIGTERM to pid ${pid}`);
82
+ }
83
+ catch {
84
+ console.log(`Process ${pid} not found (already stopped?)`);
85
+ }
86
+ }
87
+ else {
88
+ console.log('No daemon pid file found.');
89
+ }
90
+ });
91
+ // ─── restart ──────────────────────────────────────────────────────────────────
92
+ program
93
+ .command('restart')
94
+ .description('Restart the journal daemon (re-evaluates org policy / key storage)')
95
+ .action(async () => {
96
+ const workspace = getWorkspaceRoot();
97
+ const journalRoot = getJournalRoot(workspace);
98
+ const socketPath = getSocketPath(journalRoot);
99
+ const pidFile = getPidFile(journalRoot);
100
+ // Stop if running
101
+ if (existsSync(pidFile)) {
102
+ const pid = parseInt(readFileSync(pidFile, 'utf-8').trim(), 10);
103
+ try {
104
+ process.kill(pid, 'SIGTERM');
105
+ }
106
+ catch { /* already stopped */ }
107
+ }
108
+ // Wait for the socket to go down (up to ~3s)
109
+ for (let i = 0; i < 12; i++) {
110
+ if (!(await JournalClient.isRunning(socketPath)))
111
+ break;
112
+ await new Promise((r) => setTimeout(r, 250));
113
+ }
114
+ // Start fresh
115
+ const child = spawn(process.execPath, [process.argv[1], 'start', '--foreground'], {
116
+ detached: true,
117
+ stdio: 'ignore',
118
+ env: { ...process.env, OMNITYPE_WORKSPACE: workspace },
119
+ });
120
+ child.unref();
121
+ writeFileSync(pidFile, String(child.pid), 'utf-8');
122
+ console.log(`Daemon restarted (pid ${child.pid})`);
123
+ });
124
+ // ─── status ───────────────────────────────────────────────────────────────────
125
+ program
126
+ .command('status')
127
+ .description('Check daemon status')
128
+ .action(async () => {
129
+ const workspace = getWorkspaceRoot();
130
+ const journalRoot = getJournalRoot(workspace);
131
+ const socketPath = getSocketPath(journalRoot);
132
+ const running = await JournalClient.isRunning(socketPath);
133
+ console.log(running ? 'running' : 'stopped');
134
+ process.exit(running ? 0 : 1);
135
+ });
136
+ // ─── health ───────────────────────────────────────────────────────────────────
137
+ program
138
+ .command('health')
139
+ .description('Show journal health and attribution tier breakdown')
140
+ .option('--json', 'Output as JSON')
141
+ .action(async (opts) => {
142
+ const workspace = getWorkspaceRoot();
143
+ const journalRoot = getJournalRoot(workspace);
144
+ const socketPath = getSocketPath(journalRoot);
145
+ if (!(await JournalClient.isRunning(socketPath))) {
146
+ console.error('Daemon not running. Start it with: omnitype-daemon start');
147
+ process.exit(1);
148
+ }
149
+ const client = new JournalClient(socketPath);
150
+ await client.connect();
151
+ const { head, install } = await client.health();
152
+ // Query overall attribution across all tracked files for tier breakdown
153
+ const tierCounts = { T0: 0, T1: 0, T2: 0, T3: 0 };
154
+ const originCounts = { ai: 0, user: 0, paste: 0, tool: 0, external: 0, unknown: 0 };
155
+ let totalSpans = 0;
156
+ try {
157
+ const { spans: allSpans } = await client.querySpans('*', 'T3');
158
+ for (const sp of allSpans) {
159
+ tierCounts[sp.tier] = (tierCounts[sp.tier] ?? 0) + 1;
160
+ originCounts[sp.origin] = (originCounts[sp.origin] ?? 0) + 1;
161
+ totalSpans++;
162
+ }
163
+ }
164
+ catch { /* daemon may not support wildcard — skip tier breakdown */ }
165
+ client.disconnect();
166
+ if (opts.json) {
167
+ console.log(JSON.stringify({ head, install, tierCounts, originCounts, totalSpans }, null, 2));
168
+ return;
169
+ }
170
+ const tierBar = (tier, count) => {
171
+ const pct = totalSpans > 0 ? Math.round((count / totalSpans) * 20) : 0;
172
+ const bar = '█'.repeat(pct) + '░'.repeat(20 - pct);
173
+ const label = { T0: 'Deterministic', T1: 'Hook-inferred', T2: 'Retrospective', T3: 'Filesystem-only' }[tier] ?? tier;
174
+ return ` ${tier} ${bar} ${count.toString().padStart(6)} spans ${label}`;
175
+ };
176
+ console.log('\n\x1b[1mOmniType Journal — Health\x1b[0m');
177
+ console.log('─'.repeat(56));
178
+ console.log(` Install ID : ${install.install_id}`);
179
+ console.log(` Workspace : ${install.workspace}`);
180
+ console.log(` Head seq : ${head.seq}`);
181
+ console.log(` Head hash : ${head.lastHash.slice(0, 16)}...`);
182
+ console.log(` Protocol : v${install.protocol_version}`);
183
+ console.log('');
184
+ if (totalSpans > 0) {
185
+ console.log('\x1b[1m Attribution Tier Breakdown\x1b[0m');
186
+ console.log(' ' + '─'.repeat(54));
187
+ for (const tier of ['T0', 'T1', 'T2', 'T3']) {
188
+ console.log(tierBar(tier, tierCounts[tier] ?? 0));
189
+ }
190
+ console.log('');
191
+ const confScore = ((tierCounts['T0'] ?? 0) * 1.0 +
192
+ (tierCounts['T1'] ?? 0) * 0.85 +
193
+ (tierCounts['T2'] ?? 0) * 0.60 +
194
+ (tierCounts['T3'] ?? 0) * 0.30) / totalSpans;
195
+ console.log('\x1b[1m Origin Breakdown\x1b[0m');
196
+ console.log(' ' + '─'.repeat(54));
197
+ for (const [origin, count] of Object.entries(originCounts).filter(([, c]) => c > 0)) {
198
+ const pct = Math.round((count / totalSpans) * 100);
199
+ console.log(` ${origin.padEnd(10)} ${count.toString().padStart(6)} spans (${pct}%)`);
200
+ }
201
+ console.log('');
202
+ console.log(` Confidence score: \x1b[1m${(confScore * 100).toFixed(1)}%\x1b[0m (weighted average across tiers)`);
203
+ console.log(' Note: "unknown" origin = unattributed write, NOT assumed AI');
204
+ }
205
+ else {
206
+ console.log(' No spans recorded yet. Start coding with an AI tool to populate attribution data.');
207
+ }
208
+ console.log('');
209
+ });
210
+ // ─── doctor ───────────────────────────────────────────────────────────────────
211
+ program
212
+ .command('doctor')
213
+ .description('Verify chain integrity and diagnose issues')
214
+ .action(async () => {
215
+ const workspace = getWorkspaceRoot();
216
+ const journalRoot = getJournalRoot(workspace);
217
+ const paths = resolvePaths(journalRoot);
218
+ console.log('\nOmniType — Doctor\n');
219
+ const checks = [];
220
+ // 1. Journal directory exists
221
+ checks.push({ label: 'Journal directory', ok: existsSync(journalRoot) });
222
+ // 2. Install key exists and is 32 bytes
223
+ const keyPath = join(paths.keysDir, 'install.key');
224
+ const keyOk = existsSync(keyPath) && readFileSync(keyPath).length === 32;
225
+ checks.push({ label: 'Install key (32 bytes, mode 0600)', ok: keyOk });
226
+ // 3. Daemon running
227
+ const socketPath = getSocketPath(journalRoot);
228
+ const running = await JournalClient.isRunning(socketPath);
229
+ checks.push({ label: 'Daemon running', ok: running });
230
+ // 4. Head.json consistent
231
+ const headPath = join(paths.chainDir, 'head.json');
232
+ let headOk = false;
233
+ if (existsSync(headPath)) {
234
+ try {
235
+ const h = JSON.parse(readFileSync(headPath, 'utf-8'));
236
+ headOk = typeof h.seq === 'number' && typeof h.hash === 'string' && h.hash.length === 64;
237
+ }
238
+ catch { /* not ok */ }
239
+ }
240
+ checks.push({ label: 'Chain head valid', ok: headOk });
241
+ // 5. git notes.rewriteRef configured
242
+ let rewriteOk = false;
243
+ try {
244
+ const val = execSync('git config notes.rewriteRef', { encoding: 'utf-8', cwd: workspace, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
245
+ rewriteOk = val.includes('refs/notes/ai');
246
+ }
247
+ catch { /* not set */ }
248
+ checks.push({
249
+ label: 'git notes.rewriteRef configured',
250
+ ok: rewriteOk,
251
+ detail: rewriteOk ? undefined : 'Run: git config notes.rewriteRef refs/notes/ai',
252
+ });
253
+ let allOk = true;
254
+ for (const check of checks) {
255
+ const icon = check.ok ? '✓' : '✗';
256
+ console.log(` ${icon} ${check.label}`);
257
+ if (!check.ok && check.detail)
258
+ console.log(` → ${check.detail}`);
259
+ if (!check.ok)
260
+ allOk = false;
261
+ }
262
+ console.log('');
263
+ console.log(allOk ? 'All checks passed.' : 'Some checks failed — see above.');
264
+ process.exit(allOk ? 0 : 1);
265
+ });
266
+ // ─── blame ────────────────────────────────────────────────────────────────────
267
+ program
268
+ .command('blame <file>')
269
+ .description('Show per-line attribution (v2 journal + legacy .omni / git notes)')
270
+ .option('--min-tier <tier>', 'Minimum journal tier (T0/T1/T2/T3)', 'T3')
271
+ .action(async (file, opts) => {
272
+ const workspace = getWorkspaceRoot();
273
+ const journalRoot = getJournalRoot(workspace);
274
+ const socketPath = getSocketPath(journalRoot);
275
+ const absPath = resolve(workspace, file);
276
+ const relPath = relative(workspace, absPath).replace(/\\/g, '/');
277
+ let fileContent;
278
+ try {
279
+ fileContent = readFileSync(absPath, 'utf-8');
280
+ }
281
+ catch {
282
+ console.error(`Cannot read file: ${absPath}`);
283
+ process.exit(1);
284
+ }
285
+ const lines = fileContent.split('\n');
286
+ const legacyMap = resolveLegacyLineMap(workspace, relPath, absPath);
287
+ // Journal spans (optional — daemon may be stopped)
288
+ const journalByLine = new Map();
289
+ let spanCount = 0;
290
+ if (await JournalClient.isRunning(socketPath)) {
291
+ const client = new JournalClient(socketPath);
292
+ try {
293
+ await client.connect();
294
+ const installKey = loadOrCreateInstallKeySecure(joinPath(journalRoot, 'keys'));
295
+ await client.authenticate(installKey, 'omnitype-cli', {
296
+ capabilities: { 'cli.query': 'available' },
297
+ manifestSigVerified: true,
298
+ runtimeMode: 'in-process',
299
+ });
300
+ const { spans } = await client.querySpans(relPath, opts.minTier);
301
+ spanCount = spans.length;
302
+ const bytes = Buffer.from(fileContent, 'utf-8');
303
+ const lineStarts = [0];
304
+ for (let i = 0; i < bytes.length; i++) {
305
+ if (bytes[i] === 0x0a)
306
+ lineStarts.push(i + 1);
307
+ }
308
+ for (const span of spans) {
309
+ const startLine = lineStarts.findLastIndex((s) => s <= span.byte_start);
310
+ const endLine = lineStarts.findLastIndex((s) => s <= span.byte_end);
311
+ for (let l = Math.max(0, startLine); l <= Math.min(endLine, lines.length - 1); l++) {
312
+ journalByLine.set(l + 1, {
313
+ origin: span.origin,
314
+ tool: span.actor?.tool ?? '',
315
+ tier: span.tier,
316
+ confidence: span.confidence ?? 0,
317
+ });
318
+ }
319
+ }
320
+ }
321
+ finally {
322
+ client.disconnect();
323
+ }
324
+ }
325
+ let gitBlame = [];
326
+ try {
327
+ const raw = execFileSync('git', ['-C', workspace, 'blame', '--line-porcelain', absPath], {
328
+ encoding: 'utf-8',
329
+ stdio: ['pipe', 'pipe', 'ignore'],
330
+ });
331
+ gitBlame = parseGitBlamePorcelain(raw);
332
+ }
333
+ catch { /* no git blame */ }
334
+ const gitByLine = new Map(gitBlame.map((b) => [b.lineNum, b]));
335
+ const COLORS = {
336
+ ai: '\x1b[35m',
337
+ user: '\x1b[32m',
338
+ paste: '\x1b[34m',
339
+ tool: '\x1b[33m',
340
+ existing: '\x1b[90m',
341
+ unknown: '\x1b[90m',
342
+ external: '\x1b[90m',
343
+ reset: '\x1b[0m',
344
+ };
345
+ const originCounts = {};
346
+ const sourceCounts = {};
347
+ console.log(`\n\x1b[1mAttribution: ${relPath}\x1b[0m (journal tier >= ${opts.minTier})`);
348
+ console.log(`Lines: ${lines.length} | Journal spans: ${spanCount} | Legacy (.omni/git-note): ${legacyMap.size}`);
349
+ console.log('');
350
+ console.log(` LINE COMMIT AUTHOR TIER CONF ORIGIN TOOL SOURCE`);
351
+ console.log(' ' + '─'.repeat(78));
352
+ lines.forEach((line, i) => {
353
+ const lineNum = i + 1;
354
+ const merged = mergeLineAttribution(journalByLine.get(lineNum), legacyMap.get(lineNum));
355
+ const git = gitByLine.get(lineNum);
356
+ const sha = git?.shortSha ?? ' ';
357
+ const author = (git?.author ?? '').slice(0, 12).padEnd(12);
358
+ const origin = merged.origin;
359
+ const tool = (merged.tool || merged.model || '').slice(0, 10);
360
+ const color = COLORS[origin] ?? COLORS['unknown'];
361
+ const num = String(lineNum).padStart(5, ' ');
362
+ const conf = merged.confidence > 0 ? `${(merged.confidence * 100).toFixed(0)}%` : ' ';
363
+ const truncated = line.length > 60 ? line.slice(0, 57) + '...' : line;
364
+ originCounts[origin] = (originCounts[origin] ?? 0) + 1;
365
+ sourceCounts[merged.source] = (sourceCounts[merged.source] ?? 0) + 1;
366
+ console.log(`${num} ${sha} ${author} ${merged.tier} ${conf.padStart(4)} ${color}${origin.padEnd(8)} ${tool.padEnd(10)}${COLORS['reset']} ${merged.source.padEnd(8)} ${truncated}`);
367
+ });
368
+ const originSummary = Object.entries(originCounts)
369
+ .map(([o, c]) => `${o}:${c}`)
370
+ .join(' ');
371
+ const srcSummary = Object.entries(sourceCounts)
372
+ .map(([s, c]) => `${s}:${c}`)
373
+ .join(' ');
374
+ console.log('');
375
+ console.log(`Origins: ${originSummary}`);
376
+ console.log(`Sources: ${srcSummary}`);
377
+ });
378
+ // ─── push ─────────────────────────────────────────────────────────────────────
379
+ program
380
+ .command('push')
381
+ .description('Manually push pending journal events to the cloud (org installs only)')
382
+ .option('--project <name>', 'Override project name')
383
+ .option('--dry-run', 'Show what would be uploaded without uploading')
384
+ .action(async (opts) => {
385
+ const workspace = getWorkspaceRoot();
386
+ const journalRoot = getJournalRoot(workspace);
387
+ // Load install info to get org_id and auth_token
388
+ const infoPath = join(journalRoot, 'install.json');
389
+ if (!existsSync(infoPath)) {
390
+ console.error('No install.json found. Is the daemon initialized?');
391
+ process.exit(1);
392
+ }
393
+ const install = JSON.parse(readFileSync(infoPath, 'utf-8'));
394
+ if (!install.org_id) {
395
+ console.log('Cloud sync disabled (personal install). Sign in to an org to enable cloud push.');
396
+ return;
397
+ }
398
+ const queue = new (await import('./cloud/pending.js')).PendingQueue(journalRoot);
399
+ queue.load();
400
+ const pending = queue.pendingCount;
401
+ if (pending === 0) {
402
+ console.log('Nothing to push — no pending events.');
403
+ return;
404
+ }
405
+ if (opts.dryRun) {
406
+ console.log(`Dry run: ${pending} event(s) would be uploaded to ${INGEST_ENDPOINT}.`);
407
+ return;
408
+ }
409
+ // We need a live Journal to use CloudShipper; use a read-only shimmed one
410
+ // by starting a minimal shipper against the pending queue directly
411
+ console.log(`Uploading ${pending} pending event(s)...`);
412
+ const shipper = new CloudShipper(
413
+ // Minimal Journal stub — shipper only needs subscribe() for start()
414
+ { subscribe: () => () => { } }, { ...install, project_name: opts.project ?? install.project_name ?? 'default' }, journalRoot);
415
+ const result = await shipper.flush();
416
+ console.log(`Pushed: accepted=${result.accepted} skipped=${result.skipped} still-pending=${result.pending}`);
417
+ });
418
+ const INGEST_ENDPOINT = 'https://api.imrishav.life/v2/journal/ingest';
419
+ // ─── export ───────────────────────────────────────────────────────────────────
420
+ program
421
+ .command('export')
422
+ .description('Export raw journal events to stdout')
423
+ .option('--format <fmt>', 'Output format: ndjson (default) or json', 'ndjson')
424
+ .option('--from-seq <n>', 'Start from sequence number', '0')
425
+ .action(async (opts) => {
426
+ const workspace = getWorkspaceRoot();
427
+ const journalRoot = getJournalRoot(workspace);
428
+ const paths = resolvePaths(journalRoot);
429
+ const fromSeq = parseInt(opts.fromSeq, 10) || 0;
430
+ const { SegmentReader } = await import('./log/segment.js');
431
+ const events = [];
432
+ for (const evt of SegmentReader.readAll(paths.segmentsDir)) {
433
+ if (evt.seq >= fromSeq)
434
+ events.push(evt);
435
+ }
436
+ if (opts.format === 'json') {
437
+ process.stdout.write(JSON.stringify(events, null, 2) + '\n');
438
+ }
439
+ else {
440
+ for (const evt of events) {
441
+ process.stdout.write(JSON.stringify(evt) + '\n');
442
+ }
443
+ }
444
+ });
445
+ // ─── verify ───────────────────────────────────────────────────────────────────
446
+ program
447
+ .command('verify')
448
+ .description('Verify chain integrity (seq gaps, prev-hash pointers, HMAC sigs)')
449
+ .option('--from-seq <n>', 'Start from sequence number')
450
+ .option('--to-seq <n>', 'End at sequence number')
451
+ .option('--report <fmt>', 'Output format: text (default) or json', 'text')
452
+ .action(async (opts) => {
453
+ const workspace = getWorkspaceRoot();
454
+ const journalRoot = getJournalRoot(workspace);
455
+ const paths = resolvePaths(journalRoot);
456
+ const { verifyChain, formatVerifyResult } = await import('./verify/chain-verify.js');
457
+ const result = await verifyChain({
458
+ fromSeq: opts.fromSeq !== undefined ? parseInt(opts.fromSeq, 10) : undefined,
459
+ toSeq: opts.toSeq !== undefined ? parseInt(opts.toSeq, 10) : undefined,
460
+ report: opts.report,
461
+ keysDir: paths.keysDir,
462
+ segmentsDir: paths.segmentsDir,
463
+ });
464
+ process.stdout.write(formatVerifyResult(result, opts.report));
465
+ process.exit(result.ok ? 0 : 1);
466
+ });
467
+ // ─── install-hooks ────────────────────────────────────────────────────────────
468
+ program
469
+ .command('install-hooks')
470
+ .description('Install preToolUse hooks into all detected AI tools')
471
+ .action(async () => {
472
+ const workspace = getWorkspaceRoot();
473
+ const sdk = await loadAdapterSdk();
474
+ if (!sdk) {
475
+ console.error('@omnitype-code/adapter-sdk not found. Run: npm install @omnitype-code/adapter-sdk');
476
+ process.exit(1);
477
+ }
478
+ const results = sdk.installHooks(workspace);
479
+ console.log('\nOmniType — Hook Installation\n');
480
+ for (const r of results) {
481
+ if (r.skipped) {
482
+ console.log(` - ${r.toolId.padEnd(16)} (not installed)`);
483
+ }
484
+ else if (r.alreadyCurrent) {
485
+ console.log(` ✓ ${r.toolId.padEnd(16)} already up to date`);
486
+ }
487
+ else if (r.installed) {
488
+ console.log(` ✓ ${r.toolId.padEnd(16)} hook installed`);
489
+ }
490
+ else {
491
+ console.log(` ✗ ${r.toolId.padEnd(16)} FAILED: ${r.reason}`);
492
+ }
493
+ }
494
+ const installed = results.filter((r) => r.installed).length;
495
+ console.log(`\n${installed} hook(s) installed/updated.`);
496
+ });
497
+ // ─── Internal daemon runner (called when --foreground) ────────────────────────
498
+ function omniHome() {
499
+ return join(process.env['HOME'] ?? process.env['USERPROFILE'] ?? '', '.omnitype');
500
+ }
501
+ function apiBase() {
502
+ if (process.env['OMNITYPE_API_URL'])
503
+ return process.env['OMNITYPE_API_URL'].replace(/\/$/, '');
504
+ try {
505
+ const cfg = JSON.parse(readFileSync(join(omniHome(), 'config.json'), 'utf-8'));
506
+ if (cfg.api_url)
507
+ return cfg.api_url.replace(/\/$/, '');
508
+ }
509
+ catch { /* default below */ }
510
+ return 'https://api.imrishav.life';
511
+ }
512
+ /**
513
+ * Resolve whether hardened mode is enabled. Source of truth is org policy, which
514
+ * the daemon fetches from the backend and caches to ~/.omnitype/policy.json.
515
+ * Reading the cache is synchronous so the key-storage decision is available at
516
+ * startup; a fresh value (see refreshPolicyCache) applies on the next start.
517
+ * Personal / non-hardened installs leave this false → frictionless, no prompt.
518
+ */
519
+ function resolveHardened() {
520
+ try {
521
+ const cache = JSON.parse(readFileSync(join(omniHome(), 'policy.json'), 'utf-8'));
522
+ // Hardened only applies to org installs (defense in depth: never harden a personal install).
523
+ return cache.hardened === true && !!cache.org_id;
524
+ }
525
+ catch {
526
+ return false;
527
+ }
528
+ }
529
+ /**
530
+ * Fetch the latest org policy and cache it. Runs in the background on daemon
531
+ * start so an admin toggling hardened mode propagates to every member with zero
532
+ * per-user setup — they just need a daemon restart for the new key-storage mode
533
+ * to take effect. Returns the freshly-fetched hardened value (or null if offline).
534
+ */
535
+ async function refreshPolicyCache() {
536
+ let token = '';
537
+ try {
538
+ const cfg = JSON.parse(readFileSync(join(omniHome(), 'config.json'), 'utf-8'));
539
+ token = cfg.token ?? process.env['OMNITYPE_TOKEN'] ?? '';
540
+ }
541
+ catch {
542
+ token = process.env['OMNITYPE_TOKEN'] ?? '';
543
+ }
544
+ if (!token)
545
+ return null;
546
+ try {
547
+ const controller = new AbortController();
548
+ const t = setTimeout(() => controller.abort(), 10_000);
549
+ const resp = await fetch(`${apiBase()}/v2/journal/merge/_policy`, {
550
+ headers: { Authorization: `Bearer ${token}` },
551
+ signal: controller.signal,
552
+ });
553
+ clearTimeout(t);
554
+ if (!resp.ok)
555
+ return null;
556
+ const policy = await resp.json();
557
+ mkdirSync(omniHome(), { recursive: true });
558
+ writeFileSync(join(omniHome(), 'policy.json'), JSON.stringify(policy, null, 2));
559
+ return policy.hardened === true && !!policy.org_id;
560
+ }
561
+ catch {
562
+ return null;
563
+ }
564
+ }
565
+ function runDaemon(workspace, journalRoot, socketPath) {
566
+ process.title = 'omnitype-daemon';
567
+ const hardened = resolveHardened();
568
+ if (hardened) {
569
+ console.error('[omnitype-daemon] hardened mode ON — install key held in OS keychain, not mirrored to disk');
570
+ }
571
+ // Refresh org policy in the background; a change applies on the next restart.
572
+ void refreshPolicyCache().then((fresh) => {
573
+ if (fresh !== null && fresh !== hardened) {
574
+ console.error(`[omnitype-daemon] org policy changed (hardened=${fresh}). Restart the daemon to apply: omnitype-daemon restart`);
575
+ }
576
+ });
577
+ const journal = new Journal(journalRoot, workspace, { hardened });
578
+ const installKey = journal.getInstallKey();
579
+ const server = new DaemonServer(journal, socketPath, installKey, hardened);
580
+ // Start cloud shipper (org-only; silently disabled for personal installs)
581
+ const installInfo = journal.getInstallInfo();
582
+ const shipper = new CloudShipper(journal, installInfo, journalRoot);
583
+ shipper.start();
584
+ // Cloud anchor — pins the chain head server-side so local tampering / deletion
585
+ // is detectable. Org-only and zero-config: it reuses the login token already in
586
+ // install.json, so an org user does nothing extra. Personal installs skip it.
587
+ const ingestUrl = process.env['OMNITYPE_INGEST_URL'] ?? 'https://api.imrishav.life/v2/journal/ingest';
588
+ const anchorAuth = installInfo.auth_token ?? process.env['OMNITYPE_TOKEN'] ?? '';
589
+ const anchor = new CloudAnchor(installKey, installInfo.install_id, new AnchorQueue(journalRoot), () => journal.getChainHead(), {
590
+ ingestUrl,
591
+ authToken: anchorAuth,
592
+ orgId: installInfo.org_id ?? null,
593
+ projectName: installInfo.project_name ?? 'default',
594
+ });
595
+ if (anchor.publishing) {
596
+ anchor.start();
597
+ console.error(`[omnitype-daemon] cloud anchor ON — chain head pinned server-side every 30s${hardened ? ' (hardened)' : ''}`);
598
+ }
599
+ // Wire adapter suite (lazy — graceful if adapter-sdk not installed)
600
+ loadAdapterSdk().then((sdk) => {
601
+ if (!sdk)
602
+ return;
603
+ const info = journal.getInstallInfo();
604
+ const emit = (actor, body, tier, adapterId) => {
605
+ journal.append(actor, body, tier, adapterId);
606
+ };
607
+ const suite = sdk.startBuiltinAdapters(emit, workspace, info.user, info.host);
608
+ process.once('SIGTERM', () => suite.stop());
609
+ console.error(`[omnitype-daemon] built-in adapters started (hook-poller + transcript-scavenger + filesystem-watcher)`);
610
+ });
611
+ process.on('SIGTERM', () => {
612
+ console.error('[omnitype-daemon] shutting down');
613
+ shipper.stop();
614
+ anchor.stop();
615
+ server.close().then(() => {
616
+ journal.close();
617
+ process.exit(0);
618
+ });
619
+ });
620
+ process.on('SIGINT', () => process.kill(process.pid, 'SIGTERM'));
621
+ // Heartbeat every 5s
622
+ setInterval(() => {
623
+ journal.appendSync({
624
+ kind: 'system',
625
+ tool: 'omnitype-daemon',
626
+ user: journal.getInstallInfo().user,
627
+ host: journal.getInstallInfo().host,
628
+ workspace,
629
+ }, {
630
+ type: 'Heartbeat',
631
+ head_seq: journal.getChainHead().seq,
632
+ head_hash: journal.getChainHead().lastHash,
633
+ }, 'T0', 'omnitype-daemon');
634
+ }, 5000).unref();
635
+ console.error(`[omnitype-daemon] ready — workspace: ${workspace}`);
636
+ }
637
+ program.parse(process.argv);
638
+ //# sourceMappingURL=cli.js.map