UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

143 lines (142 loc) • 6.53 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * Usage event helpers for telemetry and utility-based re-ranking. * * Schema (created by ensureUsageEventsSchema): * id, event_type, query, entry_id (nullable), entry_ref, signal, metadata, source, created_at */ import { rethrowIfTestIsolationError } from "../../core/errors.js"; const USAGE_EVENT_SOURCES = new Set(["user", "improve", "task", "audit", "unknown"]); /** * Resolve subprocess provenance without treating an invalid value as user * demand. * * `fallback` (spec docs/plans/specs/p1b-model-extraction.md §5.2, F-1) is what * an unset/empty ambient value resolves to — it defaults to `"user"`, which * reproduces every pre-P1b call site byte-for-byte (P-07). A caller that * already knows the invocation's provenance (the task runner, threading its * `ExecutionProvenanceContext`) passes its resolved value as the fallback * instead, so a recognized ambient `AKM_EVENT_SOURCE` still wins everywhere it * won before, and only the *default* changes. */ export function resolveUsageEventSource(env = process.env, fallback = "user") { const raw = env.AKM_EVENT_SOURCE; if (raw === undefined || raw === "") return fallback; return USAGE_EVENT_SOURCES.has(raw) ? raw : "unknown"; } // ── Schema ────────────────────────────────────────────────────────────────── export function ensureUsageEventsSchema(db) { db.exec(` CREATE TABLE IF NOT EXISTS usage_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_type TEXT NOT NULL, query TEXT, entry_id INTEGER, entry_ref TEXT, signal TEXT, metadata TEXT, source TEXT NOT NULL DEFAULT 'user', created_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_usage_events_entry ON usage_events(entry_id); CREATE INDEX IF NOT EXISTS idx_usage_events_type ON usage_events(event_type); CREATE INDEX IF NOT EXISTS idx_usage_events_ref ON usage_events(entry_ref); CREATE INDEX IF NOT EXISTS idx_usage_events_source ON usage_events(source); `); } // ── Insert ─────────────────────────────────────────────────────────────────── /** * Insert a usage event into the database. Fire-and-forget: errors are * silently caught so callers are never blocked or disrupted. */ export function insertUsageEvent(db, event) { try { db.prepare(`INSERT INTO usage_events (event_type, query, entry_id, entry_ref, signal, metadata, source) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(event.event_type, event.query ?? null, event.entry_id ?? null, event.entry_ref ?? null, event.signal ?? null, event.metadata ?? null, event.source ?? "unknown"); } catch (error) { rethrowIfTestIsolationError(error); /* fire-and-forget: silently ignore errors */ } } // ── Query ──────────────────────────────────────────────────────────────────── /** * Aggregate positive/negative feedback counts for a single entry. * * Lifted verbatim from `akm feedback` (feedback-cli.ts) where the same * SUM(CASE …) query was hand-rolled inline. Returns plain numbers (NULL SUMs * over an empty set are coalesced to 0) so the result fully materialises before * any owning connection closes. */ export function countFeedbackSignals(db, entryId) { const counts = db .prepare(`SELECT SUM(CASE WHEN signal = 'positive' THEN 1 ELSE 0 END) AS pos, SUM(CASE WHEN signal = 'negative' THEN 1 ELSE 0 END) AS neg FROM usage_events WHERE event_type = 'feedback' AND entry_id = ? AND source = 'user'`) .get(entryId); return { pos: counts?.pos ?? 0, neg: counts?.neg ?? 0 }; } /** * Count usage events of a given `event_type`. * * Lifted verbatim from `akm improve` (improve.ts) where the show-event count * was hand-rolled inline to drive the warning that the retrieval scope matches * only search-retrieved assets. */ export function countUsageEventsByType(db, eventType) { return db.prepare("SELECT COUNT(*) AS cnt FROM usage_events WHERE event_type = ?").get(eventType) .cnt; } /** * Durable refs a user-attributed `search`, `curate` or `show` returned, or a * user `feedback` named, at or after `sinceIso`. Machine traffic (`improve`, * `task`, `audit`, `unknown`) is not demand, as in {@link countFeedbackSignals}. */ export function listUsedEntryRefs(db, sinceIso) { const rows = db .prepare(`SELECT DISTINCT entry_ref FROM usage_events WHERE event_type IN ('search', 'curate', 'show', 'feedback') AND source = 'user' AND entry_ref IS NOT NULL AND julianday(created_at) >= julianday(?)`) .all(sinceIso); return rows.map((row) => row.entry_ref); } /** * The distinct query texts of user `search` and `curate` events that returned * `conceptId` (any bundle prefix), most recent first. */ export function listRetrievalQueries(db, conceptId) { const rows = db .prepare(`SELECT query, MAX(created_at) AS last_at FROM usage_events WHERE event_type IN ('search', 'curate') AND source = 'user' AND query IS NOT NULL AND trim(query) != '' AND instr(entry_ref, '//') > 0 AND substr(entry_ref, instr(entry_ref, '//') + 2) = ? GROUP BY query ORDER BY last_at DESC`) .all(conceptId); return rows.map((row) => row.query); } /** Usage events older than this many days are purged on every `akm index`. */ export const USAGE_EVENT_RETENTION_DAYS = 90; /** * Delete usage events older than the given number of days. */ export function purgeOldUsageEvents(db, retentionDays) { if (!Number.isFinite(retentionDays) || retentionDays <= 0) return; try { const cutoff = new Date(Date.now() - retentionDays * 86_400_000).toISOString(); db.prepare("DELETE FROM usage_events WHERE created_at < ?").run(cutoff); } catch { /* Table may not exist yet */ } }