@lakitu/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +166 -0
  2. package/convex/_generated/api.d.ts +45 -0
  3. package/convex/_generated/api.js +23 -0
  4. package/convex/_generated/dataModel.d.ts +58 -0
  5. package/convex/_generated/server.d.ts +143 -0
  6. package/convex/_generated/server.js +93 -0
  7. package/convex/cloud/CLAUDE.md +238 -0
  8. package/convex/cloud/_generated/api.ts +84 -0
  9. package/convex/cloud/_generated/component.ts +861 -0
  10. package/convex/cloud/_generated/dataModel.ts +60 -0
  11. package/convex/cloud/_generated/server.ts +156 -0
  12. package/convex/cloud/convex.config.ts +16 -0
  13. package/convex/cloud/index.ts +29 -0
  14. package/convex/cloud/intentSchema/generate.ts +447 -0
  15. package/convex/cloud/intentSchema/index.ts +16 -0
  16. package/convex/cloud/intentSchema/types.ts +418 -0
  17. package/convex/cloud/ksaPolicy.ts +554 -0
  18. package/convex/cloud/mail.ts +92 -0
  19. package/convex/cloud/schema.ts +322 -0
  20. package/convex/cloud/utils/kanbanContext.ts +229 -0
  21. package/convex/cloud/workflows/agentBoard.ts +451 -0
  22. package/convex/cloud/workflows/agentPrompt.ts +272 -0
  23. package/convex/cloud/workflows/agentThread.ts +374 -0
  24. package/convex/cloud/workflows/compileSandbox.ts +146 -0
  25. package/convex/cloud/workflows/crudBoard.ts +217 -0
  26. package/convex/cloud/workflows/crudKSAs.ts +262 -0
  27. package/convex/cloud/workflows/crudLorobeads.ts +371 -0
  28. package/convex/cloud/workflows/crudSkills.ts +205 -0
  29. package/convex/cloud/workflows/crudThreads.ts +708 -0
  30. package/convex/cloud/workflows/lifecycleSandbox.ts +1396 -0
  31. package/convex/cloud/workflows/sandboxConvex.ts +1046 -0
  32. package/convex/sandbox/README.md +90 -0
  33. package/convex/sandbox/_generated/api.d.ts +2934 -0
  34. package/convex/sandbox/_generated/api.js +23 -0
  35. package/convex/sandbox/_generated/dataModel.d.ts +60 -0
  36. package/convex/sandbox/_generated/server.d.ts +143 -0
  37. package/convex/sandbox/_generated/server.js +93 -0
  38. package/convex/sandbox/actions/bash.ts +130 -0
  39. package/convex/sandbox/actions/browser.ts +282 -0
  40. package/convex/sandbox/actions/file.ts +336 -0
  41. package/convex/sandbox/actions/lsp.ts +325 -0
  42. package/convex/sandbox/actions/pdf.ts +119 -0
  43. package/convex/sandbox/agent/codeExecLoop.ts +535 -0
  44. package/convex/sandbox/agent/decisions.ts +284 -0
  45. package/convex/sandbox/agent/index.ts +515 -0
  46. package/convex/sandbox/agent/subagents.ts +651 -0
  47. package/convex/sandbox/brandResearch/index.ts +417 -0
  48. package/convex/sandbox/context/index.ts +7 -0
  49. package/convex/sandbox/context/session.ts +402 -0
  50. package/convex/sandbox/convex.config.ts +17 -0
  51. package/convex/sandbox/index.ts +51 -0
  52. package/convex/sandbox/nodeActions/codeExec.ts +130 -0
  53. package/convex/sandbox/planning/beads.ts +187 -0
  54. package/convex/sandbox/planning/index.ts +8 -0
  55. package/convex/sandbox/planning/sync.ts +194 -0
  56. package/convex/sandbox/prompts/codeExec.ts +852 -0
  57. package/convex/sandbox/prompts/modes.ts +231 -0
  58. package/convex/sandbox/prompts/system.ts +142 -0
  59. package/convex/sandbox/schema.ts +510 -0
  60. package/convex/sandbox/state/artifacts.ts +99 -0
  61. package/convex/sandbox/state/checkpoints.ts +341 -0
  62. package/convex/sandbox/state/files.ts +383 -0
  63. package/convex/sandbox/state/index.ts +10 -0
  64. package/convex/sandbox/state/verification.actions.ts +268 -0
  65. package/convex/sandbox/state/verification.ts +101 -0
  66. package/convex/sandbox/tsconfig.json +25 -0
  67. package/convex/sandbox/utils/codeExecHelpers.ts +52 -0
  68. package/dist/cli/commands/build.d.ts +19 -0
  69. package/dist/cli/commands/build.d.ts.map +1 -0
  70. package/dist/cli/commands/build.js +223 -0
  71. package/dist/cli/commands/init.d.ts +16 -0
  72. package/dist/cli/commands/init.d.ts.map +1 -0
  73. package/dist/cli/commands/init.js +148 -0
  74. package/dist/cli/commands/publish.d.ts +12 -0
  75. package/dist/cli/commands/publish.d.ts.map +1 -0
  76. package/dist/cli/commands/publish.js +33 -0
  77. package/dist/cli/index.d.ts +14 -0
  78. package/dist/cli/index.d.ts.map +1 -0
  79. package/dist/cli/index.js +40 -0
  80. package/dist/sdk/builders.d.ts +104 -0
  81. package/dist/sdk/builders.d.ts.map +1 -0
  82. package/dist/sdk/builders.js +214 -0
  83. package/dist/sdk/index.d.ts +29 -0
  84. package/dist/sdk/index.d.ts.map +1 -0
  85. package/dist/sdk/index.js +38 -0
  86. package/dist/sdk/types.d.ts +107 -0
  87. package/dist/sdk/types.d.ts.map +1 -0
  88. package/dist/sdk/types.js +6 -0
  89. package/ksa/README.md +263 -0
  90. package/ksa/_generated/REFERENCE.md +2954 -0
  91. package/ksa/_generated/registry.ts +257 -0
  92. package/ksa/_shared/configReader.ts +302 -0
  93. package/ksa/_shared/configSchemas.ts +649 -0
  94. package/ksa/_shared/gateway.ts +175 -0
  95. package/ksa/_shared/ksaBehaviors.ts +411 -0
  96. package/ksa/_shared/ksaProxy.ts +248 -0
  97. package/ksa/_shared/localDb.ts +302 -0
  98. package/ksa/index.ts +134 -0
  99. package/package.json +93 -0
  100. package/runtime/browser/agent-browser.ts +330 -0
  101. package/runtime/entrypoint.ts +194 -0
  102. package/runtime/lsp/manager.ts +366 -0
  103. package/runtime/pdf/pdf-generator.ts +50 -0
  104. package/runtime/pdf/renderer.ts +357 -0
  105. package/runtime/pdf/schema.ts +97 -0
  106. package/runtime/services/file-watcher.ts +191 -0
  107. package/template/build.ts +307 -0
  108. package/template/e2b/Dockerfile +69 -0
  109. package/template/e2b/e2b.toml +13 -0
  110. package/template/e2b/prebuild.sh +68 -0
  111. package/template/e2b/start.sh +14 -0
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Sandbox Compiler
3
+ *
4
+ * Manages compilation manifest for sandbox definitions.
5
+ */
6
+
7
+ import { v } from "convex/values";
8
+ import { query, internalMutation } from "../_generated/server";
9
+
10
+ // ============================================
11
+ // Manifest Management
12
+ // ============================================
13
+
14
+ /** Save compilation manifest */
15
+ export const saveManifest = internalMutation({
16
+ args: {
17
+ version: v.string(),
18
+ manifest: v.array(v.object({
19
+ type: v.string(),
20
+ name: v.string(),
21
+ path: v.string(),
22
+ })),
23
+ },
24
+ handler: async (ctx, args) => {
25
+ // Clear old entries for this version
26
+ const existing = await ctx.db
27
+ .query("compiledSandbox")
28
+ .withIndex("by_version", (q) => q.eq("version", args.version))
29
+ .collect();
30
+
31
+ for (const entry of existing) {
32
+ await ctx.db.delete(entry._id);
33
+ }
34
+
35
+ // Insert new entries
36
+ for (const item of args.manifest) {
37
+ await ctx.db.insert("compiledSandbox", {
38
+ version: args.version,
39
+ type: item.type as "tool" | "skill" | "agent" | "service",
40
+ name: item.name,
41
+ r2Key: item.path,
42
+ contentHash: "",
43
+ createdAt: Date.now(),
44
+ });
45
+ }
46
+
47
+ return { saved: args.manifest.length };
48
+ },
49
+ });
50
+
51
+ /** Get the latest compiled manifest */
52
+ export const getManifest = query({
53
+ args: {
54
+ version: v.optional(v.string()),
55
+ },
56
+ handler: async (ctx, args) => {
57
+ if (args.version) {
58
+ return await ctx.db
59
+ .query("compiledSandbox")
60
+ .withIndex("by_version", (q) => q.eq("version", args.version!))
61
+ .collect();
62
+ }
63
+
64
+ // Get latest version
65
+ const latest = await ctx.db
66
+ .query("compiledSandbox")
67
+ .order("desc")
68
+ .first();
69
+
70
+ if (!latest) return [];
71
+
72
+ return await ctx.db
73
+ .query("compiledSandbox")
74
+ .withIndex("by_version", (q) => q.eq("version", latest.version))
75
+ .collect();
76
+ },
77
+ });
78
+
79
+ /** Get manifest version info */
80
+ export const getLatestVersion = query({
81
+ args: {},
82
+ handler: async (ctx) => {
83
+ const latest = await ctx.db
84
+ .query("compiledSandbox")
85
+ .order("desc")
86
+ .first();
87
+
88
+ return latest?.version ?? null;
89
+ },
90
+ });
91
+
92
+ // ============================================
93
+ // Custom Tools Queries
94
+ // ============================================
95
+
96
+ /** List custom tools (for compilation) */
97
+ export const listCustomTools = query({
98
+ args: {},
99
+ handler: async (ctx) => {
100
+ return await ctx.db
101
+ .query("customTools")
102
+ .filter((q) => q.eq(q.field("enabled"), true))
103
+ .collect();
104
+ },
105
+ });
106
+
107
+ /** Get tool implementation for a custom tool */
108
+ export const getToolImplementation = query({
109
+ args: { toolId: v.string() },
110
+ handler: async (ctx, args) => {
111
+ const tool = await ctx.db
112
+ .query("customTools")
113
+ .withIndex("by_toolId", (q) => q.eq("toolId", args.toolId))
114
+ .first();
115
+
116
+ return tool?.implementation || null;
117
+ },
118
+ });
119
+
120
+ // ============================================
121
+ // Built-in Metadata Queries (deprecated - metadata now in Lakitu)
122
+ // ============================================
123
+
124
+ /** Get all built-in tool metadata */
125
+ export const getBuiltInTools = query({
126
+ args: {},
127
+ handler: async () => [],
128
+ });
129
+
130
+ /** Get all built-in skill metadata */
131
+ export const getBuiltInSkills = query({
132
+ args: {},
133
+ handler: async () => [],
134
+ });
135
+
136
+ /** Get all built-in deliverable metadata */
137
+ export const getBuiltInDeliverables = query({
138
+ args: {},
139
+ handler: async () => [],
140
+ });
141
+
142
+ /** Get all agent definitions */
143
+ export const getAgents = query({
144
+ args: {},
145
+ handler: async () => [],
146
+ });
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Board Planning Workflow - AI-assisted board creation
3
+ *
4
+ * Spawns an agent with board-manager skill to generate a plan,
5
+ * then executes the plan on user approval.
6
+ */
7
+
8
+ import { v } from "convex/values";
9
+ import { action, mutation, query, internalAction } from "../_generated/server";
10
+ import { api, internal } from "../_generated/api";
11
+
12
+ // ============================================
13
+ // Schema
14
+ // ============================================
15
+
16
+ const boardPlanSchema = v.object({
17
+ title: v.string(),
18
+ description: v.string(),
19
+ stages: v.array(v.object({
20
+ name: v.string(),
21
+ type: v.union(v.literal("agent"), v.literal("human")),
22
+ description: v.string(),
23
+ skillId: v.optional(v.string()),
24
+ order: v.number(),
25
+ })),
26
+ });
27
+
28
+ // ============================================
29
+ // Generate Plan - Spawns agent to create plan
30
+ // ============================================
31
+
32
+ export const generatePlan = action({
33
+ args: {
34
+ workspaceId: v.string(),
35
+ userPrompt: v.string(),
36
+ useOpenCode: v.optional(v.boolean()), // Feature flag for new OpenCode sandbox
37
+ },
38
+ handler: async (ctx, args) => {
39
+ // Use OpenCode by default for board planning (more reliable)
40
+ const useOpenCode = args.useOpenCode !== false;
41
+
42
+ // Build prompt - OpenCode uses built-in file editing
43
+ const prompt = useOpenCode
44
+ ? `Create a workflow board for: ${args.userPrompt}
45
+
46
+ Write a JSON file to /home/user/workspace/board_plan.json with this structure:
47
+ {
48
+ "title": "Board name",
49
+ "description": "What this workflow accomplishes",
50
+ "stages": [
51
+ {"name": "Stage Name", "type": "agent" or "human", "description": "What happens", "order": 0}
52
+ ]
53
+ }
54
+
55
+ Rules:
56
+ - Create 3-6 stages
57
+ - First stage: usually "human" (user provides input)
58
+ - Middle stages: "agent" (AI automated tasks)
59
+ - Last stage: "human" (review) or "agent" (deliver result)`
60
+ : args.userPrompt;
61
+
62
+ const systemPrompt = useOpenCode ? undefined : `You are CodeMode. Create workflow boards.
63
+
64
+ ## Rules
65
+ - Create 3-6 stages
66
+ - First stage: usually "human" (user input)
67
+ - Middle: "agent" stages with relevant skills
68
+ - Last: "human" (review) or "agent" (deliver)
69
+
70
+ Output a JSON plan with title, description, and stages array.`;
71
+
72
+ try {
73
+ // Start Lakitu sandbox session
74
+ const result = await ctx.runAction(api.workflows.sandboxConvex.startSession, {
75
+ projectId: args.workspaceId,
76
+ prompt,
77
+ config: {
78
+ model: "anthropic/claude-3.5-haiku",
79
+ purpose: "board-planning",
80
+ },
81
+ });
82
+
83
+ if (!result.success) {
84
+ return { success: false, error: result.error || "Failed to start session" };
85
+ }
86
+
87
+ const sessionId = result.sessionId;
88
+
89
+ // Wait for completion (poll session status)
90
+ let session: any = null;
91
+ for (let i = 0; i < 120; i++) { // 2 min max for planning
92
+ await new Promise(r => setTimeout(r, 1000));
93
+ session = await ctx.runQuery(api.workflows.sandboxConvex.getSession, { sessionId });
94
+
95
+ if (session?.status === "completed" || session?.status === "failed") {
96
+ break;
97
+ }
98
+ }
99
+
100
+ if (!session || session.status !== "completed") {
101
+ return { success: false, sessionId, error: "Timeout or session failed" };
102
+ }
103
+
104
+ const output = session.output as any;
105
+
106
+ // Try to parse plan from response
107
+ const response = output?.response || "";
108
+ if (response) {
109
+ const jsonMatch = response.match(/\{[\s\S]*"title"[\s\S]*"stages"[\s\S]*\}/);
110
+ if (jsonMatch) {
111
+ try {
112
+ const plan = JSON.parse(jsonMatch[0]);
113
+ if (plan.title && plan.stages) {
114
+ return { success: true, sessionId, plan };
115
+ }
116
+ } catch {}
117
+ }
118
+ }
119
+
120
+ // Try to find plan in tool call results
121
+ const toolCalls = output?.toolCalls || [];
122
+ for (const tc of toolCalls) {
123
+ if (tc.result && typeof tc.result === "string") {
124
+ const jsonMatch = tc.result.match(/\{[\s\S]*"title"[\s\S]*"stages"[\s\S]*\}/);
125
+ if (jsonMatch) {
126
+ try {
127
+ const plan = JSON.parse(jsonMatch[0]);
128
+ if (plan.title && plan.stages) {
129
+ return { success: true, sessionId, plan };
130
+ }
131
+ } catch {}
132
+ }
133
+ }
134
+ }
135
+
136
+ return {
137
+ success: false,
138
+ sessionId,
139
+ error: "Agent did not generate a structured plan. Please try again.",
140
+ debug: { response: response?.substring(0, 500), toolCalls: toolCalls?.length },
141
+ };
142
+ } catch (error: any) {
143
+ console.error("[generatePlan] Error:", error);
144
+ return {
145
+ success: false,
146
+ error: error.message,
147
+ };
148
+ }
149
+ },
150
+ });
151
+
152
+ // ============================================
153
+ // Execute Plan - Creates board from approved plan
154
+ // ============================================
155
+
156
+ export const executePlan = action({
157
+ args: {
158
+ workspaceId: v.string(),
159
+ plan: boardPlanSchema,
160
+ },
161
+ handler: async (ctx, args) => {
162
+ const { workspaceId, plan } = args;
163
+
164
+ try {
165
+ // Create board
166
+ const boardId = await ctx.runMutation(api.features.kanban.boards.create, {
167
+ workspaceId,
168
+ name: plan.title,
169
+ description: plan.description,
170
+ });
171
+
172
+ // Create tasks for each stage
173
+ const taskIds: string[] = [];
174
+ for (const stage of plan.stages) {
175
+ const taskId = await ctx.runMutation(api.features.kanban.boards.addTask, {
176
+ boardId,
177
+ name: stage.name,
178
+ description: stage.description,
179
+ stageType: stage.type,
180
+ skillId: stage.skillId,
181
+ order: stage.order,
182
+ });
183
+ taskIds.push(taskId);
184
+ }
185
+
186
+ return {
187
+ success: true,
188
+ boardId,
189
+ taskIds,
190
+ };
191
+ } catch (error: any) {
192
+ return {
193
+ success: false,
194
+ error: error.message,
195
+ };
196
+ }
197
+ },
198
+ });
199
+
200
+ // ============================================
201
+ // Stream-based generation (for real-time UI)
202
+ // ============================================
203
+
204
+ export const streamGeneratePlan = internalAction({
205
+ args: {
206
+ workspaceId: v.string(),
207
+ userPrompt: v.string(),
208
+ planSessionId: v.string(), // For streaming updates
209
+ },
210
+ handler: async (ctx, args) => {
211
+ // This allows streaming updates to the UI via the session
212
+ return ctx.runAction(internal.workflows.create.board.generatePlan, {
213
+ workspaceId: args.workspaceId,
214
+ userPrompt: args.userPrompt,
215
+ });
216
+ },
217
+ });
@@ -0,0 +1,262 @@
1
+ /**
2
+ * KSA CRUD Operations
3
+ *
4
+ * Provides queries for the KSA registry to the frontend.
5
+ * KSAs are defined in ksaPolicy.ts - this just exposes them.
6
+ */
7
+
8
+ import { query } from "../_generated/server";
9
+ import { v } from "convex/values";
10
+ import {
11
+ KSA_REGISTRY,
12
+ CONFIG_SCHEMAS,
13
+ CONFIG_DEFAULTS,
14
+ CORE_KSAS,
15
+ CATEGORY_LABELS,
16
+ CATEGORY_DESCRIPTIONS,
17
+ GROUP_LABELS,
18
+ GROUP_DESCRIPTIONS,
19
+ GROUP_ICONS,
20
+ GROUP_ORDER,
21
+ getKSA,
22
+ getKSAsByCategory,
23
+ getKSAsByGroup,
24
+ getSkillsByGroup,
25
+ getKSAsByNames,
26
+ getDefaultKSAs,
27
+ getConfigSchema,
28
+ getConfigDefaults,
29
+ mergeWithDefaults,
30
+ searchKSAs,
31
+ type KSAInfo,
32
+ type KSACategory,
33
+ type KSAGroup,
34
+ type ConfigField,
35
+ } from "../ksaPolicy";
36
+
37
+ /**
38
+ * List all available KSAs.
39
+ */
40
+ export const list = query({
41
+ args: {
42
+ category: v.optional(v.union(v.literal("core"), v.literal("skills"), v.literal("deliverables"))),
43
+ },
44
+ handler: async (_ctx, args): Promise<KSAInfo[]> => {
45
+ if (args.category) {
46
+ return getKSAsByCategory(args.category);
47
+ }
48
+ return KSA_REGISTRY;
49
+ },
50
+ });
51
+
52
+ /**
53
+ * Get a single KSA by name.
54
+ */
55
+ export const get = query({
56
+ args: { name: v.string() },
57
+ handler: async (_ctx, args): Promise<KSAInfo | null> => {
58
+ return getKSA(args.name) || null;
59
+ },
60
+ });
61
+
62
+ /**
63
+ * Get multiple KSAs by names.
64
+ */
65
+ export const getByNames = query({
66
+ args: { names: v.array(v.string()) },
67
+ handler: async (_ctx, args): Promise<KSAInfo[]> => {
68
+ return getKSAsByNames(args.names);
69
+ },
70
+ });
71
+
72
+ /**
73
+ * Get KSAs grouped by category.
74
+ */
75
+ export const listGrouped = query({
76
+ args: {},
77
+ handler: async (): Promise<{
78
+ core: KSAInfo[];
79
+ skills: KSAInfo[];
80
+ deliverables: KSAInfo[];
81
+ }> => {
82
+ return {
83
+ core: getKSAsByCategory("core"),
84
+ skills: getKSAsByCategory("skills"),
85
+ deliverables: getKSAsByCategory("deliverables"),
86
+ };
87
+ },
88
+ });
89
+
90
+ /**
91
+ * Get category metadata.
92
+ */
93
+ export const getCategories = query({
94
+ args: {},
95
+ handler: async (): Promise<
96
+ Array<{
97
+ id: KSACategory;
98
+ label: string;
99
+ description: string;
100
+ ksaCount: number;
101
+ }>
102
+ > => {
103
+ const categories: KSACategory[] = ["core", "skills", "deliverables"];
104
+ return categories.map((id) => ({
105
+ id,
106
+ label: CATEGORY_LABELS[id],
107
+ description: CATEGORY_DESCRIPTIONS[id],
108
+ ksaCount: getKSAsByCategory(id).length,
109
+ }));
110
+ },
111
+ });
112
+
113
+ /**
114
+ * Get the names of core KSAs (always available).
115
+ */
116
+ export const getCoreKSANames = query({
117
+ args: {},
118
+ handler: async (): Promise<string[]> => {
119
+ return CORE_KSAS;
120
+ },
121
+ });
122
+
123
+ /**
124
+ * Get a default set of KSAs for a given purpose.
125
+ */
126
+ export const getDefaultKSASet = query({
127
+ args: {
128
+ purpose: v.union(
129
+ v.literal("research"),
130
+ v.literal("content"),
131
+ v.literal("automation"),
132
+ v.literal("minimal"),
133
+ v.literal("all")
134
+ ),
135
+ },
136
+ handler: async (_ctx, args): Promise<string[]> => {
137
+ return getDefaultKSAs(args.purpose);
138
+ },
139
+ });
140
+
141
+ // ============================================================================
142
+ // Config Schema Queries
143
+ // ============================================================================
144
+
145
+ /**
146
+ * Get config schema for a KSA.
147
+ */
148
+ export const getKSAConfigSchema = query({
149
+ args: { name: v.string() },
150
+ handler: async (_ctx, args): Promise<Record<string, ConfigField> | null> => {
151
+ return getConfigSchema(args.name) || null;
152
+ },
153
+ });
154
+
155
+ /**
156
+ * Get default config for a KSA.
157
+ */
158
+ export const getKSADefaults = query({
159
+ args: { name: v.string() },
160
+ handler: async (_ctx, args): Promise<Record<string, unknown> | null> => {
161
+ return getConfigDefaults(args.name) || null;
162
+ },
163
+ });
164
+
165
+ /**
166
+ * Get all config schemas.
167
+ */
168
+ export const getAllConfigSchemas = query({
169
+ args: {},
170
+ handler: async (): Promise<Record<string, Record<string, ConfigField>>> => {
171
+ return CONFIG_SCHEMAS;
172
+ },
173
+ });
174
+
175
+ /**
176
+ * Merge user config with defaults for a KSA.
177
+ */
178
+ export const getMergedConfig = query({
179
+ args: {
180
+ name: v.string(),
181
+ userConfig: v.any(),
182
+ },
183
+ handler: async (_ctx, args): Promise<Record<string, unknown>> => {
184
+ return mergeWithDefaults(args.name, args.userConfig || {});
185
+ },
186
+ });
187
+
188
+ // ============================================================================
189
+ // Search
190
+ // ============================================================================
191
+
192
+ /**
193
+ * Search KSAs by keyword.
194
+ */
195
+ export const search = query({
196
+ args: { keyword: v.string() },
197
+ handler: async (_ctx, args): Promise<KSAInfo[]> => {
198
+ return searchKSAs(args.keyword);
199
+ },
200
+ });
201
+
202
+ /**
203
+ * Get KSAs grouped for the library panel.
204
+ */
205
+ export const listForLibrary = query({
206
+ args: {},
207
+ handler: async (): Promise<{
208
+ byCategory: {
209
+ core: KSAInfo[];
210
+ skills: KSAInfo[];
211
+ deliverables: KSAInfo[];
212
+ };
213
+ skillsByGroup: Record<KSAGroup, KSAInfo[]>;
214
+ groupOrder: KSAGroup[];
215
+ }> => {
216
+ return {
217
+ byCategory: {
218
+ core: getKSAsByCategory("core"),
219
+ skills: getKSAsByCategory("skills"),
220
+ deliverables: getKSAsByCategory("deliverables"),
221
+ },
222
+ skillsByGroup: getSkillsByGroup(),
223
+ groupOrder: GROUP_ORDER,
224
+ };
225
+ },
226
+ });
227
+
228
+ /**
229
+ * Get group metadata for skills subcategories.
230
+ */
231
+ export const getGroups = query({
232
+ args: {},
233
+ handler: async (): Promise<
234
+ Array<{
235
+ id: KSAGroup;
236
+ label: string;
237
+ description: string;
238
+ icon: string;
239
+ ksaCount: number;
240
+ }>
241
+ > => {
242
+ return GROUP_ORDER.map((id) => ({
243
+ id,
244
+ label: GROUP_LABELS[id],
245
+ description: GROUP_DESCRIPTIONS[id],
246
+ icon: GROUP_ICONS[id],
247
+ ksaCount: getKSAsByGroup(id).length,
248
+ }));
249
+ },
250
+ });
251
+
252
+ /**
253
+ * Get KSAs by group.
254
+ */
255
+ export const listByGroup = query({
256
+ args: {
257
+ group: v.literal("research"),
258
+ },
259
+ handler: async (_ctx, args): Promise<KSAInfo[]> => {
260
+ return getKSAsByGroup(args.group);
261
+ },
262
+ });