@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 fs2 from "fs"; | ||
| import path2 from "path"; | ||
| import os2 from "os"; | ||
| // src/utils/safe-fs.ts | ||
| init_esm_shims(); | ||
| import fs from "fs"; | ||
| function isUnsafeSymlink(filePath) { | ||
| try { | ||
| return fs.lstatSync(filePath).isSymbolicLink(); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| function safeWriteFileSync(filePath, data, options) { | ||
| if (isUnsafeSymlink(filePath)) { | ||
| console.error(`[DeerDawn] Refusing to write through symlink at ${filePath} \u2014 remove it manually if this is expected.`); | ||
| return false; | ||
| } | ||
| fs.writeFileSync(filePath, data, options); | ||
| return true; | ||
| } | ||
| // src/agent-session.ts | ||
| init_esm_shims(); | ||
| import os from "os"; | ||
| import { randomBytes } from "crypto"; | ||
| var cachedSessionId = null; | ||
| function getAgentSessionId() { | ||
| if (cachedSessionId) return cachedSessionId; | ||
| const pid = typeof process.pid === "number" ? process.pid : 0; | ||
| cachedSessionId = `agent-${pid.toString(36)}-${randomBytes(3).toString("hex")}`; | ||
| return cachedSessionId; | ||
| } | ||
| function getAgentSessionContext(cwd) { | ||
| let host = null; | ||
| try { | ||
| host = os.hostname() || null; | ||
| } catch { | ||
| host = null; | ||
| } | ||
| let resolvedCwd = null; | ||
| try { | ||
| resolvedCwd = cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd() || null; | ||
| } catch { | ||
| resolvedCwd = null; | ||
| } | ||
| return { cwd: resolvedCwd, host, status: "active" }; | ||
| } | ||
| // src/client.ts | ||
| 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(os2.homedir(), ".deerdawn"); | ||
| } | ||
| function getQueuePath() { | ||
| return path2.join(getDeerdawnHome(), "cloud-write-queue.json"); | ||
| } | ||
| function ensureDeerdawnHome() { | ||
| fs2.mkdirSync(getDeerdawnHome(), { recursive: true }); | ||
| } | ||
| function readQueueState() { | ||
| try { | ||
| return JSON.parse(fs2.readFileSync(getQueuePath(), "utf8")); | ||
| } catch { | ||
| return { entries: [] }; | ||
| } | ||
| } | ||
| function writeQueueState(state) { | ||
| ensureDeerdawnHome(); | ||
| safeWriteFileSync(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; | ||
| params.session_id = getAgentSessionId(); | ||
| const identity = getAgentSessionContext(cwd); | ||
| if (identity.host) params.host = identity.host; | ||
| const { data } = await this.client.get("/api/v1/context/active", { params }); | ||
| return data; | ||
| } | ||
| async pushContextUpdate(projectId, summary, tool = "claude_code", commands) { | ||
| const sessionId = getAgentSessionId(); | ||
| const [extractionResult] = await Promise.all([ | ||
| this.client.post("/api/v1/context/extraction", { | ||
| session_id: sessionId, | ||
| tool, | ||
| project_id: projectId, | ||
| // Tells a peer WHERE this agent is working, not just that it exists. | ||
| session_context: getAgentSessionContext(), | ||
| 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; | ||
| } | ||
| // Archived projects only. Used by start_session so a user whose projects all | ||
| // aged out is told their memory is intact and restorable. | ||
| async listArchivedContextProjects() { | ||
| const { data } = await this.client.get("/api/v1/context/projects", { params: { archived: 1 } }); | ||
| 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; | ||
| } | ||
| /** | ||
| * The project's raw structured context — ALWAYS the flat context object | ||
| * (`{project_id, project_name, updated_at, ...context}`), whichever endpoint | ||
| * served it. | ||
| * | ||
| * These two branches used to return different shapes: the projectId path | ||
| * unwrapped `data.context`, while the no-projectId path returned the whole | ||
| * `/active` envelope, which has no `context` key at all. Callers therefore had | ||
| * to guess, and `manage_todos` guessed wrong in the one direction that fails | ||
| * silently — reading `ctx.context.todo_items` off a shape where `.context` is | ||
| * undefined yields no error, just a permanently empty board. | ||
| * | ||
| * One shape, decided here, so no caller has to branch on it again. | ||
| */ | ||
| async getContextJson(projectId) { | ||
| const path3 = projectId ? `/api/v1/context/projects/${encodeURIComponent(projectId)}/inject` : "/api/v1/context/active"; | ||
| const { data } = await this.client.get(path3, { params: { format: "json" } }); | ||
| if (data && typeof data === "object" && "context" in data) return data.context ?? {}; | ||
| 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; | ||
| query.session_id = getAgentSessionId(); | ||
| 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 buildSubagentBrief(params) { | ||
| const { data } = await this.client.post("/api/v1/context/subagent-brief", params); | ||
| return data.brief || data; | ||
| } | ||
| async recordHotPaths(projectId, items) { | ||
| const { data } = await this.client.post(`/api/v1/context/projects/${encodeURIComponent(projectId)}/hot-paths`, { items }); | ||
| return data; | ||
| } | ||
| // ─── Trails ──────────────────────────────────────────────────────────────── | ||
| // exportTrail returns a PRIVATE, unscanned archive. Publishing is a separate | ||
| // gated flow (preview -> per-finding decisions -> publish); there is no | ||
| // one-call publish here on purpose. | ||
| async exportTrail(projectId, source, options) { | ||
| const query = [ | ||
| ["repo_url", source?.repo_url], | ||
| ["commit_sha", source?.commit_sha], | ||
| ["branch", source?.branch], | ||
| ["kind", options?.kind], | ||
| ["tool", "claude_code"] | ||
| ].filter(([, value]) => Boolean(value)).map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`).join("&"); | ||
| const { data } = await this.client.get( | ||
| `/api/v1/context/projects/${encodeURIComponent(projectId)}/trail${query ? `?${query}` : ""}` | ||
| ); | ||
| return data; | ||
| } | ||
| async previewTrailPublish(projectId) { | ||
| const { data } = await this.client.get(`/api/v1/context/projects/${encodeURIComponent(projectId)}/trail/preview`); | ||
| return data; | ||
| } | ||
| async importTrail(input) { | ||
| const { data } = await this.client.post("/api/v1/context/trail/import", input); | ||
| 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, | ||
| safeWriteFileSync, | ||
| getAgentSessionId, | ||
| DeerdawnClient | ||
| }; |
| #!/usr/bin/env node | ||
| import { createRequire } from "module"; | ||
| const require = createRequire(import.meta.url); | ||
| import { | ||
| DeerdawnClient | ||
| } from "./chunk-VCJ5LQ54.js"; | ||
| export { | ||
| DeerdawnClient | ||
| }; |
+3
-3
| { | ||
| "name": "@deerdawn/mcp-server", | ||
| "mcpName": "com.deerdawn/deerdawn", | ||
| "version": "1.0.41", | ||
| "version": "1.0.42", | ||
| "description": "MCP server for DeerDawn — AI session memory that briefs every new AI session", | ||
@@ -36,3 +36,3 @@ "homepage": "https://deerdawn.com", | ||
| "dependencies": { | ||
| "@modelcontextprotocol/sdk": "^1.0.4", | ||
| "@modelcontextprotocol/sdk": "^1.30.0", | ||
| "axios": "^1.7.9", | ||
@@ -46,4 +46,4 @@ "zod": "^4.4.3" | ||
| "eslint": "^9.18.0", | ||
| "nock": "^14.0.0", | ||
| "tsup": "^8.3.5", | ||
| "nock": "^14.0.0", | ||
| "typescript": "^6.0.3", | ||
@@ -50,0 +50,0 @@ "vitest": "^4.1.7" |
+10
-2
@@ -14,3 +14,3 @@ # DeerDawn MCP Server | ||
| ```bash | ||
| claude mcp add -s user --transport http deerdawn https://api.deerdawn.com/api/v1/mcp | ||
| claude mcp add -s user --transport http deerdawn https://api.deerdawn.com/mcp | ||
| ``` | ||
@@ -21,3 +21,3 @@ | ||
| ``` | ||
| https://api.deerdawn.com/api/v1/mcp | ||
| https://api.deerdawn.com/mcp | ||
| ``` | ||
@@ -61,2 +61,10 @@ | ||
| Show the active project’s task board directly in a terminal, or select a project | ||
| by its exact ID, exact name, or a unique part of its name: | ||
| ```bash | ||
| npx -y @deerdawn/mcp-server@latest tasks | ||
| npx -y @deerdawn/mcp-server@latest tasks "DeerDawn" | ||
| ``` | ||
| Or register manually (user scope so it works across all projects): | ||
@@ -63,0 +71,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 http from "http"; | ||
| import https from "https"; | ||
| import fs2 from "fs"; | ||
| import path2 from "path"; | ||
| import os from "os"; | ||
| // src/utils/safe-fs.ts | ||
| init_esm_shims(); | ||
| import fs from "fs"; | ||
| function isUnsafeSymlink(filePath) { | ||
| try { | ||
| return fs.lstatSync(filePath).isSymbolicLink(); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| function safeWriteFileSync(filePath, data, options) { | ||
| if (isUnsafeSymlink(filePath)) { | ||
| console.error(`[DeerDawn] Refusing to write through symlink at ${filePath} \u2014 remove it manually if this is expected.`); | ||
| return false; | ||
| } | ||
| fs.writeFileSync(filePath, data, options); | ||
| return true; | ||
| } | ||
| // src/client.ts | ||
| 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() { | ||
| fs2.mkdirSync(getDeerdawnHome(), { recursive: true }); | ||
| } | ||
| function readQueueState() { | ||
| try { | ||
| return JSON.parse(fs2.readFileSync(getQueuePath(), "utf8")); | ||
| } catch { | ||
| return { entries: [] }; | ||
| } | ||
| } | ||
| function writeQueueState(state) { | ||
| ensureDeerdawnHome(); | ||
| safeWriteFileSync(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; | ||
| } | ||
| /** | ||
| * The project's raw structured context — ALWAYS the flat context object | ||
| * (`{project_id, project_name, updated_at, ...context}`), whichever endpoint | ||
| * served it. | ||
| * | ||
| * These two branches used to return different shapes: the projectId path | ||
| * unwrapped `data.context`, while the no-projectId path returned the whole | ||
| * `/active` envelope, which has no `context` key at all. Callers therefore had | ||
| * to guess, and `manage_todos` guessed wrong in the one direction that fails | ||
| * silently — reading `ctx.context.todo_items` off a shape where `.context` is | ||
| * undefined yields no error, just a permanently empty board. | ||
| * | ||
| * One shape, decided here, so no caller has to branch on it again. | ||
| */ | ||
| async getContextJson(projectId) { | ||
| const path3 = projectId ? `/api/v1/context/projects/${encodeURIComponent(projectId)}/inject` : "/api/v1/context/active"; | ||
| const { data } = await this.client.get(path3, { params: { format: "json" } }); | ||
| if (data && typeof data === "object" && "context" in data) return data.context ?? {}; | ||
| 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 buildSubagentBrief(params) { | ||
| const { data } = await this.client.post("/api/v1/context/subagent-brief", params); | ||
| return data.brief || 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, | ||
| safeWriteFileSync, | ||
| DeerdawnClient | ||
| }; |
| #!/usr/bin/env node | ||
| import { createRequire } from "module"; | ||
| const require = createRequire(import.meta.url); | ||
| import { | ||
| DeerdawnClient | ||
| } from "./chunk-AGVTTUZL.js"; | ||
| export { | ||
| DeerdawnClient | ||
| }; |
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
443032
13.26%9120
10.94%435
1.87%101
5.21%