feat: sync open-source context and capsule removal
- remove legacy contexts surfaces from the app, MCP bridge, standalone server, and docs - keep getContext and retrieveQueryContext while aligning the simplified graph-only contract - harden dev startup migration behavior and disable the accidental chat surface in open source Generated with Codex
This commit is contained in:
@@ -2,6 +2,7 @@ import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { getInternalApiBaseUrl } from '@/services/runtime/apiBase';
|
||||
import { formatNodeForChat } from '../infrastructure/nodeFormatter';
|
||||
import { getInternalAuthHeaders } from '@/services/auth/internalAuth';
|
||||
|
||||
function extractTextFromMessageContent(content: unknown): string {
|
||||
if (typeof content === 'string') {
|
||||
@@ -56,14 +57,13 @@ function inferSourceFromContext(params: { title: string; description?: string; s
|
||||
}
|
||||
|
||||
export const createNodeTool = tool({
|
||||
description: 'Create a node after you have already decided this should be a net-new write. If the user explicitly asked to save or import something and duplicate/update checks are complete, write immediately. If you are only suggesting a save, propose the node first and wait for confirmation. Leave context blank by default. Only set context when the user explicitly wants one and it is clear and useful; when that happens, use context_name rather than a numeric ID. Focus on a clean title, a strong natural description that says what the thing is, preserved source text, and the right metadata. When the node comes from the user\'s own idea, note, or dictated thought, preserve their actual wording in source with only minimal cleanup instead of flattening it into a summary. Do not block creation if the description is incomplete. If the description framing is materially inferred, create the node first and then invite one concise user correction pass.',
|
||||
description: 'Create a node after you have already decided this should be a net-new write. If the user explicitly asked to save or import something and duplicate/update checks are complete, write immediately. If you are only suggesting a save, propose the node first and wait for confirmation. Focus on a clean title, a strong natural description that says what the thing is, preserved source text, and the right metadata. When the node comes from the user\'s own idea, note, or dictated thought, preserve their actual wording in source with only minimal cleanup instead of flattening it into a summary. Do not block creation if the description is incomplete. If the description framing is materially inferred, create the node first and then invite one concise user correction pass.',
|
||||
inputSchema: z.object({
|
||||
title: z.string().describe('The title of the node'),
|
||||
description: z.string().max(500).optional().describe('Optional natural description. If you have enough context, describe what this is, why it belongs in Brad\'s graph, and its current workflow status in normal prose. Do not use labels like WHAT:, WHY:, or STATUS:.'),
|
||||
source: z.string().optional().describe('Canonical source content for embedding. For external content, store the actual transcript/article/document text. For user-authored ideas or dictated notes, store the user\'s original wording as fully as possible with only minimal cleanup such as obvious whitespace or transcription artifacts. Do not replace raw user thinking with a thin summary.'),
|
||||
link: z.string().optional().describe('A URL link to the source'),
|
||||
event_date: z.string().optional().describe('When the thing actually happened (ISO 8601). Not when it was added to the graph.'),
|
||||
context_name: z.string().optional().describe('Optional primary context name. Use only when the user explicitly wants this node assigned to a known context.'),
|
||||
metadata: z.record(z.any()).optional().describe('Optional node metadata. Use canonical keys when known: type, state, captured_method, captured_by, and source_metadata. Source-specific facts belong inside source_metadata.')
|
||||
}).passthrough(),
|
||||
execute: async (params, context) => {
|
||||
@@ -74,12 +74,12 @@ export const createNodeTool = tool({
|
||||
// Call the nodes API endpoint
|
||||
const response = await fetch(`${getInternalApiBaseUrl()}/api/nodes`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: getInternalAuthHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify({ ...params, source: canonicalSource ?? params.source })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -88,6 +88,7 @@ export const createNodeTool = tool({
|
||||
};
|
||||
}
|
||||
|
||||
// Format the created node for chat display
|
||||
const formattedDisplay = formatNodeForChat({
|
||||
id: result.data.id,
|
||||
title: result.data.title,
|
||||
@@ -97,9 +98,9 @@ export const createNodeTool = tool({
|
||||
success: true,
|
||||
data: {
|
||||
...result.data,
|
||||
formatted_display: formattedDisplay,
|
||||
formatted_display: formattedDisplay
|
||||
},
|
||||
message: `Created: ${formattedDisplay}`
|
||||
message: `Created node ${formattedDisplay}`
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
@@ -111,4 +112,5 @@ export const createNodeTool = tool({
|
||||
}
|
||||
});
|
||||
|
||||
// Legacy export for backwards compatibility
|
||||
export const createItemTool = createNodeTool;
|
||||
|
||||
@@ -36,8 +36,6 @@ export const getNodesByIdTool = tool({
|
||||
title: node.title,
|
||||
link: node.link,
|
||||
event_date: node.event_date ?? null,
|
||||
context_id: node.context_id ?? null,
|
||||
context: node.context ?? null,
|
||||
chunk_status: node.chunk_status || 'unknown',
|
||||
created_at: node.created_at,
|
||||
updated_at: node.updated_at,
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { contextService } from '@/services/database/contextService';
|
||||
|
||||
type QueryContextFilters = {
|
||||
id?: number;
|
||||
name?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
includeNodes?: boolean;
|
||||
};
|
||||
|
||||
function matchesSearch(value: string | null | undefined, search: string): boolean {
|
||||
if (!value) return false;
|
||||
return value.toLowerCase().includes(search);
|
||||
}
|
||||
|
||||
export const queryContextsTool = tool({
|
||||
description: 'List and inspect contexts. Use this to discover available primary scopes before filtering nodes or assigning node context.',
|
||||
inputSchema: z.object({
|
||||
filters: z.object({
|
||||
id: z.number().int().positive().describe('Exact context ID lookup.').optional(),
|
||||
name: z.string().describe('Exact context name lookup.').optional(),
|
||||
search: z.string().describe('Case-insensitive search across context names and descriptions.').optional(),
|
||||
limit: z.number().min(1).max(100).default(50).describe('Maximum number of contexts to return.').optional(),
|
||||
includeNodes: z.boolean().default(false).describe('Include the node list for an exact single-context lookup.').optional(),
|
||||
}).optional(),
|
||||
}),
|
||||
execute: async ({ filters = {} }: { filters?: QueryContextFilters }) => {
|
||||
try {
|
||||
const limit = filters.limit || 50;
|
||||
const normalizedName = filters.name?.trim();
|
||||
const normalizedSearch = filters.search?.trim().toLowerCase();
|
||||
|
||||
let contexts = await contextService.listContexts();
|
||||
|
||||
if (filters.id !== undefined) {
|
||||
contexts = contexts.filter((context) => context.id === filters.id);
|
||||
}
|
||||
|
||||
if (normalizedName) {
|
||||
contexts = contexts.filter((context) => context.name.toLowerCase() === normalizedName.toLowerCase());
|
||||
}
|
||||
|
||||
if (normalizedSearch) {
|
||||
contexts = contexts.filter((context) =>
|
||||
matchesSearch(context.name, normalizedSearch) ||
|
||||
matchesSearch(context.description, normalizedSearch)
|
||||
);
|
||||
}
|
||||
|
||||
const limitedContexts = contexts.slice(0, limit);
|
||||
const canIncludeNodes =
|
||||
filters.includeNodes === true &&
|
||||
limitedContexts.length === 1 &&
|
||||
(filters.id !== undefined || Boolean(normalizedName));
|
||||
|
||||
const contextsWithNodes = await Promise.all(
|
||||
limitedContexts.map(async (context) => {
|
||||
if (!canIncludeNodes) return context;
|
||||
|
||||
const nodes = await contextService.getNodesForContext(context.id);
|
||||
return {
|
||||
...context,
|
||||
nodes: nodes.map((node) => ({
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
description: node.description ?? null,
|
||||
context_id: node.context_id ?? null,
|
||||
context: node.context ?? null,
|
||||
updated_at: node.updated_at,
|
||||
})),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const message = contextsWithNodes.length === 0
|
||||
? 'No contexts found.'
|
||||
: `Found ${contextsWithNodes.length} context${contextsWithNodes.length === 1 ? '' : 's'}:\n${contextsWithNodes.map((context) => `• ${context.name} (#${context.id}, ${context.count} nodes)`).join('\n')}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
contexts: contextsWithNodes,
|
||||
count: contextsWithNodes.length,
|
||||
total_available: contexts.length,
|
||||
filters_applied: filters,
|
||||
},
|
||||
message,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('QueryContexts tool error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to query contexts',
|
||||
data: {
|
||||
contexts: [],
|
||||
count: 0,
|
||||
filters_applied: filters,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -84,7 +84,6 @@ export const queryEdgeTool = tool({
|
||||
id: connection.connected_node.id,
|
||||
title: connection.connected_node.title,
|
||||
description: truncateText(connection.connected_node.description, 140),
|
||||
context: connection.connected_node.context ?? null,
|
||||
formatted_display: formattedNode
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,8 +4,6 @@ import { formatNodeForChat } from '../infrastructure/nodeFormatter';
|
||||
import { directNodeLookup } from '@/services/retrieval/directNodeLookup';
|
||||
|
||||
type QueryNodeFilters = {
|
||||
contextId?: number;
|
||||
context_name?: string;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
createdAfter?: string;
|
||||
@@ -15,10 +13,9 @@ type QueryNodeFilters = {
|
||||
};
|
||||
|
||||
export const queryNodesTool = tool({
|
||||
description: 'Find specific existing nodes in the graph by searching title, description, and source. Use this first when the user is trying to locate a node they already created or a specific existing podcast, article, idea, person, project, or note. For broader current-turn grounding of a substantive question, use retrieveQueryContext instead. Leave context blank by default. If the user explicitly wants a context filter, use context_name rather than a numeric ID.',
|
||||
description: 'Find specific existing nodes in the graph by searching title, description, and source. Use this first when the user is trying to locate a node they already created or a specific existing podcast, article, idea, person, project, or note. For broader current-turn grounding of a substantive question, use retrieveQueryContext instead.',
|
||||
inputSchema: z.object({
|
||||
filters: z.object({
|
||||
context_name: z.string().describe('Optional primary context name filter. Use only when the user explicitly wants a context-specific lookup.').optional(),
|
||||
search: z.string().describe('Search term to match against node title, description, or source').optional(),
|
||||
limit: z.number().min(1).max(50).default(10).describe('Maximum number of results to return'),
|
||||
createdAfter: z.string().optional().describe('ISO date (YYYY-MM-DD). Only return nodes created on or after this date.'),
|
||||
@@ -33,8 +30,6 @@ export const queryNodesTool = tool({
|
||||
const result = await directNodeLookup({
|
||||
search: filters.search,
|
||||
limit: filters.limit,
|
||||
context_name: filters.context_name,
|
||||
contextId: filters.contextId,
|
||||
createdAfter: filters.createdAfter,
|
||||
createdBefore: filters.createdBefore,
|
||||
eventAfter: filters.eventAfter,
|
||||
|
||||
@@ -7,15 +7,13 @@ export const retrieveQueryContextTool = tool({
|
||||
inputSchema: z.object({
|
||||
query: z.string().min(1).describe('The raw user query for this turn.'),
|
||||
focused_node_id: z.number().int().positive().nullable().optional().describe('Optional currently focused node ID.'),
|
||||
active_context_id: z.number().int().positive().nullable().optional().describe('Optional active context ID as a soft hint.'),
|
||||
limit: z.number().int().min(1).max(12).optional().describe('Maximum number of nodes to return.'),
|
||||
}),
|
||||
execute: async ({ query, focused_node_id, active_context_id, limit }) => {
|
||||
execute: async ({ query, focused_node_id, limit }) => {
|
||||
try {
|
||||
const result = await retrieveQueryContext({
|
||||
query,
|
||||
focused_node_id: focused_node_id ?? null,
|
||||
active_context_id: active_context_id ?? null,
|
||||
limit,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { getInternalApiBaseUrl } from '@/services/runtime/apiBase';
|
||||
import { getInternalAuthHeaders } from '@/services/auth/internalAuth';
|
||||
|
||||
export const updateNodeTool = tool({
|
||||
description: 'Update node fields when the existing node is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates should proceed once the target node is clear; if you are only proposing a change, ask first. Use this to enrich or correct nodes without losing canonical source content. Context is preserved unless the user explicitly wants it changed. When that happens, prefer context_name rather than a numeric ID. Use clear_context only when the user explicitly wants the context removed. When fixing a user-authored idea node, source should preserve the user\'s original wording as fully as possible. Never block an update because the description is incomplete. If the new description framing is materially inferred, complete the update and then invite one concise user feedback pass.',
|
||||
description: 'Update node fields when the existing node is clearly the same artifact and a net-new node would be redundant. Explicit user-directed updates should proceed once the target node is clear; if you are only proposing a change, ask first. Use this to enrich or correct nodes without losing canonical source content. When fixing a user-authored idea node, source should preserve the user\'s original wording as fully as possible. Never block an update because the description is incomplete. If the new description framing is materially inferred, complete the update and then invite one concise user feedback pass.',
|
||||
inputSchema: z.object({
|
||||
id: z.number().describe('The ID of the node to update'),
|
||||
updates: z.object({
|
||||
@@ -12,8 +13,6 @@ export const updateNodeTool = tool({
|
||||
source: z.string().optional().describe('Canonical source content for embedding. Use this to set or correct the raw source text. For user-authored ideas or dictated notes, preserve the user\'s original wording with only minimal cleanup rather than compressing it into a summary.'),
|
||||
link: z.string().optional().describe('New link'),
|
||||
event_date: z.string().optional().describe('When the thing actually happened (ISO 8601). Not when it was added to the graph.'),
|
||||
context_name: z.string().optional().describe('Optional primary context name. Use only when the user explicitly wants to assign this node to a known context.'),
|
||||
clear_context: z.boolean().optional().describe('Set true only when the user explicitly wants to remove the node context.'),
|
||||
metadata: z.record(z.any()).optional().describe('Metadata patch. It now merges with existing metadata instead of replacing the full blob. Use canonical keys: type, state, captured_method, captured_by, source_metadata.')
|
||||
}).passthrough().describe('Object containing the fields to update. Derived analysis should be stored in a separate linked node, not appended to the source node.')
|
||||
}),
|
||||
@@ -30,12 +29,12 @@ export const updateNodeTool = tool({
|
||||
// Call the nodes API endpoint
|
||||
const response = await fetch(`${getInternalApiBaseUrl()}/api/nodes/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: getInternalAuthHeaders({ 'Content-Type': 'application/json' }),
|
||||
body: JSON.stringify(updates)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { getInternalApiBaseUrl } from '@/services/runtime/apiBase';
|
||||
import { formatNodeForChat } from '../infrastructure/nodeFormatter';
|
||||
|
||||
export const writeContextTool = tool({
|
||||
description: 'Write one atomic durable context node to the graph only after the user has explicitly approved the save. Use this for agent-suggested capture after you already proposed the node briefly and got a clear yes. Prefer ordinary create/update flows for explicit user-directed capture. Never call it unless the user has clearly said yes.',
|
||||
inputSchema: z.object({
|
||||
title: z.string().min(1).max(160).describe('Clear proposed node title.'),
|
||||
description: z.string().min(1).max(500).describe('Natural description of what this context is and why it matters.'),
|
||||
source: z.string().optional().describe('Optional source or verbatim user wording to preserve.'),
|
||||
context_name: z.string().optional().describe('Optional primary context name. Use only when the user explicitly wants this saved under a known context.'),
|
||||
metadata: z.record(z.any()).optional().describe('Optional metadata patch.'),
|
||||
confirmed_by_user: z.boolean().describe('Must be true. Reject the write otherwise.'),
|
||||
}).passthrough(),
|
||||
execute: async ({ title, description, source, context_name, metadata, confirmed_by_user, ...legacyParams }) => {
|
||||
if (!confirmed_by_user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'writeContext requires explicit user confirmation before writing to the graph.',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getInternalApiBaseUrl()}/api/nodes`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: title.trim(),
|
||||
description: description.trim(),
|
||||
source: source?.trim() || undefined,
|
||||
context_name: context_name?.trim() || undefined,
|
||||
context_id: typeof legacyParams.context_id === 'number' || legacyParams.context_id === null
|
||||
? legacyParams.context_id
|
||||
: undefined,
|
||||
metadata: {
|
||||
captured_by: 'human',
|
||||
captured_method: 'write_context',
|
||||
...(metadata || {}),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error || 'Failed to write context node',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
const formattedDisplay = formatNodeForChat({
|
||||
id: result.data.id,
|
||||
title: result.data.title,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...result.data,
|
||||
formatted_display: formattedDisplay,
|
||||
},
|
||||
message: `Saved context as ${formattedDisplay}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to write context node',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -38,8 +38,9 @@ export const TOOL_GROUP_ASSIGNMENTS: Record<string, string> = {
|
||||
retrieveQueryContext: 'core',
|
||||
getNodesById: 'core',
|
||||
queryEdge: 'core',
|
||||
queryContexts: 'core',
|
||||
searchContentEmbeddings: 'core',
|
||||
listSkills: 'core',
|
||||
readSkill: 'core',
|
||||
|
||||
// Orchestration: Web search and reasoning (orchestrator only)
|
||||
webSearch: 'orchestration',
|
||||
@@ -47,7 +48,6 @@ export const TOOL_GROUP_ASSIGNMENTS: Record<string, string> = {
|
||||
|
||||
// Execution: Write operations and extraction (workers only)
|
||||
createNode: 'execution',
|
||||
writeContext: 'execution',
|
||||
updateNode: 'execution',
|
||||
deleteNode: 'execution',
|
||||
createEdge: 'execution',
|
||||
@@ -56,6 +56,8 @@ export const TOOL_GROUP_ASSIGNMENTS: Record<string, string> = {
|
||||
youtubeExtract: 'execution',
|
||||
websiteExtract: 'execution',
|
||||
paperExtract: 'execution',
|
||||
writeSkill: 'execution',
|
||||
deleteSkill: 'execution'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,10 +3,8 @@ import { queryNodesTool } from '../database/queryNodes';
|
||||
import { retrieveQueryContextTool } from '../database/retrieveQueryContext';
|
||||
import { getNodesByIdTool } from '../database/getNodesById';
|
||||
import { queryEdgeTool } from '../database/queryEdge';
|
||||
import { queryContextsTool } from '../database/queryContexts';
|
||||
import { createNodeTool } from '../database/createNode';
|
||||
import { updateNodeTool } from '../database/updateNode';
|
||||
import { writeContextTool } from '../database/writeContext';
|
||||
import { deleteNodeTool } from '../database/deleteNode';
|
||||
import { createEdgeTool } from '../database/createEdge';
|
||||
import { updateEdgeTool } from '../database/updateEdge';
|
||||
@@ -17,17 +15,22 @@ import { youtubeExtractTool } from '../other/youtubeExtract';
|
||||
import { websiteExtractTool } from '../other/websiteExtract';
|
||||
import { paperExtractTool } from '../other/paperExtract';
|
||||
import { sqliteQueryTool } from '../other/sqliteQuery';
|
||||
import { listSkillsTool } from '../skills/listSkills';
|
||||
import { readSkillTool } from '../skills/readSkill';
|
||||
import { writeSkillTool } from '../skills/writeSkill';
|
||||
import { deleteSkillTool } from '../skills/deleteSkill';
|
||||
import { logEvalToolCall } from '@/services/evals/evalsLogger';
|
||||
|
||||
// Read tools (graph queries)
|
||||
// Read tools (graph queries + skills)
|
||||
const CORE_TOOLS: Record<string, any> = {
|
||||
sqliteQuery: sqliteQueryTool,
|
||||
queryNodes: queryNodesTool,
|
||||
retrieveQueryContext: retrieveQueryContextTool,
|
||||
getNodesById: getNodesByIdTool,
|
||||
queryEdge: queryEdgeTool,
|
||||
queryContexts: queryContextsTool,
|
||||
searchContentEmbeddings: searchContentEmbeddingsTool,
|
||||
listSkills: listSkillsTool,
|
||||
readSkill: readSkillTool,
|
||||
};
|
||||
|
||||
// Utility tools
|
||||
@@ -39,7 +42,6 @@ const ORCHESTRATION_TOOLS: Record<string, any> = {
|
||||
// Write tools (includes extraction)
|
||||
const EXECUTION_TOOLS: Record<string, any> = {
|
||||
createNode: createNodeTool,
|
||||
writeContext: writeContextTool,
|
||||
updateNode: updateNodeTool,
|
||||
deleteNode: deleteNodeTool,
|
||||
createEdge: createEdgeTool,
|
||||
@@ -47,6 +49,8 @@ const EXECUTION_TOOLS: Record<string, any> = {
|
||||
youtubeExtract: youtubeExtractTool,
|
||||
websiteExtract: websiteExtractTool,
|
||||
paperExtract: paperExtractTool,
|
||||
writeSkill: writeSkillTool,
|
||||
deleteSkill: deleteSkillTool,
|
||||
};
|
||||
|
||||
export const TOOL_SETS = {
|
||||
|
||||
@@ -45,7 +45,7 @@ function isReadOnlyQuery(sql: string): boolean {
|
||||
}
|
||||
|
||||
export const sqliteQueryTool = tool({
|
||||
description: 'Execute read-only SQL queries (SELECT/WITH/PRAGMA). Tables: nodes, contexts, edges, chunks, logs, and migration snapshots. Use PRAGMA table_info(tablename) for schema.',
|
||||
description: 'Execute read-only SQL queries (SELECT/WITH/PRAGMA). Tables: nodes, edges, chunks, logs, chats, voice_usage, and migration snapshots. Use PRAGMA table_info(tablename) for schema.',
|
||||
|
||||
inputSchema: z.object({
|
||||
sql: z.string().describe('The SQL query to execute. Must be a SELECT, WITH, or PRAGMA statement.'),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { deleteSkill } from '@/services/skills/skillService';
|
||||
import { eventBroadcaster } from '@/services/events';
|
||||
|
||||
export const deleteSkillTool = tool({
|
||||
description: 'Delete a skill by name.',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('The name of the skill to delete'),
|
||||
}),
|
||||
execute: async ({ name }) => {
|
||||
try {
|
||||
const result = deleteSkill(name);
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
eventBroadcaster.broadcast({ type: 'GUIDE_UPDATED', data: { name } });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { name },
|
||||
message: `Skill "${name}" deleted`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete skill',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { listSkills } from '@/services/skills/skillService';
|
||||
|
||||
export const listSkillsTool = tool({
|
||||
description: 'List all available skills with their names and descriptions.',
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
try {
|
||||
const skills = listSkills();
|
||||
return {
|
||||
success: true,
|
||||
data: skills,
|
||||
message: `Found ${skills.length} skills`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to list skills',
|
||||
data: [],
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { readSkill } from '@/services/skills/skillService';
|
||||
|
||||
export const readSkillTool = tool({
|
||||
description: 'Read a skill by name. Returns the full markdown content with instructions.',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('The name of the skill to read'),
|
||||
}),
|
||||
execute: async ({ name }) => {
|
||||
try {
|
||||
const skill = readSkill(name);
|
||||
if (!skill) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Skill "${name}" not found`,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: skill,
|
||||
message: `Loaded skill: ${skill.name}`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to read skill',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { writeSkill } from '@/services/skills/skillService';
|
||||
import { eventBroadcaster } from '@/services/events';
|
||||
|
||||
export const writeSkillTool = tool({
|
||||
description: 'Write or update a skill. Content should be full markdown with YAML frontmatter (name, description).',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('The name of the skill to write'),
|
||||
content: z.string().describe('Full markdown content including YAML frontmatter'),
|
||||
}),
|
||||
execute: async ({ name, content }) => {
|
||||
try {
|
||||
const result = writeSkill(name, content);
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: result.error,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
eventBroadcaster.broadcast({ type: 'GUIDE_UPDATED', data: { name } });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { name },
|
||||
message: `Skill "${name}" saved`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to write skill',
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user