feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
27
dexto/packages/analytics/src/constants.ts
Normal file
27
dexto/packages/analytics/src/constants.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// packages/analytics/src/constants.ts
|
||||
|
||||
// Single source for PostHog configuration.
|
||||
// Embed your public key here (safe to publish). Host can stay default.
|
||||
export const DEFAULT_POSTHOG_KEY = 'phc_IJHITHjBKOjDyFiVeilfdumcGniXMuLeXeiLQhYvwDW';
|
||||
export const DEFAULT_POSTHOG_HOST = 'https://app.posthog.com';
|
||||
|
||||
/**
|
||||
* Single opt-out switch for analytics.
|
||||
*
|
||||
* Usage:
|
||||
* DEXTO_ANALYTICS_DISABLED=1 dexto ...
|
||||
*
|
||||
* When set to a truthy value ("1", "true", "yes"), analytics are fully disabled.
|
||||
*/
|
||||
export function isAnalyticsDisabled(): boolean {
|
||||
const v = process.env.DEXTO_ANALYTICS_DISABLED;
|
||||
return typeof v === 'string' && /^(1|true|yes)$/i.test(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic per-command timeout (in milliseconds) used by the analytics wrapper.
|
||||
*
|
||||
* This does NOT terminate the command. It emits a non-terminating timeout
|
||||
* event when the duration threshold is crossed to help diagnose long runs.
|
||||
*/
|
||||
export const COMMAND_TIMEOUT_MS = 120000; // 2 minutes (default for quick commands)
|
||||
175
dexto/packages/analytics/src/events.ts
Normal file
175
dexto/packages/analytics/src/events.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Platform source for analytics events.
|
||||
* Used to distinguish which interface generated the event.
|
||||
*/
|
||||
export type AnalyticsSource = 'cli' | 'webui';
|
||||
|
||||
/**
|
||||
* LLM token consumption event.
|
||||
* Emitted after each LLM response with token usage data.
|
||||
*
|
||||
*/
|
||||
export interface LLMTokensConsumedEvent {
|
||||
source: AnalyticsSource;
|
||||
sessionId: string;
|
||||
provider?: string | undefined;
|
||||
model?: string | undefined;
|
||||
inputTokens?: number | undefined;
|
||||
outputTokens?: number | undefined;
|
||||
reasoningTokens?: number | undefined;
|
||||
totalTokens?: number | undefined;
|
||||
cacheReadTokens?: number | undefined;
|
||||
cacheWriteTokens?: number | undefined;
|
||||
/** Estimated input tokens (before LLM call, using length/4 heuristic) */
|
||||
estimatedInputTokens?: number | undefined;
|
||||
/** Accuracy of estimate vs actual: (estimated - actual) / actual * 100 */
|
||||
estimateAccuracyPercent?: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message sent event.
|
||||
* Emitted when user sends a message to the agent.
|
||||
*/
|
||||
export interface MessageSentEvent {
|
||||
source: AnalyticsSource;
|
||||
sessionId: string;
|
||||
provider: string;
|
||||
model: string;
|
||||
hasImage: boolean;
|
||||
hasFile: boolean;
|
||||
messageLength: number;
|
||||
messageCount?: number | undefined;
|
||||
isQueued?: boolean | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool called event.
|
||||
* Emitted when a tool is invoked by the LLM.
|
||||
*/
|
||||
export interface ToolCalledEvent {
|
||||
source: AnalyticsSource;
|
||||
sessionId: string;
|
||||
toolName: string;
|
||||
mcpServer?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool result event.
|
||||
* Emitted when a tool execution completes.
|
||||
*/
|
||||
export interface ToolResultEvent {
|
||||
source: AnalyticsSource;
|
||||
sessionId: string;
|
||||
toolName: string;
|
||||
success: boolean;
|
||||
approvalStatus?: 'approved' | 'rejected' | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session created event.
|
||||
*/
|
||||
export interface SessionCreatedEvent {
|
||||
source: AnalyticsSource;
|
||||
sessionId: string;
|
||||
trigger: 'first_message' | 'manual' | 'resume';
|
||||
}
|
||||
|
||||
/**
|
||||
* Session reset event (conversation cleared).
|
||||
*/
|
||||
export interface SessionResetEvent {
|
||||
source: AnalyticsSource;
|
||||
sessionId: string;
|
||||
messageCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM switched event.
|
||||
*/
|
||||
export interface LLMSwitchedEvent {
|
||||
source: AnalyticsSource;
|
||||
sessionId?: string | undefined;
|
||||
fromProvider: string;
|
||||
fromModel: string;
|
||||
toProvider: string;
|
||||
toModel: string;
|
||||
trigger: 'user_action' | 'config_change';
|
||||
}
|
||||
|
||||
/**
|
||||
* Session switched event.
|
||||
* Emitted when user switches to a different session.
|
||||
*/
|
||||
export interface SessionSwitchedEvent {
|
||||
source: AnalyticsSource;
|
||||
fromSessionId: string | null;
|
||||
toSessionId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent switched event.
|
||||
* Emitted when user switches to a different agent.
|
||||
*/
|
||||
export interface AgentSwitchedEvent {
|
||||
source: AnalyticsSource;
|
||||
fromAgentId: string | null;
|
||||
toAgentId: string;
|
||||
toAgentName?: string | undefined;
|
||||
sessionId?: string | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP server connected event.
|
||||
* Emitted when an MCP server connects successfully.
|
||||
*/
|
||||
export interface MCPServerConnectedEvent {
|
||||
source: AnalyticsSource;
|
||||
serverName: string;
|
||||
transportType: 'stdio' | 'http' | 'sse';
|
||||
toolCount?: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* File attached event.
|
||||
* Emitted when user attaches a file to a message.
|
||||
*/
|
||||
export interface FileAttachedEvent {
|
||||
source: AnalyticsSource;
|
||||
sessionId: string;
|
||||
fileType: string;
|
||||
fileSizeBytes?: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Image attached event.
|
||||
* Emitted when user attaches an image to a message.
|
||||
*/
|
||||
export interface ImageAttachedEvent {
|
||||
source: AnalyticsSource;
|
||||
sessionId: string;
|
||||
imageType: string;
|
||||
imageSizeBytes?: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared analytics event map containing events supported by ALL platforms.
|
||||
* CLI and WebUI extend this map with platform-specific events.
|
||||
*
|
||||
* IMPORTANT: If an event is tracked by both CLI and WebUI, add it here.
|
||||
* Platform-specific events should be added to the respective platform's event map.
|
||||
*/
|
||||
export interface SharedAnalyticsEventMap {
|
||||
dexto_llm_tokens_consumed: LLMTokensConsumedEvent;
|
||||
dexto_message_sent: MessageSentEvent;
|
||||
dexto_tool_called: ToolCalledEvent;
|
||||
dexto_tool_result: ToolResultEvent;
|
||||
dexto_session_created: SessionCreatedEvent;
|
||||
dexto_session_reset: SessionResetEvent;
|
||||
dexto_llm_switched: LLMSwitchedEvent;
|
||||
dexto_session_switched: SessionSwitchedEvent;
|
||||
dexto_agent_switched: AgentSwitchedEvent;
|
||||
dexto_mcp_server_connected: MCPServerConnectedEvent;
|
||||
dexto_image_attached: ImageAttachedEvent;
|
||||
}
|
||||
|
||||
export type SharedAnalyticsEventName = keyof SharedAnalyticsEventMap;
|
||||
12
dexto/packages/analytics/src/index.ts
Normal file
12
dexto/packages/analytics/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// packages/analytics/src/index.ts
|
||||
// Shared analytics utilities for Dexto CLI and WebUI
|
||||
|
||||
export {
|
||||
DEFAULT_POSTHOG_KEY,
|
||||
DEFAULT_POSTHOG_HOST,
|
||||
COMMAND_TIMEOUT_MS,
|
||||
isAnalyticsDisabled,
|
||||
} from './constants.js';
|
||||
export { loadState, saveState } from './state.js';
|
||||
export type { AnalyticsState } from './state.js';
|
||||
export * from './events.js';
|
||||
90
dexto/packages/analytics/src/state.ts
Normal file
90
dexto/packages/analytics/src/state.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
// packages/analytics/src/state.ts
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
import os from 'os';
|
||||
import { randomUUID, createHash } from 'crypto';
|
||||
import { createRequire } from 'module';
|
||||
import { getDextoGlobalPath } from '@dexto/core';
|
||||
|
||||
/**
|
||||
* Shape of the persisted analytics state written to
|
||||
* ~/.dexto/telemetry/state.json.
|
||||
*
|
||||
* - distinctId: Anonymous ID (UUID) for grouping events by machine.
|
||||
* - createdAt: ISO timestamp when the state was first created.
|
||||
* - commandRunCounts: Local counters per command for coarse diagnostics.
|
||||
*/
|
||||
export interface AnalyticsState {
|
||||
distinctId: string;
|
||||
createdAt: string; // ISO string
|
||||
commandRunCounts?: Record<string, number>;
|
||||
}
|
||||
|
||||
const STATE_DIR = getDextoGlobalPath('telemetry');
|
||||
const STATE_FILE = path.join(STATE_DIR, 'state.json');
|
||||
|
||||
/**
|
||||
* Load the persisted analytics state, creating a new file if missing.
|
||||
* Returns a valid state object with defaults populated.
|
||||
*/
|
||||
export async function loadState(): Promise<AnalyticsState> {
|
||||
try {
|
||||
const content = await fs.readFile(STATE_FILE, 'utf8');
|
||||
const parsed = JSON.parse(content) as Partial<AnalyticsState>;
|
||||
// Validate minimal shape
|
||||
if (!parsed.distinctId) throw new Error('invalid state');
|
||||
return {
|
||||
distinctId: parsed.distinctId,
|
||||
createdAt: parsed.createdAt || new Date().toISOString(),
|
||||
commandRunCounts: parsed.commandRunCounts ?? {},
|
||||
};
|
||||
} catch {
|
||||
await fs.mkdir(STATE_DIR, { recursive: true });
|
||||
const state: AnalyticsState = {
|
||||
distinctId: computeDistinctId(),
|
||||
createdAt: new Date().toISOString(),
|
||||
commandRunCounts: {},
|
||||
};
|
||||
await saveState(state);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the analytics state to ~/.dexto/telemetry/state.json.
|
||||
*/
|
||||
export async function saveState(state: AnalyticsState): Promise<void> {
|
||||
await fs.mkdir(STATE_DIR, { recursive: true });
|
||||
await fs.writeFile(STATE_FILE, JSON.stringify(state, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a stable, privacy‑safe machine identifier so identity
|
||||
* survives ~/.dexto deletion by default.
|
||||
*
|
||||
* Strategy:
|
||||
* - Prefer node-machine-id (hashed), which abstracts platform differences.
|
||||
* - Fallback to a salted/hashed hostname.
|
||||
* - As a last resort, generate a random UUID.
|
||||
*/
|
||||
function computeDistinctId(): string {
|
||||
try {
|
||||
// node-machine-id is CommonJS; require lazily to avoid import-time failures
|
||||
const requireCJS = createRequire(import.meta.url);
|
||||
const { machineIdSync } = requireCJS('node-machine-id') as {
|
||||
machineIdSync: (original?: boolean) => string;
|
||||
};
|
||||
// machineIdSync(true) returns a hashed, stable identifier
|
||||
const id = machineIdSync(true);
|
||||
if (typeof id === 'string' && id.length > 0) return `DEXTO-${id}`;
|
||||
} catch {
|
||||
// fall through to hostname hash
|
||||
}
|
||||
// Fallback: hash hostname to avoid exposing raw value
|
||||
const hostname = os.hostname() || 'unknown-host';
|
||||
const digest = createHash('sha256').update(hostname).digest('hex');
|
||||
if (digest) return `DEXTO-${digest.slice(0, 32)}`;
|
||||
// Last resort
|
||||
return `DEXTO-${randomUUID()}`;
|
||||
}
|
||||
Reference in New Issue
Block a user