viberag 0.6.2 → 0.7.1

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 CHANGED
@@ -132,6 +132,8 @@ The following sections describe manual MCP server setup configurations for vario
132
132
  claude mcp add viberag -- npx viberag-mcp
133
133
  ```
134
134
 
135
+ > **Tool Search:** Claude Code supports MCP Tool Search (beta) to discover MCP tools on-demand when many tools are installed. It is enabled by default; to force-enable set `ENABLE_TOOL_SEARCH=true` when launching `claude`.
136
+
135
137
  **Global Config:** `~/.claude.json`
136
138
 
137
139
  ```json
package/dist/cli/app.js CHANGED
@@ -28,9 +28,17 @@ import { getViberagDir } from '../daemon/lib/constants.js';
28
28
  import { loadConfig } from '../daemon/lib/config.js';
29
29
  import { checkNpmForUpdate } from '../daemon/lib/update-check.js';
30
30
  import { checkV2IndexCompatibility } from '../daemon/services/v2/manifest.js';
31
+ import { createTelemetryClient } from '../daemon/lib/telemetry/client.js';
32
+ import { initSentry } from '../daemon/lib/telemetry/sentry.js';
31
33
  const require = createRequire(import.meta.url);
32
34
  // Path is relative from dist/ after compilation
33
35
  const { version } = require('../../package.json');
36
+ const cliTelemetry = createTelemetryClient({
37
+ service: 'cli',
38
+ projectRoot: process.cwd(),
39
+ version,
40
+ });
41
+ const cliSentry = initSentry({ service: 'cli', version });
34
42
  // Available slash commands for autocomplete with descriptions
35
43
  const COMMANDS = [
36
44
  { command: '/help', description: 'Show available commands' },
@@ -46,6 +54,8 @@ const COMMANDS = [
46
54
  { command: '/status', description: 'Show daemon and index status' },
47
55
  { command: '/cancel', description: 'Cancel indexing or warmup' },
48
56
  { command: '/mcp-setup', description: 'Configure MCP for AI tools' },
57
+ { command: '/telemetry', description: 'Configure telemetry mode' },
58
+ { command: '/privacy-policy', description: 'Show privacy policy' },
49
59
  { command: '/clean', description: 'Remove Viberag from project' },
50
60
  { command: '/quit', description: 'Exit the application' },
51
61
  ];
@@ -218,6 +228,11 @@ function AppContent() {
218
228
  startMcpSetupWizard,
219
229
  startCleanWizard,
220
230
  isInitialized: isInitialized ?? false,
231
+ telemetry: cliTelemetry,
232
+ shutdownTelemetry: async () => {
233
+ await cliTelemetry.shutdown();
234
+ await cliSentry.shutdown();
235
+ },
221
236
  });
222
237
  const handleSubmit = (text) => {
223
238
  if (!text.trim())
@@ -3,6 +3,7 @@
3
3
  * Consolidates all command routing and handler implementations.
4
4
  */
5
5
  import type { SearchResultsData } from '../../common/types.js';
6
+ import type { TelemetryClient } from '../../daemon/lib/telemetry/client.js';
6
7
  type CommandContext = {
7
8
  addOutput: (type: 'user' | 'system', content: string) => void;
8
9
  addSearchResults: (data: SearchResultsData) => void;
@@ -12,8 +13,10 @@ type CommandContext = {
12
13
  startMcpSetupWizard: (showPrompt?: boolean) => void;
13
14
  startCleanWizard: () => void;
14
15
  isInitialized: boolean;
16
+ telemetry: TelemetryClient;
17
+ shutdownTelemetry: () => Promise<void>;
15
18
  };
16
- export declare function useCommands({ addOutput, addSearchResults, projectRoot, stdout, startInitWizard, startMcpSetupWizard, startCleanWizard, isInitialized, }: CommandContext): {
19
+ export declare function useCommands({ addOutput, addSearchResults, projectRoot, stdout, startInitWizard, startMcpSetupWizard, startCleanWizard, isInitialized, telemetry, shutdownTelemetry, }: CommandContext): {
17
20
  isCommand: (text: string) => boolean;
18
21
  executeCommand: (text: string) => void;
19
22
  };
@@ -2,13 +2,22 @@
2
2
  * CLI command handling hook.
3
3
  * Consolidates all command routing and handler implementations.
4
4
  */
5
+ import crypto from 'node:crypto';
6
+ import { spawn } from 'node:child_process';
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
5
10
  import { useCallback } from 'react';
6
11
  import { useApp } from 'ink';
7
12
  import { runIndex, formatIndexStats, runSearch, getStatus, loadIndexStats, cancelActivity, runEval, formatEvalReport, } from './handlers.js';
8
13
  import { setupVSCodeTerminal } from '../../common/commands/terminalSetup.js';
9
14
  import { useAppDispatch } from '../store/hooks.js';
10
15
  import { AppActions } from '../store/app/slice.js';
11
- export function useCommands({ addOutput, addSearchResults, projectRoot, stdout, startInitWizard, startMcpSetupWizard, startCleanWizard, isInitialized, }) {
16
+ import { DaemonClient } from '../../client/index.js';
17
+ import { VIBERAG_PRIVACY_POLICY } from '../../daemon/lib/telemetry/privacy-policy.js';
18
+ import { loadUserSettings, parseTelemetryMode, resolveEffectiveTelemetryMode, setTelemetryMode, } from '../../daemon/lib/user-settings.js';
19
+ import { captureException, flushSentry, } from '../../daemon/lib/telemetry/sentry.js';
20
+ export function useCommands({ addOutput, addSearchResults, projectRoot, stdout, startInitWizard, startMcpSetupWizard, startCleanWizard, isInitialized, telemetry, shutdownTelemetry, }) {
12
21
  const dispatch = useAppDispatch();
13
22
  const { exit } = useApp();
14
23
  // Command handlers
@@ -25,6 +34,8 @@ export function useCommands({ addOutput, addSearchResults, projectRoot, stdout,
25
34
  /status - Show index status
26
35
  /cancel [target] - Cancel indexing or warmup (targets: indexing, warmup)
27
36
  /mcp-setup - Configure MCP server for AI coding tools
37
+ /telemetry [mode] - Set telemetry (disabled|stripped|default)
38
+ /privacy-policy - Show privacy policy for telemetry
28
39
  /clean - Remove VibeRAG from project (delete project data)
29
40
  /quit - Exit
30
41
 
@@ -126,6 +137,134 @@ Manual MCP Setup:
126
137
  const handleClean = useCallback(() => {
127
138
  startCleanWizard();
128
139
  }, [startCleanWizard]);
140
+ const handleTelemetry = useCallback((arg) => {
141
+ const requested = arg?.trim();
142
+ const showCurrent = async () => {
143
+ const settings = await loadUserSettings();
144
+ const effective = resolveEffectiveTelemetryMode(settings);
145
+ const source = effective.source === 'env'
146
+ ? 'VIBERAG_TELEMETRY env var'
147
+ : 'global settings file';
148
+ addOutput('system', `Telemetry mode: ${effective.mode} (from ${source})\n\nModes:\n disabled - no telemetry or error reporting\n stripped - privacy-preserving telemetry (no query text)\n default - includes query text (best-effort redaction)\n\nSet with:\n /telemetry disabled|stripped|default\n\nThis setting is global (applies to CLI, daemon, and MCP).`);
149
+ await telemetry.captureOperation({
150
+ operation_kind: 'cli_command',
151
+ name: '/telemetry',
152
+ projectRoot,
153
+ input: { action: 'show', effective_mode: effective.mode },
154
+ output: null,
155
+ success: true,
156
+ duration_ms: 0,
157
+ });
158
+ };
159
+ const setMode = async (mode) => {
160
+ const parsed = parseTelemetryMode(mode);
161
+ if (!parsed) {
162
+ addOutput('system', `Invalid telemetry mode: ${mode}\n\nUsage:\n /telemetry disabled|stripped|default`);
163
+ return;
164
+ }
165
+ await setTelemetryMode(parsed);
166
+ addOutput('system', `Telemetry mode set to: ${parsed}\n\nThis setting is global (applies to CLI, daemon, and MCP).\nIf the daemon is already running, it may take a few seconds to pick up the change.`);
167
+ await telemetry.captureOperation({
168
+ operation_kind: 'cli_command',
169
+ name: '/telemetry',
170
+ projectRoot,
171
+ input: { action: 'set', mode: parsed },
172
+ output: null,
173
+ success: true,
174
+ duration_ms: 0,
175
+ });
176
+ };
177
+ void (async () => {
178
+ if (!requested) {
179
+ await showCurrent();
180
+ return;
181
+ }
182
+ await setMode(requested);
183
+ })().catch(err => {
184
+ addOutput('system', `Telemetry error: ${err.message}`);
185
+ });
186
+ }, [addOutput, projectRoot, telemetry]);
187
+ const handlePrivacyPolicy = useCallback(() => {
188
+ addOutput('system', VIBERAG_PRIVACY_POLICY);
189
+ telemetry.capture({
190
+ event: 'viberag_privacy_policy_viewed',
191
+ properties: { service: 'cli' },
192
+ });
193
+ }, [addOutput, telemetry]);
194
+ const handleTestException = useCallback((arg) => {
195
+ void (async () => {
196
+ const testId = crypto.randomUUID();
197
+ const settings = await loadUserSettings();
198
+ const effective = resolveEffectiveTelemetryMode(settings);
199
+ if (effective.mode === 'disabled') {
200
+ addOutput('system', `Telemetry is disabled, so error reporting is also disabled.\n\nSet with:\n /telemetry default\n\nThen re-run:\n /test-exception`);
201
+ return;
202
+ }
203
+ addOutput('system', `Triggering test exceptions (test_id=${testId}).\nThis is an undocumented command.`);
204
+ // CLI exception (captured, not fatal)
205
+ const cliError = new Error(`VibeRAG test exception (cli)${arg ? `: ${arg}` : ''}`);
206
+ captureException(cliError, {
207
+ tags: { service: 'cli', test_exception: 'true' },
208
+ extra: { test_id: testId },
209
+ });
210
+ await flushSentry(2000);
211
+ // Daemon exception (captured inside daemon handler)
212
+ if (isInitialized) {
213
+ const client = new DaemonClient(projectRoot);
214
+ try {
215
+ await client.testException(`test_id=${testId}`);
216
+ }
217
+ catch {
218
+ // Expected: daemon throws
219
+ }
220
+ finally {
221
+ await client.disconnect();
222
+ }
223
+ }
224
+ else {
225
+ addOutput('system', 'Skipping daemon test exception (project not initialized).');
226
+ }
227
+ // MCP exception (one-shot process)
228
+ const modulePath = fileURLToPath(import.meta.url);
229
+ const mcpScriptPath = path.resolve(path.dirname(modulePath), '../../mcp/index.js');
230
+ const env = {
231
+ ...process.env,
232
+ VIBERAG_TEST_EXCEPTION: '1',
233
+ VIBERAG_TEST_EXCEPTION_ID: testId,
234
+ };
235
+ const spawnAndWait = (command, args) => new Promise((resolve, reject) => {
236
+ const child = spawn(command, args, {
237
+ cwd: projectRoot,
238
+ env,
239
+ stdio: 'ignore',
240
+ windowsHide: true,
241
+ });
242
+ child.on('error', reject);
243
+ child.on('exit', code => resolve(code));
244
+ });
245
+ try {
246
+ await fs.access(mcpScriptPath);
247
+ const exitCode = await spawnAndWait(process.execPath, [
248
+ mcpScriptPath,
249
+ ]);
250
+ addOutput('system', `MCP test exception process exited (code ${exitCode ?? 'unknown'}).`);
251
+ }
252
+ catch {
253
+ try {
254
+ const exitCode = await spawnAndWait('npx', ['viberag-mcp']);
255
+ addOutput('system', `MCP test exception process exited (code ${exitCode ?? 'unknown'}).`);
256
+ }
257
+ catch (error) {
258
+ const message = error instanceof Error ? error.message : String(error);
259
+ addOutput('system', `Failed to run MCP test exception process: ${message}\n\nTry manually:\n VIBERAG_TEST_EXCEPTION=1 npx viberag-mcp`);
260
+ }
261
+ }
262
+ addOutput('system', `Done. Check Sentry for events tagged test_exception=true (test_id=${testId}).`);
263
+ })().catch(err => {
264
+ const message = err instanceof Error ? err.message : String(err);
265
+ addOutput('system', `Test exception command failed: ${message}`);
266
+ });
267
+ }, [addOutput, isInitialized, projectRoot]);
129
268
  const handleUnknown = useCallback((command) => {
130
269
  addOutput('system', `Unknown command: ${command}. Type /help for available commands.`);
131
270
  }, [addOutput]);
@@ -153,6 +292,16 @@ Manual MCP Setup:
153
292
  handleCancel(target || undefined);
154
293
  return;
155
294
  }
295
+ if (command.startsWith('/telemetry')) {
296
+ const arg = trimmed.slice('/telemetry'.length).trim();
297
+ handleTelemetry(arg || undefined);
298
+ return;
299
+ }
300
+ if (command.startsWith('/test-exception')) {
301
+ const arg = trimmed.slice('/test-exception'.length).trim();
302
+ handleTestException(arg || undefined);
303
+ return;
304
+ }
156
305
  switch (command) {
157
306
  case '/help':
158
307
  handleHelp();
@@ -181,6 +330,9 @@ Manual MCP Setup:
181
330
  case '/mcp-setup':
182
331
  handleMcpSetup();
183
332
  break;
333
+ case '/privacy-policy':
334
+ handlePrivacyPolicy();
335
+ break;
184
336
  case '/clean':
185
337
  case '/uninstall':
186
338
  handleClean();
@@ -188,7 +340,9 @@ Manual MCP Setup:
188
340
  case '/quit':
189
341
  case '/exit':
190
342
  case '/q':
191
- exit();
343
+ void shutdownTelemetry()
344
+ .catch(() => { })
345
+ .finally(() => exit());
192
346
  break;
193
347
  default:
194
348
  handleUnknown(command);
@@ -206,6 +360,10 @@ Manual MCP Setup:
206
360
  handleEval,
207
361
  handleCancel,
208
362
  handleMcpSetup,
363
+ handleTelemetry,
364
+ handlePrivacyPolicy,
365
+ handleTestException,
366
+ shutdownTelemetry,
209
367
  handleClean,
210
368
  handleUnknown,
211
369
  ]);
@@ -19,6 +19,7 @@ export declare class DaemonClient {
19
19
  private readonly socketPath;
20
20
  private readonly autoStart;
21
21
  private readonly connectTimeout;
22
+ private readonly clientSource;
22
23
  private connection;
23
24
  private connectPromise;
24
25
  constructor(options: DaemonClientOptions | string);
@@ -48,6 +49,7 @@ export declare class DaemonClient {
48
49
  * Ensure connected before making a request.
49
50
  */
50
51
  private ensureConnected;
52
+ private request;
51
53
  /**
52
54
  * Search the codebase.
53
55
  */
@@ -75,7 +77,7 @@ export declare class DaemonClient {
75
77
  /**
76
78
  * Index the codebase.
77
79
  */
78
- index(options?: ClientIndexOptions): Promise<IndexStats>;
80
+ index(options?: ClientIndexOptions, timeoutMs?: number): Promise<IndexStats>;
79
81
  /**
80
82
  * Start indexing asynchronously.
81
83
  */
@@ -115,6 +117,12 @@ export declare class DaemonClient {
115
117
  indexStatus: string;
116
118
  protocolVersion: number;
117
119
  }>;
120
+ /**
121
+ * Trigger a test exception in the daemon (undocumented).
122
+ *
123
+ * Useful for validating Sentry error reporting.
124
+ */
125
+ testException(message?: string): Promise<void>;
118
126
  }
119
127
  export { getSocketPath, getLockPath, isDaemonRunning, isDaemonLocked, } from './auto-start.js';
120
128
  export type { DaemonClientOptions, ClientSearchOptions, ClientIndexOptions, DaemonStatusResponse, PingResponse, SearchResults, IndexStats, WatcherStatus, SlotState, FailedChunk, } from './types.js';
@@ -44,6 +44,12 @@ export class DaemonClient {
44
44
  writable: true,
45
45
  value: void 0
46
46
  });
47
+ Object.defineProperty(this, "clientSource", {
48
+ enumerable: true,
49
+ configurable: true,
50
+ writable: true,
51
+ value: void 0
52
+ });
47
53
  Object.defineProperty(this, "connection", {
48
54
  enumerable: true,
49
55
  configurable: true,
@@ -61,11 +67,13 @@ export class DaemonClient {
61
67
  this.projectRoot = options;
62
68
  this.autoStart = true;
63
69
  this.connectTimeout = 5000;
70
+ this.clientSource = 'cli';
64
71
  }
65
72
  else {
66
73
  this.projectRoot = options.projectRoot;
67
74
  this.autoStart = options.autoStart ?? true;
68
75
  this.connectTimeout = options.connectTimeout ?? 5000;
76
+ this.clientSource = options.clientSource ?? 'cli';
69
77
  }
70
78
  this.socketPath = getSocketPath(this.projectRoot);
71
79
  }
@@ -144,12 +152,18 @@ export class DaemonClient {
144
152
  await this.connect();
145
153
  }
146
154
  }
155
+ async request(method, params, timeoutMs) {
156
+ await this.ensureConnected();
157
+ const withMeta = params
158
+ ? { ...params, __client: { source: this.clientSource } }
159
+ : { __client: { source: this.clientSource } };
160
+ return this.connection.request(method, withMeta, timeoutMs);
161
+ }
147
162
  /**
148
163
  * Search the codebase.
149
164
  */
150
165
  async search(query, options) {
151
- await this.ensureConnected();
152
- return this.connection.request('search', {
166
+ return this.request('search', {
153
167
  query,
154
168
  ...options,
155
169
  });
@@ -158,86 +172,82 @@ export class DaemonClient {
158
172
  * Fetch a symbol definition row by symbol_id.
159
173
  */
160
174
  async getSymbol(symbol_id) {
161
- await this.ensureConnected();
162
- return this.connection.request('getSymbol', { symbol_id });
175
+ return this.request('getSymbol', { symbol_id });
163
176
  }
164
177
  /**
165
178
  * Find usages for a symbol name or symbol_id.
166
179
  */
167
180
  async findUsages(options) {
168
- await this.ensureConnected();
169
- return this.connection.request('findUsages', options);
181
+ return this.request('findUsages', options);
170
182
  }
171
183
  /**
172
184
  * Run the v2 eval harness (quality + latency).
173
185
  */
174
186
  async eval(options) {
175
- await this.ensureConnected();
176
- return this.connection.request('eval', options);
187
+ return this.request('eval', options);
177
188
  }
178
189
  /**
179
190
  * Expand context for a hit (symbols/chunks/files).
180
191
  */
181
192
  async expandContext(args) {
182
- await this.ensureConnected();
183
- return this.connection.request('expandContext', args);
193
+ return this.request('expandContext', args);
184
194
  }
185
195
  /**
186
196
  * Index the codebase.
187
197
  */
188
- async index(options) {
189
- await this.ensureConnected();
190
- return this.connection.request('index', options);
198
+ async index(options, timeoutMs) {
199
+ return this.request('index', options, timeoutMs);
191
200
  }
192
201
  /**
193
202
  * Start indexing asynchronously.
194
203
  */
195
204
  async indexAsync(options) {
196
- await this.ensureConnected();
197
- return this.connection.request('indexAsync', options);
205
+ return this.request('indexAsync', options);
198
206
  }
199
207
  /**
200
208
  * Get daemon status.
201
209
  * Clients should poll this endpoint for state updates.
202
210
  */
203
211
  async status() {
204
- await this.ensureConnected();
205
- return this.connection.request('status');
212
+ return this.request('status');
206
213
  }
207
214
  /**
208
215
  * Get watcher status.
209
216
  */
210
217
  async watchStatus() {
211
- await this.ensureConnected();
212
- return this.connection.request('watchStatus');
218
+ return this.request('watchStatus');
213
219
  }
214
220
  /**
215
221
  * Request daemon shutdown.
216
222
  */
217
223
  async shutdown(reason) {
218
- await this.ensureConnected();
219
- await this.connection.request('shutdown', { reason });
224
+ await this.request('shutdown', { reason });
220
225
  }
221
226
  /**
222
227
  * Cancel the current daemon activity (indexing or warmup).
223
228
  */
224
229
  async cancel(options) {
225
- await this.ensureConnected();
226
- return this.connection.request('cancel', options);
230
+ return this.request('cancel', options);
227
231
  }
228
232
  /**
229
233
  * Ping the daemon.
230
234
  */
231
235
  async ping() {
232
- await this.ensureConnected();
233
- return this.connection.request('ping');
236
+ return this.request('ping');
234
237
  }
235
238
  /**
236
239
  * Get health information.
237
240
  */
238
241
  async health() {
239
- await this.ensureConnected();
240
- return this.connection.request('health');
242
+ return this.request('health');
243
+ }
244
+ /**
245
+ * Trigger a test exception in the daemon (undocumented).
246
+ *
247
+ * Useful for validating Sentry error reporting.
248
+ */
249
+ async testException(message) {
250
+ await this.request('testException', { message });
241
251
  }
242
252
  }
243
253
  // Re-export types and utilities
@@ -37,6 +37,13 @@ export interface DaemonClientOptions {
37
37
  autoStart?: boolean;
38
38
  /** Connection timeout in ms (default: 5000) */
39
39
  connectTimeout?: number;
40
+ /**
41
+ * Caller identity for telemetry correlation and filtering.
42
+ *
43
+ * Used to reduce duplicate telemetry events (e.g. when MCP tools already
44
+ * capture operations at the tool boundary).
45
+ */
46
+ clientSource?: 'cli' | 'mcp' | 'unknown';
40
47
  }
41
48
  /**
42
49
  * Search options for client.
@@ -12,6 +12,7 @@ import { z } from 'zod';
12
12
  import { PROTOCOL_VERSION } from './protocol.js';
13
13
  import { daemonState } from './state.js';
14
14
  import { isAbortError } from './lib/abort.js';
15
+ import { captureException, flushSentry } from './lib/telemetry/sentry.js';
15
16
  // ============================================================================
16
17
  // Parameter Schemas
17
18
  // ============================================================================
@@ -232,6 +233,23 @@ const healthHandler = async (_params, ctx) => {
232
233
  protocolVersion: PROTOCOL_VERSION,
233
234
  };
234
235
  };
236
+ /**
237
+ * Test exception handler (undocumented).
238
+ *
239
+ * Used to validate Sentry error reporting across services.
240
+ */
241
+ const testExceptionHandler = async (params, _ctx) => {
242
+ const messageValue = params?.['message'];
243
+ const message = typeof messageValue === 'string' ? messageValue : undefined;
244
+ const error = new Error(`VibeRAG test exception (daemon)${message ? `: ${message}` : ''}`);
245
+ captureException(error, {
246
+ tags: { service: 'daemon', test_exception: 'true' },
247
+ extra: { message: message ?? null },
248
+ });
249
+ // Best-effort flush so it shows up quickly.
250
+ await flushSentry(2000);
251
+ throw error;
252
+ };
235
253
  // ============================================================================
236
254
  // Handler Registry
237
255
  // ============================================================================
@@ -253,5 +271,6 @@ export function createHandlers() {
253
271
  shutdown: shutdownHandler,
254
272
  ping: pingHandler,
255
273
  health: healthHandler,
274
+ testException: testExceptionHandler,
256
275
  };
257
276
  }
@@ -22,6 +22,7 @@
22
22
  * - Auto-shutdown after 5 minutes of idle (no connected clients)
23
23
  */
24
24
  import fs from 'node:fs/promises';
25
+ import { createRequire } from 'node:module';
25
26
  import lockfile from 'proper-lockfile';
26
27
  import { DaemonOwner } from './owner.js';
27
28
  import { DaemonServer } from './server.js';
@@ -29,7 +30,17 @@ import { LifecycleManager } from './lifecycle.js';
29
30
  import { createHandlers } from './handlers.js';
30
31
  import { configExists, loadConfig } from './lib/config.js';
31
32
  import { getCanonicalProjectRoot, getDaemonLockPath, getRunDir, } from './lib/constants.js';
33
+ import { createTelemetryClient } from './lib/telemetry/client.js';
34
+ import { captureException, initSentry } from './lib/telemetry/sentry.js';
35
+ const require = createRequire(import.meta.url);
36
+ const pkg = require('../../package.json');
32
37
  const projectRoot = getCanonicalProjectRoot(process.env['VIBERAG_PROJECT_ROOT'] ?? process.cwd());
38
+ const telemetry = createTelemetryClient({
39
+ service: 'daemon',
40
+ projectRoot,
41
+ version: pkg.version,
42
+ });
43
+ const sentry = initSentry({ service: 'daemon', version: pkg.version });
33
44
  // Lock file path - inside the global run directory
34
45
  const LOCK_FILE_PATH = getDaemonLockPath(projectRoot);
35
46
  const RUN_DIR = getRunDir(projectRoot);
@@ -106,7 +117,11 @@ async function main() {
106
117
  // Create components
107
118
  const owner = new DaemonOwner(projectRoot);
108
119
  const server = new DaemonServer(owner);
109
- const lifecycle = new LifecycleManager(server, owner, idleTimeoutMs);
120
+ server.setTelemetry(telemetry);
121
+ const lifecycle = new LifecycleManager(server, owner, idleTimeoutMs, async () => {
122
+ await telemetry.shutdown();
123
+ await sentry.shutdown();
124
+ });
110
125
  // Register handlers
111
126
  server.setHandlers(createHandlers());
112
127
  // Start server
@@ -122,8 +137,11 @@ async function main() {
122
137
  console.error(`[daemon] PID: ${process.pid}`);
123
138
  }
124
139
  // Run main with error handling
125
- main().catch(error => {
140
+ main().catch(async (error) => {
126
141
  // Pass Error object directly to preserve stack trace (ADR-011)
127
142
  console.error('[daemon] Fatal error:', error);
143
+ captureException(error, { tags: { service: 'daemon', fatal: 'true' } });
144
+ await telemetry.shutdown();
145
+ await sentry.shutdown();
128
146
  process.exit(1);
129
147
  });
@@ -86,6 +86,12 @@ export declare function getDaemonPidPath(projectRoot: string): string;
86
86
  * Get the daemon lock file path.
87
87
  */
88
88
  export declare function getDaemonLockPath(projectRoot: string): string;
89
+ /**
90
+ * Get the global user settings file path.
91
+ *
92
+ * Path: {VIBERAG_HOME}/settings.json
93
+ */
94
+ export declare function getUserSettingsPath(): string;
89
95
  /**
90
96
  * Get the global secrets directory.
91
97
  */
@@ -150,6 +150,14 @@ export function getDaemonLockPath(projectRoot) {
150
150
  // ============================================================================
151
151
  // Secrets Paths
152
152
  // ============================================================================
153
+ /**
154
+ * Get the global user settings file path.
155
+ *
156
+ * Path: {VIBERAG_HOME}/settings.json
157
+ */
158
+ export function getUserSettingsPath() {
159
+ return path.join(getViberagHomeDir(), 'settings.json');
160
+ }
153
161
  /**
154
162
  * Get the global secrets directory.
155
163
  */
@@ -0,0 +1,31 @@
1
+ /**
2
+ * PostHog telemetry client wrapper for VibeRAG.
3
+ *
4
+ * - Telemetry is enabled by default (opt-out).
5
+ * - Settings are global under VIBERAG_HOME and shared by CLI/daemon/MCP.
6
+ * - Captures inputs/outputs but strips file contents / code text.
7
+ */
8
+ export type TelemetryServiceName = 'cli' | 'daemon' | 'mcp';
9
+ export type TelemetryClient = {
10
+ captureOperation: (args: {
11
+ operation_kind: 'daemon_method' | 'mcp_tool' | 'cli_command';
12
+ name: string;
13
+ projectRoot?: string;
14
+ input?: unknown;
15
+ output?: unknown;
16
+ success: boolean;
17
+ duration_ms: number;
18
+ error?: unknown;
19
+ request_id?: string;
20
+ }) => Promise<string>;
21
+ capture: (args: {
22
+ event: string;
23
+ properties?: Record<string, unknown>;
24
+ }) => void;
25
+ shutdown: () => Promise<void>;
26
+ };
27
+ export declare function createTelemetryClient(args: {
28
+ service: TelemetryServiceName;
29
+ projectRoot?: string;
30
+ version: string;
31
+ }): TelemetryClient;