@deerdawn/mcp-server
Advanced tools
| #!/usr/bin/env node | ||
| import { createRequire } from "module"; | ||
| const require = createRequire(import.meta.url); | ||
| var __create = Object.create; | ||
| var __defProp = Object.defineProperty; | ||
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
| var __getOwnPropNames = Object.getOwnPropertyNames; | ||
| var __getProtoOf = Object.getPrototypeOf; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { | ||
| get: (a, b) => (typeof require !== "undefined" ? require : a)[b] | ||
| }) : x)(function(x) { | ||
| if (typeof require !== "undefined") return require.apply(this, arguments); | ||
| throw Error('Dynamic require of "' + x + '" is not supported'); | ||
| }); | ||
| var __esm = (fn, res) => function __init() { | ||
| return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; | ||
| }; | ||
| var __commonJS = (cb, mod) => function __require2() { | ||
| return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; | ||
| }; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") { | ||
| for (let key of __getOwnPropNames(from)) | ||
| if (!__hasOwnProp.call(to, key) && key !== except) | ||
| __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
| } | ||
| return to; | ||
| }; | ||
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( | ||
| // If the importer is in node compatibility mode or this is not an ESM | ||
| // file that has been converted to a CommonJS file using a Babel- | ||
| // compatible transform (i.e. "__esModule" has not been set), then set | ||
| // "default" to the CommonJS "module.exports" for node compatibility. | ||
| isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, | ||
| mod | ||
| )); | ||
| // node_modules/tsup/assets/esm_shims.js | ||
| import path from "path"; | ||
| import { fileURLToPath } from "url"; | ||
| var init_esm_shims = __esm({ | ||
| "node_modules/tsup/assets/esm_shims.js"() { | ||
| "use strict"; | ||
| } | ||
| }); | ||
| // src/client.ts | ||
| init_esm_shims(); | ||
| import axios from "axios"; | ||
| import http from "http"; | ||
| import https from "https"; | ||
| import fs from "fs"; | ||
| import path2 from "path"; | ||
| import os from "os"; | ||
| function isLocalApiUrl(apiUrl) { | ||
| try { | ||
| const host = new URL(apiUrl).hostname; | ||
| return host === "localhost" || host === "127.0.0.1" || host === "::1"; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| function getDeerdawnHome() { | ||
| return process.env.DEERDAWN_HOME || path2.join(os.homedir(), ".deerdawn"); | ||
| } | ||
| function getQueuePath() { | ||
| return path2.join(getDeerdawnHome(), "cloud-write-queue.json"); | ||
| } | ||
| function ensureDeerdawnHome() { | ||
| fs.mkdirSync(getDeerdawnHome(), { recursive: true }); | ||
| } | ||
| function readQueueState() { | ||
| try { | ||
| return JSON.parse(fs.readFileSync(getQueuePath(), "utf8")); | ||
| } catch { | ||
| return { entries: [] }; | ||
| } | ||
| } | ||
| function writeQueueState(state) { | ||
| ensureDeerdawnHome(); | ||
| fs.writeFileSync(getQueuePath(), JSON.stringify(state, null, 2)); | ||
| } | ||
| function mapOrgUsageToSettings(data) { | ||
| const plan = data?.plan || {}; | ||
| return { | ||
| org_id: plan.org_id || "", | ||
| name: data?.org_name || "", | ||
| plan: typeof plan.tier === "string" ? plan.tier : "free", | ||
| fail_mode: plan.fail_mode === "open" ? "open" : "closed", | ||
| redaction_mode: plan.redaction_mode === "permissive" ? "permissive" : "strict", | ||
| is_locked: !!plan.is_locked, | ||
| shadow_mode: !!plan.shadow_mode, | ||
| redaction_keys: Array.isArray(plan.redaction_keys) ? plan.redaction_keys : [], | ||
| created_at: (/* @__PURE__ */ new Date()).toISOString(), | ||
| usage: data?.usage, | ||
| limits: data?.limits | ||
| }; | ||
| } | ||
| var DeerdawnClient = class { | ||
| client; | ||
| cache; | ||
| config; | ||
| MAX_CACHE_SIZE = 100; | ||
| MAX_CACHE_ITEM_SIZE_MB = 5; | ||
| constructor(config) { | ||
| if (!config.apiKey || config.apiKey.trim() !== config.apiKey) { | ||
| throw new Error("Invalid API key: cannot be empty or contain whitespace"); | ||
| } | ||
| if (!isLocalApiUrl(config.apiUrl) && !config.apiKey.startsWith("dd_")) { | ||
| throw new Error("Invalid API key format: must start with dd_ prefix"); | ||
| } | ||
| this.config = config; | ||
| this.cache = /* @__PURE__ */ new Map(); | ||
| this.client = axios.create({ | ||
| baseURL: config.apiUrl, | ||
| headers: { | ||
| "x-api-key": config.apiKey, | ||
| "content-type": "application/json" | ||
| }, | ||
| timeout: 3e4, | ||
| // Reuse TCP+TLS connections across calls. Without this, axios opens a | ||
| // fresh socket per request and pays a full handshake (~50ms) every time — | ||
| // multiplied across the several sequential calls a single tool can make. | ||
| httpAgent: new http.Agent({ keepAlive: true, maxSockets: 16 }), | ||
| httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 16 }) | ||
| }); | ||
| this.client.interceptors.response.use( | ||
| (response) => response, | ||
| async (error) => { | ||
| const cfg = error.config; | ||
| const response = error.response; | ||
| if (!cfg) { | ||
| throw error; | ||
| } | ||
| if (!cfg.__retryCount) { | ||
| cfg.__retryCount = 0; | ||
| cfg.__firstRetryTimestamp = Date.now(); | ||
| } | ||
| const totalRetryTime = Date.now() - (cfg.__firstRetryTimestamp || Date.now()); | ||
| if (totalRetryTime > 6e4) { | ||
| throw new Error("Retry timeout exceeded (60s)"); | ||
| } | ||
| const shouldRetry = (response?.status === 429 || response?.status && response.status >= 500) && cfg.__retryCount < this.config.maxRetries; | ||
| if (shouldRetry) { | ||
| cfg.__retryCount++; | ||
| const delay = Math.min( | ||
| this.config.retryBackoffMs * Math.pow(2, cfg.__retryCount - 1), | ||
| 3e4 | ||
| ); | ||
| await new Promise((resolve) => setTimeout(resolve, delay)); | ||
| return this.client.request(cfg); | ||
| } | ||
| throw error; | ||
| } | ||
| ); | ||
| } | ||
| async recordProductEvent(event) { | ||
| const { data } = await this.client.post("/api/v1/product-events", event); | ||
| return data; | ||
| } | ||
| async recordContextEvent(event) { | ||
| try { | ||
| const { data } = await this.client.post("/api/v1/context/events", event); | ||
| return data; | ||
| } catch (error) { | ||
| const status = error?.response?.status; | ||
| const shouldQueue = !status || status >= 500 || error?.code === "ECONNREFUSED" || error?.code === "ENOTFOUND"; | ||
| if (!shouldQueue) throw error; | ||
| const state = readQueueState(); | ||
| state.entries.push({ | ||
| id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, | ||
| method: "POST", | ||
| path: "/api/v1/context/events", | ||
| body: event, | ||
| created_at: (/* @__PURE__ */ new Date()).toISOString() | ||
| }); | ||
| writeQueueState(state); | ||
| return { queued: true, queue_size: state.entries.length, event_type: event.event_type }; | ||
| } | ||
| } | ||
| async flushQueuedContextWrites() { | ||
| const state = readQueueState(); | ||
| if (!state.entries.length) return { flushed: 0, remaining: 0 }; | ||
| const remaining = []; | ||
| let flushed = 0; | ||
| for (const entry of state.entries) { | ||
| try { | ||
| await this.client.post(entry.path, entry.body); | ||
| flushed += 1; | ||
| } catch { | ||
| remaining.push(entry); | ||
| } | ||
| } | ||
| writeQueueState({ entries: remaining }); | ||
| return { flushed, remaining: remaining.length }; | ||
| } | ||
| async getOrganizationSettings() { | ||
| const { data } = await this.client.get("/api/v1/orgs/usage"); | ||
| return mapOrgUsageToSettings(data); | ||
| } | ||
| async getContextGraphProject(projectId) { | ||
| const { data } = await this.client.get(`/api/v1/context/graph/projects/${encodeURIComponent(projectId)}`); | ||
| return data.graph || data; | ||
| } | ||
| async getContextGraphSession(sessionId) { | ||
| const { data } = await this.client.get(`/api/v1/context/graph/sessions/${encodeURIComponent(sessionId)}`); | ||
| return data.graph || data; | ||
| } | ||
| async getContextGraphNode(nodeId) { | ||
| const { data } = await this.client.get(`/api/v1/context/graph/nodes/${encodeURIComponent(nodeId)}`); | ||
| return data.graph || data; | ||
| } | ||
| async queryContextGraph(input) { | ||
| const { data } = await this.client.post("/api/v1/context/graph/query", input); | ||
| return data.graph || data; | ||
| } | ||
| async updateOrganizationSettings(settings) { | ||
| const { data } = await this.client.patch("/api/v1/orgs/settings", settings); | ||
| return data; | ||
| } | ||
| async getAuditLogs(_params = {}) { | ||
| const { data } = await this.client.get("/api/v1/audit"); | ||
| const logs = data.logs || []; | ||
| return { logs, total: logs.length }; | ||
| } | ||
| async getActionRegistry() { | ||
| const cacheKey = "registry:actions"; | ||
| const cached = this.getFromCache(cacheKey); | ||
| if (cached) return cached; | ||
| try { | ||
| const { data } = await this.client.get("/api/v1/registry/actions"); | ||
| const actions = data.actions || data; | ||
| if (Array.isArray(actions) && actions.length > 0) { | ||
| this.setInCache(cacheKey, actions, 3e5); | ||
| return actions; | ||
| } | ||
| } catch { | ||
| } | ||
| return this.getDefaultActionRegistry(); | ||
| } | ||
| getDefaultActionRegistry() { | ||
| return [ | ||
| { | ||
| action_type: "auth.login", | ||
| category: "authentication", | ||
| description: "User login attempt", | ||
| risk_level: "medium" | ||
| }, | ||
| { | ||
| action_type: "auth.password_reset", | ||
| category: "authentication", | ||
| description: "Password reset request", | ||
| risk_level: "medium" | ||
| }, | ||
| { | ||
| action_type: "payment.process", | ||
| category: "financial", | ||
| description: "Payment processing", | ||
| risk_level: "high" | ||
| }, | ||
| { | ||
| action_type: "user.delete", | ||
| category: "data_management", | ||
| description: "User account deletion", | ||
| risk_level: "high" | ||
| }, | ||
| { | ||
| action_type: "data.export", | ||
| category: "data_management", | ||
| description: "Data export request", | ||
| risk_level: "medium" | ||
| } | ||
| ]; | ||
| } | ||
| getFromCache(key) { | ||
| if (!this.config.cacheEnabled) return null; | ||
| const entry = this.cache.get(key); | ||
| if (!entry) return null; | ||
| if (Date.now() > entry.expires) { | ||
| this.cache.delete(key); | ||
| return null; | ||
| } | ||
| return entry.data; | ||
| } | ||
| setInCache(key, data, ttlMs) { | ||
| if (!this.config.cacheEnabled) return; | ||
| const estimatedSize = JSON.stringify(data).length; | ||
| const estimatedSizeMB = estimatedSize / (1024 * 1024); | ||
| if (estimatedSizeMB > this.MAX_CACHE_ITEM_SIZE_MB) { | ||
| console.error(`[CACHE] Skipping large object (${estimatedSizeMB.toFixed(2)}MB)`); | ||
| return; | ||
| } | ||
| if (this.cache.size >= this.MAX_CACHE_SIZE) { | ||
| const firstKey = this.cache.keys().next().value; | ||
| if (firstKey) { | ||
| this.cache.delete(firstKey); | ||
| } | ||
| } | ||
| this.cache.set(key, { | ||
| data, | ||
| expires: Date.now() + ttlMs | ||
| }); | ||
| } | ||
| invalidateCache(prefix) { | ||
| for (const key of this.cache.keys()) { | ||
| if (key.startsWith(prefix)) { | ||
| this.cache.delete(key); | ||
| } | ||
| } | ||
| } | ||
| // Context sync methods | ||
| async getActiveContext(projectId, tool = "claude_code", verbosity = "standard") { | ||
| if (projectId) { | ||
| const { data: data2 } = await this.client.get( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/inject`, | ||
| { params: { tool, verbosity } } | ||
| ); | ||
| return { | ||
| brief: data2.payload?.body ?? null, | ||
| project_name: projectId, | ||
| project_id: projectId, | ||
| last_updated: null | ||
| }; | ||
| } | ||
| const { data } = await this.client.get("/api/v1/context/active", { params: { tool, verbosity } }); | ||
| return data; | ||
| } | ||
| // Brief shape (?shape=spine) — the product-voice 4-slot view (SHIPPED / DECIDED | ||
| // / OPEN / LANDMINE). Returns the 4-slot TEXT body plus the structured | ||
| // brief_spine (per-item trust). Opt-in: never changes the default brief text. | ||
| async getActiveBrief(projectId, tool = "claude_code", verbosity = "standard") { | ||
| if (projectId) { | ||
| const { data: data2 } = await this.client.get( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/inject`, | ||
| { params: { tool, verbosity, shape: "spine" } } | ||
| ); | ||
| return { | ||
| brief: data2.payload?.body ?? null, | ||
| brief_spine: data2.payload?.brief_spine ?? null, | ||
| project_name: projectId, | ||
| project_id: projectId, | ||
| last_updated: null | ||
| }; | ||
| } | ||
| const { data } = await this.client.get("/api/v1/context/active", { params: { tool, verbosity, shape: "spine" } }); | ||
| return data; | ||
| } | ||
| async getActiveContextByCwd(cwd, tool = "claude_code", verbosity = "standard", markSession = false, workspaceKey = null) { | ||
| const params = { cwd, tool, verbosity }; | ||
| if (markSession) params.mark_session = 1; | ||
| if (workspaceKey) params.workspace_key = workspaceKey; | ||
| const { data } = await this.client.get("/api/v1/context/active", { params }); | ||
| return data; | ||
| } | ||
| async pushContextUpdate(projectId, summary, tool = "claude_code", commands) { | ||
| const sessionId = `mcp-${Date.now()}`; | ||
| const [extractionResult] = await Promise.all([ | ||
| this.client.post("/api/v1/context/extraction", { | ||
| session_id: sessionId, | ||
| tool, | ||
| project_id: projectId, | ||
| chunks: [{ role: "assistant", text: summary }] | ||
| }).then((r) => r.data), | ||
| commands?.length ? this.client.patch(`/api/v1/context/${encodeURIComponent(projectId)}/context`, { commands }).catch((err) => { | ||
| process.stderr.write(`[DeerDawn] commands not saved: ${err?.message ?? "unknown"} | ||
| `); | ||
| return null; | ||
| }) : Promise.resolve(null) | ||
| ]); | ||
| return extractionResult; | ||
| } | ||
| async listContextProjects() { | ||
| const { data } = await this.client.get("/api/v1/context/projects"); | ||
| return data; | ||
| } | ||
| async listContextProjectTree() { | ||
| const { data } = await this.client.get("/api/v1/context/projects/tree"); | ||
| return data; | ||
| } | ||
| async createContextProject(projectName, description, parentProjectId) { | ||
| const { data } = await this.client.post("/api/v1/context/projects", { | ||
| project_name: projectName, | ||
| description, | ||
| ...parentProjectId ? { parent_project_id: parentProjectId } : {} | ||
| }); | ||
| return data; | ||
| } | ||
| async switchSubproject(projectId, workspaceKey) { | ||
| const { data } = await this.client.post("/api/v1/context/subproject/switch", { | ||
| project_id: projectId, | ||
| workspace_key: workspaceKey | ||
| }); | ||
| return data; | ||
| } | ||
| async archiveProject(projectId, unarchive = false) { | ||
| const { data } = await this.client.post( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/archive`, | ||
| { unarchive } | ||
| ); | ||
| return data; | ||
| } | ||
| async setProjectParent(projectId, parentProjectId) { | ||
| const { data } = await this.client.post( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/parent`, | ||
| { parent_project_id: parentProjectId ?? null } | ||
| ); | ||
| return data; | ||
| } | ||
| async renameProject(projectId, projectName) { | ||
| const { data } = await this.client.post( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/rename`, | ||
| { project_name: projectName } | ||
| ); | ||
| return data; | ||
| } | ||
| async getContextSections(projectId, sections, tool = "claude_code", verbosity = "standard") { | ||
| const sectionsParam = sections.join(","); | ||
| if (projectId) { | ||
| const { data: data2 } = await this.client.get( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/inject`, | ||
| { params: { tool, verbosity, sections: sectionsParam } } | ||
| ); | ||
| return { | ||
| brief: data2.payload?.body ?? null, | ||
| project_name: projectId, | ||
| project_id: projectId | ||
| }; | ||
| } | ||
| const { data } = await this.client.get("/api/v1/context/active", { params: { tool, verbosity, sections: sectionsParam } }); | ||
| return data; | ||
| } | ||
| async getContextMap(mcpAvailable = true, cwd, workspaceKey) { | ||
| const params = { mcp: mcpAvailable ? "true" : "false" }; | ||
| if (cwd) params.cwd = cwd; | ||
| if (workspaceKey) params.workspace_key = workspaceKey; | ||
| const { data } = await this.client.get("/api/v1/context/map", { params }); | ||
| return data; | ||
| } | ||
| async getContextJson(projectId) { | ||
| if (projectId) { | ||
| const { data: data2 } = await this.client.get( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/inject`, | ||
| { params: { format: "json" } } | ||
| ); | ||
| return data2.context ?? data2; | ||
| } | ||
| const { data } = await this.client.get("/api/v1/context/active", { params: { format: "json" } }); | ||
| return data; | ||
| } | ||
| async searchContext(query, limit = 5) { | ||
| const { data } = await this.client.post("/api/v1/context/search", { query, limit }); | ||
| return data; | ||
| } | ||
| async getWorkspaceOverview(params = {}) { | ||
| const query = {}; | ||
| if (params.mode) query.mode = params.mode; | ||
| if (params.verbosity) query.verbosity = params.verbosity; | ||
| if (params.include?.length) query.include = params.include.join(","); | ||
| if (params.format) query.format = params.format; | ||
| const { data } = await this.client.get("/api/v1/context/workspace/overview", { params: query }); | ||
| return data.overview || data; | ||
| } | ||
| async getWorkspaceEntities(params = {}) { | ||
| const query = {}; | ||
| if (params.types?.length) query.types = params.types.join(","); | ||
| if (params.query) query.query = params.query; | ||
| if (params.project_id) query.project_id = params.project_id; | ||
| if (params.related_project_id) query.related_project_id = params.related_project_id; | ||
| if (params.scope) query.scope = params.scope; | ||
| if (params.surface) query.surface = params.surface; | ||
| if (params.status) query.status = params.status; | ||
| if (typeof params.include_inferred === "boolean") query.include_inferred = params.include_inferred ? 1 : 0; | ||
| if (typeof params.include_resolved === "boolean") query.include_resolved = params.include_resolved ? 1 : 0; | ||
| if (typeof params.curated_only === "boolean") query.curated_only = params.curated_only ? 1 : 0; | ||
| if (typeof params.limit === "number") query.limit = params.limit; | ||
| if (params.format) query.format = params.format; | ||
| const { data } = await this.client.get("/api/v1/context/workspace/entities", { params: query }); | ||
| return data; | ||
| } | ||
| async getProjectMap(params = {}) { | ||
| const projectId = params.project_id; | ||
| if (!projectId) { | ||
| const { data: data2 } = await this.client.get("/api/v1/context/active"); | ||
| if (!data2?.project_id) return null; | ||
| params.project_id = data2.project_id; | ||
| } | ||
| const query = {}; | ||
| if (params.verbosity) query.verbosity = params.verbosity; | ||
| if (params.format) query.format = params.format; | ||
| const resolvedProjectId = String(params.project_id); | ||
| const { data } = await this.client.get(`/api/v1/context/projects/${encodeURIComponent(resolvedProjectId)}/map`, { params: query }); | ||
| return data.project_map || data; | ||
| } | ||
| async findKnownPaths(params) { | ||
| const query = { query: params.query }; | ||
| if (params.project_id) query.project_id = params.project_id; | ||
| if (typeof params.limit === "number") query.limit = params.limit; | ||
| if (params.format) query.format = params.format; | ||
| const { data } = await this.client.get("/api/v1/context/workspace/known-paths", { params: query }); | ||
| return data; | ||
| } | ||
| async getNextBestContext(params = {}) { | ||
| const query = {}; | ||
| if (params.tool) query.tool = params.tool; | ||
| if (params.cwd) query.cwd = params.cwd; | ||
| if (params.project_id) query.project_id = params.project_id; | ||
| if (typeof params.limit === "number") query.limit = params.limit; | ||
| const { data } = await this.client.get("/api/v1/context/workspace/next", { params: query }); | ||
| return data.result || data; | ||
| } | ||
| async upsertWorkspaceEntities(input) { | ||
| const { data } = await this.client.post("/api/v1/context/workspace/entities", input); | ||
| return data; | ||
| } | ||
| async recordHotPaths(projectId, items) { | ||
| const { data } = await this.client.post(`/api/v1/context/projects/${encodeURIComponent(projectId)}/hot-paths`, { items }); | ||
| return data; | ||
| } | ||
| async recordDebugFinding(projectId, input) { | ||
| const { data } = await this.client.post(`/api/v1/context/projects/${encodeURIComponent(projectId)}/debug-findings`, input); | ||
| return data; | ||
| } | ||
| async confirmClaim(projectId, input) { | ||
| const { data } = await this.client.post(`/api/v1/context/projects/${encodeURIComponent(projectId)}/verify`, input); | ||
| return data; | ||
| } | ||
| async updateGlobalContext(patch) { | ||
| const { data } = await this.client.patch("/api/v1/context/global", patch); | ||
| return data; | ||
| } | ||
| async patchProjectContext(projectId, delta) { | ||
| const { data } = await this.client.patch(`/api/v1/context/${encodeURIComponent(projectId)}/context`, delta); | ||
| return data; | ||
| } | ||
| // ── Shared skills ────────────────────────────────────────────────────────── | ||
| async listSkills(projectId) { | ||
| const params = {}; | ||
| if (projectId) params.project_id = projectId; | ||
| const { data } = await this.client.get("/api/v1/context/skills", { params }); | ||
| return data?.skills ?? []; | ||
| } | ||
| async getSkill(idOrSlug) { | ||
| const { data } = await this.client.get(`/api/v1/context/skills/${encodeURIComponent(idOrSlug)}`); | ||
| return data?.skill ?? null; | ||
| } | ||
| async upsertSkill(input) { | ||
| const { data } = await this.client.post("/api/v1/context/skills", input); | ||
| return data?.skill ?? data; | ||
| } | ||
| // Import a shared skill by pasting a GitHub repo URL. The API fetches the | ||
| // repo's SKILL.md, parses its frontmatter, and creates the shared skill. | ||
| async importSkillFromGithub(repoUrl, projectId) { | ||
| const body = { repo_url: repoUrl }; | ||
| if (projectId) body.project_id = projectId; | ||
| const { data } = await this.client.post("/api/v1/context/skills/import-github", body); | ||
| return data; | ||
| } | ||
| }; | ||
| export { | ||
| __require, | ||
| __commonJS, | ||
| __toESM, | ||
| init_esm_shims, | ||
| DeerdawnClient | ||
| }; |
| #!/usr/bin/env node | ||
| import { createRequire } from "module"; | ||
| const require = createRequire(import.meta.url); | ||
| import { | ||
| DeerdawnClient | ||
| } from "./chunk-LRWQ2CHH.js"; | ||
| export { | ||
| DeerdawnClient | ||
| }; |
+10
-1
| { | ||
| "name": "@deerdawn/mcp-server", | ||
| "mcpName": "com.deerdawn/deerdawn", | ||
| "version": "1.0.36", | ||
| "version": "1.0.37", | ||
| "description": "MCP server for DeerDawn — AI session memory that briefs every new AI session", | ||
| "homepage": "https://deerdawn.com", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/HissingSpider/DeerDawn.git", | ||
| "directory": "packages/mcp-server" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://deerdawn.com/contact" | ||
| }, | ||
| "type": "module", | ||
@@ -7,0 +16,0 @@ "bin": { |
+77
-61
@@ -5,26 +5,51 @@ # DeerDawn MCP Server | ||
| DeerDawn is AI session memory — your AI's chief of staff — for Claude Code, Claude Desktop, Cursor, Windsurf, VS Code, Zed, Codex, and any other MCP-compatible agent. Start a session anywhere and `start_session` hands the agent the brief: what you were working on, what was decided, and what's blocking — so no session starts cold. | ||
| DeerDawn is AI session memory — your AI's chief of staff — for Claude Code, Claude.ai, Claude Desktop, ChatGPT, Cursor, Windsurf, VS Code, Zed, Codex, and any other MCP-compatible agent. Start a session anywhere and `start_session` hands the agent the brief: what you were working on, what was decided, and what's blocking — so no session starts cold. | ||
| ## Quick start | ||
| ## Quick start — hosted server (recommended, no install) | ||
| **Step 1 — Get your API key** | ||
| Connect to the hosted MCP endpoint. **No API key needed** — you sign in with your browser the first time a DeerDawn tool is called. | ||
| Sign in at [app.deerdawn.com](https://app.deerdawn.com) → Settings → API Keys → Create Key. | ||
| Your key will start with `dd_`. | ||
| **Claude Code** | ||
| **Step 2 — Add to your tool** | ||
| ```bash | ||
| claude mcp add -s user --transport http deerdawn https://api.deerdawn.com/api/v1/mcp | ||
| ``` | ||
| Pick your editor below and paste in the config. Restart the tool. | ||
| **Every other tool** — add a remote MCP server (sometimes called a connector) with this URL: | ||
| **Step 3 — Call `start_session`** | ||
| ``` | ||
| https://api.deerdawn.com/api/v1/mcp | ||
| ``` | ||
| At the beginning of any session, call `start_session`. You'll get the active project context and a map of everything else in one shot. | ||
| | Tool | Where | | ||
| |------|-------| | ||
| | Claude.ai | Settings → Connectors → Add custom connector | | ||
| | ChatGPT.com | Settings → Connectors → Add MCP server (choose OAuth) | | ||
| | Cursor | Settings → MCP → Add server, or add a `"url"` entry in `.cursor/mcp.json` | | ||
| | VS Code / Copilot | Command palette → MCP: Add Server → HTTP → paste the URL | | ||
| | Windsurf, Gemini, others | Add a custom remote / HTTP MCP server with the URL | | ||
| Leave any OAuth client fields blank. Click Connect (or restart the tool) and sign in when the DeerDawn tab opens. If your tool connects without prompting for sign-in, the first DeerDawn tool call returns the sign-in URL — open it, sign in once, and retry the call; setup completes automatically. | ||
| Then say: **"Start a DeerDawn session."** | ||
| The hosted server exposes 18 core session-memory tools. For repo-file writes, automatic capture hooks, and the full 35-tool set, use the local install below. | ||
| --- | ||
| ## Configuration | ||
| ## Local install (stdio) — optional upgrade | ||
| Run the server on your machine via `npx -y @deerdawn/mcp-server@latest` when you want deeper integration: | ||
| - Writes `.deerdawn-context.md` / `.cursorrules` into your repo so context loads with no tool call. | ||
| - Installs SessionStart / Stop hooks that capture and flush context automatically. | ||
| - Exposes the full set of 35 tools (the hosted server exposes 18 core ones). | ||
| **Sign-in works the same way** — browser OAuth by default, no API key required. On first run without credentials, the server starts a device-flow sign-in: the URL is printed to stderr, saved to `~/.deerdawn/pending-auth.json`, and returned in-band by the `get_auth_status` tool (use that in GUI hosts where stderr isn't visible). After you approve in the browser, credentials are saved to `~/.deerdawn/credentials.json` and every future session is authenticated. | ||
| **Prefer non-interactive auth** (CI, headless, SSH)? Create a key at [app.deerdawn.com](https://app.deerdawn.com) → Settings → API Keys (it starts with `dd_`) and set `DEERDAWN_API_KEY` in the config's `env` block — browser sign-in is then skipped entirely. | ||
| ### Claude Code (CLI) | ||
| Run the setup command — it registers the server and handles sign-in in one step: | ||
| Run the setup command — it registers the server, signs you in, and configures hooks in one step: | ||
@@ -38,16 +63,7 @@ ```bash | ||
| ```bash | ||
| claude mcp add -s user deerdawn -e DEERDAWN_API_URL=https://api.deerdawn.com -- npx -y @deerdawn/mcp-server@latest | ||
| claude mcp add -s user deerdawn -e DEERDAWN_API_URL=https://api.deerdawn.com -e DEERDAWN_SURFACE_ID=claude_code -- npx -y @deerdawn/mcp-server@latest | ||
| ``` | ||
| If you already have an API key, pass it directly: | ||
| Restart Claude Code after registering. If you haven't signed in yet, call `get_auth_status` — it returns the browser URL to complete sign-in. | ||
| ```bash | ||
| claude mcp add -s user deerdawn \ | ||
| -e DEERDAWN_API_URL=https://api.deerdawn.com \ | ||
| -e DEERDAWN_API_KEY=dd_your_key_here \ | ||
| -- npx -y @deerdawn/mcp-server@latest | ||
| ``` | ||
| Restart Claude Code after registering. On first start, call `get_auth_status` if you haven't signed in yet — it returns a browser URL to complete sign-in. | ||
| --- | ||
@@ -57,3 +73,3 @@ | ||
| **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` | ||
| **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` | ||
| **Windows**: `%APPDATA%\Claude\claude_desktop_config.json` | ||
@@ -68,3 +84,3 @@ | ||
| "env": { | ||
| "DEERDAWN_API_KEY": "dd_your_key_here" | ||
| "DEERDAWN_API_URL": "https://api.deerdawn.com" | ||
| } | ||
@@ -80,4 +96,3 @@ } | ||
| **macOS/Linux**: `~/.cursor/mcp.json` | ||
| **Windows**: `%APPDATA%\Cursor\mcp.json` | ||
| `~/.cursor/mcp.json` (same path on macOS, Linux, and Windows): | ||
@@ -91,3 +106,3 @@ ```json | ||
| "env": { | ||
| "DEERDAWN_API_KEY": "dd_your_key_here" | ||
| "DEERDAWN_API_URL": "https://api.deerdawn.com" | ||
| } | ||
@@ -103,3 +118,3 @@ } | ||
| **macOS**: `~/.codeium/windsurf/mcp_config.json` | ||
| **macOS**: `~/.codeium/windsurf/mcp_config.json` | ||
| **Windows**: `%APPDATA%\Codeium\windsurf\mcp_config.json` | ||
@@ -114,3 +129,3 @@ | ||
| "env": { | ||
| "DEERDAWN_API_KEY": "dd_your_key_here" | ||
| "DEERDAWN_API_URL": "https://api.deerdawn.com" | ||
| } | ||
@@ -136,3 +151,3 @@ } | ||
| "env": { | ||
| "DEERDAWN_API_KEY": "dd_your_key_here" | ||
| "DEERDAWN_API_URL": "https://api.deerdawn.com" | ||
| } | ||
@@ -155,3 +170,3 @@ } | ||
| "env": { | ||
| "DEERDAWN_API_KEY": "dd_your_key_here" | ||
| "DEERDAWN_API_URL": "https://api.deerdawn.com" | ||
| } | ||
@@ -177,3 +192,3 @@ } | ||
| "env": { | ||
| "DEERDAWN_API_KEY": "dd_your_key_here" | ||
| "DEERDAWN_API_URL": "https://api.deerdawn.com" | ||
| } | ||
@@ -190,12 +205,20 @@ } | ||
| Open **Settings → MCP Servers → Add Server**: | ||
| Add to `~/.codex/config.toml`: | ||
| - **Command**: `npx` | ||
| - **Args**: `-y @deerdawn/mcp-server@latest` | ||
| - **Env**: `DEERDAWN_API_KEY` = your key from [app.deerdawn.com](https://app.deerdawn.com) → Settings → API Keys | ||
| ```toml | ||
| [mcp_servers.deerdawn] | ||
| command = "npx" | ||
| args = ["-y", "@deerdawn/mcp-server@latest"] | ||
| startup_timeout_sec = 120 | ||
| **No API key yet?** Add the server without `DEERDAWN_API_KEY` first, then call `get_auth_status` — it returns a browser sign-in URL. After signing in, your key is saved automatically. | ||
| [mcp_servers.deerdawn.env] | ||
| DEERDAWN_API_URL = "https://api.deerdawn.com" | ||
| ``` | ||
| --- | ||
| To skip browser sign-in in any of these configs, add `"DEERDAWN_API_KEY": "dd_your_key_here"` to the `env` block (Codex TOML: `DEERDAWN_API_KEY = "dd_your_key_here"` under `[mcp_servers.deerdawn.env]`). | ||
| --- | ||
| ## Host naming note | ||
@@ -210,10 +233,10 @@ | ||
| All optional. With no env vars at all, the server starts and walks you through browser sign-in. | ||
| | Variable | Required | Default | Description | | ||
| |----------|----------|---------|-------------| | ||
| | `DEERDAWN_API_KEY` | Yes* | — | Your API key (`dd_...`). Get one at app.deerdawn.com → Settings → API Keys. | | ||
| | `DEERDAWN_API_URL` | No | `https://api.deerdawn.com` | API base URL. Only change for self-hosted or staging. | | ||
| | `DEERDAWN_ENVIRONMENT` | No | `production` | `sandbox` or `production` | | ||
| | `DEERDAWN_SURFACE_ID` | No | auto-detected | Which tool this server runs in (`claude_code`, `cursor`, `codex`). Detected from the MCP client when unset; set it explicitly to pin session attribution. | | ||
| | `DEERDAWN_API_KEY` | No | — | API key (`dd_...`) for non-interactive auth (CI/headless). Default is browser OAuth sign-in. Get one at app.deerdawn.com → Settings → API Keys. | | ||
| *Required unless using the browser device flow (see "No API key?" above). | ||
| --- | ||
@@ -223,3 +246,3 @@ | ||
| These are the core tools. Call them in every session. | ||
| The local server exposes 35 tools (the hosted remote exposes 18 core ones). These are the ones to reach for in every session. | ||
@@ -336,3 +359,3 @@ ### `start_session` | ||
| If it returns your active project context, DeerDawn is already connected — you're done, skip the rest. (If you connected through the claude.ai connector rather than this standalone server, this is the normal path: `apply_setup` and `import_local_context` won't exist on that build and aren't needed — context loads automatically.) | ||
| If it returns your active project context, DeerDawn is already connected — you're done, skip the rest. (If you connected through the hosted remote / claude.ai connector rather than the local server, this is the normal path: `apply_setup` and `import_local_context` won't exist on that build and aren't needed — context loads automatically.) | ||
@@ -355,3 +378,3 @@ If `start_session` reports you're not authenticated, complete sign-in and seed context: | ||
| - Confirm `DEERDAWN_API_KEY` starts with `dd_` | ||
| - Call `get_auth_status` and finish browser sign-in via the returned URL (if you set `DEERDAWN_API_KEY` instead, confirm it starts with `dd_`) | ||
| - Fully restart the agent (not just reload) | ||
@@ -364,6 +387,6 @@ - Check MCP logs: `~/Library/Logs/Claude/mcp*.log` (Claude Desktop) or equivalent | ||
| **Server won't start** | ||
| 1. Verify `DEERDAWN_API_KEY` starts with `dd_` | ||
| 2. Fully restart your editor after saving the config | ||
| 3. Check MCP logs: | ||
| **DeerDawn tools don't appear in your agent** | ||
| The server itself starts with zero env vars — a missing API key never prevents startup. If no DeerDawn tools show up: | ||
| 1. Fully restart your editor after saving the config (not just reload) | ||
| 2. Check MCP logs: | ||
| - Claude Desktop: `~/Library/Logs/Claude/mcp*.log` | ||
@@ -375,8 +398,11 @@ - Cursor: View → Output → MCP | ||
| **Authentication error / not signed in** | ||
| Call `get_auth_status` — it returns a browser sign-in URL. Open it, approve, then retry the tool call; setup completes automatically. If you set `DEERDAWN_API_KEY`, it must start with `dd_`. | ||
| **Sign-in reports a rate limit (HTTP 429)** | ||
| This happens after several quick restarts during setup. Wait a minute before retrying — re-running setup immediately will keep hitting the limit. | ||
| **`start_session` returns no projects** | ||
| Call `import_local_context` (without `confirmed: true` first) to seed context from a `CLAUDE.md` or `AGENTS.md` in your current directory. | ||
| **Authentication error** | ||
| API key must start with `dd_`. If you're using the device flow, call `get_auth_status` and open the returned URL in your browser. | ||
| **Slow responses** | ||
@@ -387,12 +413,2 @@ The extraction pipeline runs on first import. Subsequent `start_session` calls are fast (cached context lookup, no LLM call unless something changed). | ||
| ## Governance tools (separate sub-product) | ||
| DeerDawn also includes a policy decision engine for AI agent guardrails. These tools are available in the same server but serve a different use case: | ||
| `evaluate_decision`, `get_decision`, `list_decisions`, `get_decision_stats`, `list_policies`, `create_policy`, `update_policy`, `delete_policy`, `get_policy_versions`, `create_policy_from_template`, `list_escalations`, `get_escalation`, `resolve_escalation`, `get_organization_settings`, `update_organization_settings` | ||
| See [governance.deerdawn.com](https://governance.deerdawn.com) for documentation on the policy engine. | ||
| --- | ||
| ## Development | ||
@@ -399,0 +415,0 @@ |
| #!/usr/bin/env node | ||
| import { createRequire } from "module"; | ||
| const require = createRequire(import.meta.url); | ||
| var __create = Object.create; | ||
| var __defProp = Object.defineProperty; | ||
| var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
| var __getOwnPropNames = Object.getOwnPropertyNames; | ||
| var __getProtoOf = Object.getPrototypeOf; | ||
| var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
| var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { | ||
| get: (a, b) => (typeof require !== "undefined" ? require : a)[b] | ||
| }) : x)(function(x) { | ||
| if (typeof require !== "undefined") return require.apply(this, arguments); | ||
| throw Error('Dynamic require of "' + x + '" is not supported'); | ||
| }); | ||
| var __esm = (fn, res) => function __init() { | ||
| return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; | ||
| }; | ||
| var __commonJS = (cb, mod) => function __require2() { | ||
| return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; | ||
| }; | ||
| var __copyProps = (to, from, except, desc) => { | ||
| if (from && typeof from === "object" || typeof from === "function") { | ||
| for (let key of __getOwnPropNames(from)) | ||
| if (!__hasOwnProp.call(to, key) && key !== except) | ||
| __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); | ||
| } | ||
| return to; | ||
| }; | ||
| var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( | ||
| // If the importer is in node compatibility mode or this is not an ESM | ||
| // file that has been converted to a CommonJS file using a Babel- | ||
| // compatible transform (i.e. "__esModule" has not been set), then set | ||
| // "default" to the CommonJS "module.exports" for node compatibility. | ||
| isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, | ||
| mod | ||
| )); | ||
| // node_modules/tsup/assets/esm_shims.js | ||
| import path from "path"; | ||
| import { fileURLToPath } from "url"; | ||
| var init_esm_shims = __esm({ | ||
| "node_modules/tsup/assets/esm_shims.js"() { | ||
| "use strict"; | ||
| } | ||
| }); | ||
| // src/client.ts | ||
| init_esm_shims(); | ||
| import axios from "axios"; | ||
| import fs from "fs"; | ||
| import path2 from "path"; | ||
| import os from "os"; | ||
| function isLocalApiUrl(apiUrl) { | ||
| try { | ||
| const host = new URL(apiUrl).hostname; | ||
| return host === "localhost" || host === "127.0.0.1" || host === "::1"; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| function getDeerdawnHome() { | ||
| return process.env.DEERDAWN_HOME || path2.join(os.homedir(), ".deerdawn"); | ||
| } | ||
| function getQueuePath() { | ||
| return path2.join(getDeerdawnHome(), "cloud-write-queue.json"); | ||
| } | ||
| function ensureDeerdawnHome() { | ||
| fs.mkdirSync(getDeerdawnHome(), { recursive: true }); | ||
| } | ||
| function readQueueState() { | ||
| try { | ||
| return JSON.parse(fs.readFileSync(getQueuePath(), "utf8")); | ||
| } catch { | ||
| return { entries: [] }; | ||
| } | ||
| } | ||
| function writeQueueState(state) { | ||
| ensureDeerdawnHome(); | ||
| fs.writeFileSync(getQueuePath(), JSON.stringify(state, null, 2)); | ||
| } | ||
| function mapOrgUsageToSettings(data) { | ||
| const plan = data?.plan || {}; | ||
| return { | ||
| org_id: plan.org_id || "", | ||
| name: data?.org_name || "", | ||
| plan: typeof plan.tier === "string" ? plan.tier : "free", | ||
| fail_mode: plan.fail_mode === "open" ? "open" : "closed", | ||
| redaction_mode: plan.redaction_mode === "permissive" ? "permissive" : "strict", | ||
| is_locked: !!plan.is_locked, | ||
| shadow_mode: !!plan.shadow_mode, | ||
| redaction_keys: Array.isArray(plan.redaction_keys) ? plan.redaction_keys : [], | ||
| created_at: (/* @__PURE__ */ new Date()).toISOString(), | ||
| usage: data?.usage, | ||
| limits: data?.limits | ||
| }; | ||
| } | ||
| var DeerdawnClient = class { | ||
| client; | ||
| cache; | ||
| config; | ||
| MAX_CACHE_SIZE = 100; | ||
| MAX_CACHE_ITEM_SIZE_MB = 5; | ||
| constructor(config) { | ||
| if (!config.apiKey || config.apiKey.trim() !== config.apiKey) { | ||
| throw new Error("Invalid API key: cannot be empty or contain whitespace"); | ||
| } | ||
| if (!isLocalApiUrl(config.apiUrl) && !config.apiKey.startsWith("dd_")) { | ||
| throw new Error("Invalid API key format: must start with dd_ prefix"); | ||
| } | ||
| this.config = config; | ||
| this.cache = /* @__PURE__ */ new Map(); | ||
| this.client = axios.create({ | ||
| baseURL: config.apiUrl, | ||
| headers: { | ||
| "x-api-key": config.apiKey, | ||
| "content-type": "application/json" | ||
| }, | ||
| timeout: 3e4 | ||
| }); | ||
| this.client.interceptors.response.use( | ||
| (response) => response, | ||
| async (error) => { | ||
| const cfg = error.config; | ||
| const response = error.response; | ||
| if (!cfg) { | ||
| throw error; | ||
| } | ||
| if (!cfg.__retryCount) { | ||
| cfg.__retryCount = 0; | ||
| cfg.__firstRetryTimestamp = Date.now(); | ||
| } | ||
| const totalRetryTime = Date.now() - (cfg.__firstRetryTimestamp || Date.now()); | ||
| if (totalRetryTime > 6e4) { | ||
| throw new Error("Retry timeout exceeded (60s)"); | ||
| } | ||
| const shouldRetry = (response?.status === 429 || response?.status && response.status >= 500) && cfg.__retryCount < this.config.maxRetries; | ||
| if (shouldRetry) { | ||
| cfg.__retryCount++; | ||
| const delay = Math.min( | ||
| this.config.retryBackoffMs * Math.pow(2, cfg.__retryCount - 1), | ||
| 3e4 | ||
| ); | ||
| await new Promise((resolve) => setTimeout(resolve, delay)); | ||
| return this.client.request(cfg); | ||
| } | ||
| throw error; | ||
| } | ||
| ); | ||
| } | ||
| async recordProductEvent(event) { | ||
| const { data } = await this.client.post("/api/v1/product-events", event); | ||
| return data; | ||
| } | ||
| async recordContextEvent(event) { | ||
| try { | ||
| const { data } = await this.client.post("/api/v1/context/events", event); | ||
| return data; | ||
| } catch (error) { | ||
| const status = error?.response?.status; | ||
| const shouldQueue = !status || status >= 500 || error?.code === "ECONNREFUSED" || error?.code === "ENOTFOUND"; | ||
| if (!shouldQueue) throw error; | ||
| const state = readQueueState(); | ||
| state.entries.push({ | ||
| id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, | ||
| method: "POST", | ||
| path: "/api/v1/context/events", | ||
| body: event, | ||
| created_at: (/* @__PURE__ */ new Date()).toISOString() | ||
| }); | ||
| writeQueueState(state); | ||
| return { queued: true, queue_size: state.entries.length, event_type: event.event_type }; | ||
| } | ||
| } | ||
| async flushQueuedContextWrites() { | ||
| const state = readQueueState(); | ||
| if (!state.entries.length) return { flushed: 0, remaining: 0 }; | ||
| const remaining = []; | ||
| let flushed = 0; | ||
| for (const entry of state.entries) { | ||
| try { | ||
| await this.client.post(entry.path, entry.body); | ||
| flushed += 1; | ||
| } catch { | ||
| remaining.push(entry); | ||
| } | ||
| } | ||
| writeQueueState({ entries: remaining }); | ||
| return { flushed, remaining: remaining.length }; | ||
| } | ||
| async getOrganizationSettings() { | ||
| const { data } = await this.client.get("/api/v1/orgs/usage"); | ||
| return mapOrgUsageToSettings(data); | ||
| } | ||
| async getContextGraphProject(projectId) { | ||
| const { data } = await this.client.get(`/api/v1/context/graph/projects/${encodeURIComponent(projectId)}`); | ||
| return data.graph || data; | ||
| } | ||
| async getContextGraphSession(sessionId) { | ||
| const { data } = await this.client.get(`/api/v1/context/graph/sessions/${encodeURIComponent(sessionId)}`); | ||
| return data.graph || data; | ||
| } | ||
| async getContextGraphNode(nodeId) { | ||
| const { data } = await this.client.get(`/api/v1/context/graph/nodes/${encodeURIComponent(nodeId)}`); | ||
| return data.graph || data; | ||
| } | ||
| async queryContextGraph(input) { | ||
| const { data } = await this.client.post("/api/v1/context/graph/query", input); | ||
| return data.graph || data; | ||
| } | ||
| async updateOrganizationSettings(settings) { | ||
| const { data } = await this.client.patch("/api/v1/orgs/settings", settings); | ||
| return data; | ||
| } | ||
| async getAuditLogs(_params = {}) { | ||
| const { data } = await this.client.get("/api/v1/audit"); | ||
| const logs = data.logs || []; | ||
| return { logs, total: logs.length }; | ||
| } | ||
| async getActionRegistry() { | ||
| const cacheKey = "registry:actions"; | ||
| const cached = this.getFromCache(cacheKey); | ||
| if (cached) return cached; | ||
| try { | ||
| const { data } = await this.client.get("/api/v1/registry/actions"); | ||
| const actions = data.actions || data; | ||
| if (Array.isArray(actions) && actions.length > 0) { | ||
| this.setInCache(cacheKey, actions, 3e5); | ||
| return actions; | ||
| } | ||
| } catch { | ||
| } | ||
| return this.getDefaultActionRegistry(); | ||
| } | ||
| getDefaultActionRegistry() { | ||
| return [ | ||
| { | ||
| action_type: "auth.login", | ||
| category: "authentication", | ||
| description: "User login attempt", | ||
| risk_level: "medium" | ||
| }, | ||
| { | ||
| action_type: "auth.password_reset", | ||
| category: "authentication", | ||
| description: "Password reset request", | ||
| risk_level: "medium" | ||
| }, | ||
| { | ||
| action_type: "payment.process", | ||
| category: "financial", | ||
| description: "Payment processing", | ||
| risk_level: "high" | ||
| }, | ||
| { | ||
| action_type: "user.delete", | ||
| category: "data_management", | ||
| description: "User account deletion", | ||
| risk_level: "high" | ||
| }, | ||
| { | ||
| action_type: "data.export", | ||
| category: "data_management", | ||
| description: "Data export request", | ||
| risk_level: "medium" | ||
| } | ||
| ]; | ||
| } | ||
| getFromCache(key) { | ||
| if (!this.config.cacheEnabled) return null; | ||
| const entry = this.cache.get(key); | ||
| if (!entry) return null; | ||
| if (Date.now() > entry.expires) { | ||
| this.cache.delete(key); | ||
| return null; | ||
| } | ||
| return entry.data; | ||
| } | ||
| setInCache(key, data, ttlMs) { | ||
| if (!this.config.cacheEnabled) return; | ||
| const estimatedSize = JSON.stringify(data).length; | ||
| const estimatedSizeMB = estimatedSize / (1024 * 1024); | ||
| if (estimatedSizeMB > this.MAX_CACHE_ITEM_SIZE_MB) { | ||
| console.error(`[CACHE] Skipping large object (${estimatedSizeMB.toFixed(2)}MB)`); | ||
| return; | ||
| } | ||
| if (this.cache.size >= this.MAX_CACHE_SIZE) { | ||
| const firstKey = this.cache.keys().next().value; | ||
| if (firstKey) { | ||
| this.cache.delete(firstKey); | ||
| } | ||
| } | ||
| this.cache.set(key, { | ||
| data, | ||
| expires: Date.now() + ttlMs | ||
| }); | ||
| } | ||
| invalidateCache(prefix) { | ||
| for (const key of this.cache.keys()) { | ||
| if (key.startsWith(prefix)) { | ||
| this.cache.delete(key); | ||
| } | ||
| } | ||
| } | ||
| // Context sync methods | ||
| async getActiveContext(projectId, tool = "claude_code", verbosity = "standard") { | ||
| if (projectId) { | ||
| const { data: data2 } = await this.client.get( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/inject`, | ||
| { params: { tool, verbosity } } | ||
| ); | ||
| return { | ||
| brief: data2.payload?.body ?? null, | ||
| project_name: projectId, | ||
| project_id: projectId, | ||
| last_updated: null | ||
| }; | ||
| } | ||
| const { data } = await this.client.get("/api/v1/context/active", { params: { tool, verbosity } }); | ||
| return data; | ||
| } | ||
| // Brief shape (?shape=spine) — the product-voice 4-slot view (SHIPPED / DECIDED | ||
| // / OPEN / LANDMINE). Returns the 4-slot TEXT body plus the structured | ||
| // brief_spine (per-item trust). Opt-in: never changes the default brief text. | ||
| async getActiveBrief(projectId, tool = "claude_code", verbosity = "standard") { | ||
| if (projectId) { | ||
| const { data: data2 } = await this.client.get( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/inject`, | ||
| { params: { tool, verbosity, shape: "spine" } } | ||
| ); | ||
| return { | ||
| brief: data2.payload?.body ?? null, | ||
| brief_spine: data2.payload?.brief_spine ?? null, | ||
| project_name: projectId, | ||
| project_id: projectId, | ||
| last_updated: null | ||
| }; | ||
| } | ||
| const { data } = await this.client.get("/api/v1/context/active", { params: { tool, verbosity, shape: "spine" } }); | ||
| return data; | ||
| } | ||
| async getActiveContextByCwd(cwd, tool = "claude_code", verbosity = "standard", markSession = false, workspaceKey = null) { | ||
| const params = { cwd, tool, verbosity }; | ||
| if (markSession) params.mark_session = 1; | ||
| if (workspaceKey) params.workspace_key = workspaceKey; | ||
| const { data } = await this.client.get("/api/v1/context/active", { params }); | ||
| return data; | ||
| } | ||
| async pushContextUpdate(projectId, summary, tool = "claude_code", commands) { | ||
| const sessionId = `mcp-${Date.now()}`; | ||
| const [extractionResult] = await Promise.all([ | ||
| this.client.post("/api/v1/context/extraction", { | ||
| session_id: sessionId, | ||
| tool, | ||
| project_id: projectId, | ||
| chunks: [{ role: "assistant", text: summary }] | ||
| }).then((r) => r.data), | ||
| commands?.length ? this.client.patch(`/api/v1/context/${encodeURIComponent(projectId)}/context`, { commands }).catch((err) => { | ||
| process.stderr.write(`[DeerDawn] commands not saved: ${err?.message ?? "unknown"} | ||
| `); | ||
| return null; | ||
| }) : Promise.resolve(null) | ||
| ]); | ||
| return extractionResult; | ||
| } | ||
| async listContextProjects() { | ||
| const { data } = await this.client.get("/api/v1/context/projects"); | ||
| return data; | ||
| } | ||
| async listContextProjectTree() { | ||
| const { data } = await this.client.get("/api/v1/context/projects/tree"); | ||
| return data; | ||
| } | ||
| async createContextProject(projectName, description, parentProjectId) { | ||
| const { data } = await this.client.post("/api/v1/context/projects", { | ||
| project_name: projectName, | ||
| description, | ||
| ...parentProjectId ? { parent_project_id: parentProjectId } : {} | ||
| }); | ||
| return data; | ||
| } | ||
| async switchSubproject(projectId, workspaceKey) { | ||
| const { data } = await this.client.post("/api/v1/context/subproject/switch", { | ||
| project_id: projectId, | ||
| workspace_key: workspaceKey | ||
| }); | ||
| return data; | ||
| } | ||
| async archiveProject(projectId, unarchive = false) { | ||
| const { data } = await this.client.post( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/archive`, | ||
| { unarchive } | ||
| ); | ||
| return data; | ||
| } | ||
| async setProjectParent(projectId, parentProjectId) { | ||
| const { data } = await this.client.post( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/parent`, | ||
| { parent_project_id: parentProjectId ?? null } | ||
| ); | ||
| return data; | ||
| } | ||
| async renameProject(projectId, projectName) { | ||
| const { data } = await this.client.post( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/rename`, | ||
| { project_name: projectName } | ||
| ); | ||
| return data; | ||
| } | ||
| async getContextSections(projectId, sections, tool = "claude_code", verbosity = "standard") { | ||
| const sectionsParam = sections.join(","); | ||
| if (projectId) { | ||
| const { data: data2 } = await this.client.get( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/inject`, | ||
| { params: { tool, verbosity, sections: sectionsParam } } | ||
| ); | ||
| return { | ||
| brief: data2.payload?.body ?? null, | ||
| project_name: projectId, | ||
| project_id: projectId | ||
| }; | ||
| } | ||
| const { data } = await this.client.get("/api/v1/context/active", { params: { tool, verbosity, sections: sectionsParam } }); | ||
| return data; | ||
| } | ||
| async getContextMap(mcpAvailable = true, cwd, workspaceKey) { | ||
| const params = { mcp: mcpAvailable ? "true" : "false" }; | ||
| if (cwd) params.cwd = cwd; | ||
| if (workspaceKey) params.workspace_key = workspaceKey; | ||
| const { data } = await this.client.get("/api/v1/context/map", { params }); | ||
| return data; | ||
| } | ||
| async getContextJson(projectId) { | ||
| if (projectId) { | ||
| const { data: data2 } = await this.client.get( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/inject`, | ||
| { params: { format: "json" } } | ||
| ); | ||
| return data2.context ?? data2; | ||
| } | ||
| const { data } = await this.client.get("/api/v1/context/active", { params: { format: "json" } }); | ||
| return data; | ||
| } | ||
| async searchContext(query, limit = 5) { | ||
| const { data } = await this.client.post("/api/v1/context/search", { query, limit }); | ||
| return data; | ||
| } | ||
| async getWorkspaceOverview(params = {}) { | ||
| const query = {}; | ||
| if (params.mode) query.mode = params.mode; | ||
| if (params.verbosity) query.verbosity = params.verbosity; | ||
| if (params.include?.length) query.include = params.include.join(","); | ||
| if (params.format) query.format = params.format; | ||
| const { data } = await this.client.get("/api/v1/context/workspace/overview", { params: query }); | ||
| return data.overview || data; | ||
| } | ||
| async getWorkspaceEntities(params = {}) { | ||
| const query = {}; | ||
| if (params.types?.length) query.types = params.types.join(","); | ||
| if (params.query) query.query = params.query; | ||
| if (params.project_id) query.project_id = params.project_id; | ||
| if (params.related_project_id) query.related_project_id = params.related_project_id; | ||
| if (params.scope) query.scope = params.scope; | ||
| if (params.surface) query.surface = params.surface; | ||
| if (params.status) query.status = params.status; | ||
| if (typeof params.include_inferred === "boolean") query.include_inferred = params.include_inferred ? 1 : 0; | ||
| if (typeof params.include_resolved === "boolean") query.include_resolved = params.include_resolved ? 1 : 0; | ||
| if (typeof params.curated_only === "boolean") query.curated_only = params.curated_only ? 1 : 0; | ||
| if (typeof params.limit === "number") query.limit = params.limit; | ||
| if (params.format) query.format = params.format; | ||
| const { data } = await this.client.get("/api/v1/context/workspace/entities", { params: query }); | ||
| return data; | ||
| } | ||
| async getProjectMap(params = {}) { | ||
| const projectId = params.project_id; | ||
| if (!projectId) { | ||
| const { data: data2 } = await this.client.get("/api/v1/context/active"); | ||
| if (!data2?.project_id) return null; | ||
| params.project_id = data2.project_id; | ||
| } | ||
| const query = {}; | ||
| if (params.verbosity) query.verbosity = params.verbosity; | ||
| if (params.format) query.format = params.format; | ||
| const resolvedProjectId = String(params.project_id); | ||
| const { data } = await this.client.get(`/api/v1/context/projects/${encodeURIComponent(resolvedProjectId)}/map`, { params: query }); | ||
| return data.project_map || data; | ||
| } | ||
| async findKnownPaths(params) { | ||
| const query = { query: params.query }; | ||
| if (params.project_id) query.project_id = params.project_id; | ||
| if (typeof params.limit === "number") query.limit = params.limit; | ||
| if (params.format) query.format = params.format; | ||
| const { data } = await this.client.get("/api/v1/context/workspace/known-paths", { params: query }); | ||
| return data; | ||
| } | ||
| async getNextBestContext(params = {}) { | ||
| const query = {}; | ||
| if (params.tool) query.tool = params.tool; | ||
| if (params.cwd) query.cwd = params.cwd; | ||
| if (params.project_id) query.project_id = params.project_id; | ||
| if (typeof params.limit === "number") query.limit = params.limit; | ||
| const { data } = await this.client.get("/api/v1/context/workspace/next", { params: query }); | ||
| return data.result || data; | ||
| } | ||
| async upsertWorkspaceEntities(input) { | ||
| const { data } = await this.client.post("/api/v1/context/workspace/entities", input); | ||
| return data; | ||
| } | ||
| async recordHotPaths(projectId, items) { | ||
| const { data } = await this.client.post(`/api/v1/context/projects/${encodeURIComponent(projectId)}/hot-paths`, { items }); | ||
| return data; | ||
| } | ||
| async recordDebugFinding(projectId, input) { | ||
| const { data } = await this.client.post(`/api/v1/context/projects/${encodeURIComponent(projectId)}/debug-findings`, input); | ||
| return data; | ||
| } | ||
| async confirmClaim(projectId, input) { | ||
| const { data } = await this.client.post(`/api/v1/context/projects/${encodeURIComponent(projectId)}/verify`, input); | ||
| return data; | ||
| } | ||
| async updateGlobalContext(patch) { | ||
| const { data } = await this.client.patch("/api/v1/context/global", patch); | ||
| return data; | ||
| } | ||
| async patchProjectContext(projectId, delta) { | ||
| const { data } = await this.client.patch(`/api/v1/context/${encodeURIComponent(projectId)}/context`, delta); | ||
| return data; | ||
| } | ||
| // ── Shared skills ────────────────────────────────────────────────────────── | ||
| async listSkills(projectId) { | ||
| const params = {}; | ||
| if (projectId) params.project_id = projectId; | ||
| const { data } = await this.client.get("/api/v1/context/skills", { params }); | ||
| return data?.skills ?? []; | ||
| } | ||
| async getSkill(idOrSlug) { | ||
| const { data } = await this.client.get(`/api/v1/context/skills/${encodeURIComponent(idOrSlug)}`); | ||
| return data?.skill ?? null; | ||
| } | ||
| async upsertSkill(input) { | ||
| const { data } = await this.client.post("/api/v1/context/skills", input); | ||
| return data?.skill ?? data; | ||
| } | ||
| // Import a shared skill by pasting a GitHub repo URL. The API fetches the | ||
| // repo's SKILL.md, parses its frontmatter, and creates the shared skill. | ||
| async importSkillFromGithub(repoUrl, projectId) { | ||
| const body = { repo_url: repoUrl }; | ||
| if (projectId) body.project_id = projectId; | ||
| const { data } = await this.client.post("/api/v1/context/skills/import-github", body); | ||
| return data; | ||
| } | ||
| }; | ||
| export { | ||
| __require, | ||
| __commonJS, | ||
| __toESM, | ||
| init_esm_shims, | ||
| DeerdawnClient | ||
| }; |
| #!/usr/bin/env node | ||
| import { createRequire } from "module"; | ||
| const require = createRequire(import.meta.url); | ||
| import { | ||
| DeerdawnClient | ||
| } from "./chunk-5Y3RCSO4.js"; | ||
| export { | ||
| DeerdawnClient | ||
| }; |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 2 instances
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
No website
QualityPackage does not have a website.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
353328
1.49%7501
0.7%0
-100%0
-100%410
4.06%90
-1.1%12
20%