@sweny-ai/core 0.1.10 → 0.1.12

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.
package/dist/cli/main.js CHANGED
@@ -9,6 +9,7 @@ import chalk from "chalk";
9
9
  import { execute } from "../executor.js";
10
10
  import { triageWorkflow } from "../workflows/triage.js";
11
11
  import { implementWorkflow } from "../workflows/implement.js";
12
+ import { seedContentWorkflow } from "../workflows/seed-content.js";
12
13
  import { consoleLogger } from "../types.js";
13
14
  import { ClaudeClient } from "../claude.js";
14
15
  import { createSkillMap, configuredSkills } from "../skills/index.js";
@@ -579,25 +580,38 @@ export async function workflowRunAction(file, options) {
579
580
  }
580
581
  };
581
582
  const observer = composeObservers(wfProgressObserver, options.stream ? createStreamObserver() : undefined);
582
- // Build workflow input from config
583
- const workflowInput = {
584
- timeRange: config.timeRange,
585
- severityFocus: config.severityFocus,
586
- serviceFilter: config.serviceFilter,
587
- repository: config.repository,
588
- dryRun: config.dryRun,
589
- baseBranch: config.baseBranch,
590
- prLabels: config.prLabels,
591
- additionalInstructions: config.additionalInstructions,
592
- observabilityProvider: config.observabilityProvider,
593
- ...(config.observabilityCredentials.sourceId && {
594
- betterstackSourceId: config.observabilityCredentials.sourceId,
595
- }),
596
- ...(config.observabilityCredentials.tableName && {
597
- betterstackTableName: config.observabilityCredentials.tableName,
598
- }),
599
- context: buildProviderCtx(config, mcpServers),
600
- };
583
+ // Build workflow input — prefer --input JSON if provided, else fall back to config-derived input
584
+ let workflowInput;
585
+ if (options.input && typeof options.input === "string") {
586
+ try {
587
+ workflowInput = JSON.parse(options.input);
588
+ }
589
+ catch {
590
+ console.error(chalk.red(" --input must be valid JSON"));
591
+ process.exit(1);
592
+ return;
593
+ }
594
+ }
595
+ else {
596
+ workflowInput = {
597
+ timeRange: config.timeRange,
598
+ severityFocus: config.severityFocus,
599
+ serviceFilter: config.serviceFilter,
600
+ repository: config.repository,
601
+ dryRun: config.dryRun,
602
+ baseBranch: config.baseBranch,
603
+ prLabels: config.prLabels,
604
+ additionalInstructions: config.additionalInstructions,
605
+ observabilityProvider: config.observabilityProvider,
606
+ ...(config.observabilityCredentials.sourceId && {
607
+ betterstackSourceId: config.observabilityCredentials.sourceId,
608
+ }),
609
+ ...(config.observabilityCredentials.tableName && {
610
+ betterstackTableName: config.observabilityCredentials.tableName,
611
+ }),
612
+ context: buildProviderCtx(config, mcpServers),
613
+ };
614
+ }
601
615
  try {
602
616
  const results = await execute(workflow, workflowInput, {
603
617
  skills,
@@ -633,8 +647,11 @@ export function workflowExportAction(name) {
633
647
  else if (name === "implement") {
634
648
  workflow = implementWorkflow;
635
649
  }
650
+ else if (name === "seed-content") {
651
+ workflow = seedContentWorkflow;
652
+ }
636
653
  else {
637
- console.error(chalk.red(` Unknown workflow "${name}". Available: triage, implement`));
654
+ console.error(chalk.red(` Unknown workflow "${name}". Available: triage, implement, seed-content`));
638
655
  process.exit(1);
639
656
  return;
640
657
  }
@@ -695,6 +712,7 @@ workflowCmd
695
712
  .option("--dry-run", "Validate workflow without running")
696
713
  .option("--json", "Output result as JSON on stdout; suppress progress output")
697
714
  .option("--stream", "Stream NDJSON events to stdout (for Studio / automation)")
715
+ .option("--input <json>", "JSON string of input data to pass to the workflow")
698
716
  .action(workflowRunAction);
699
717
  workflowCmd
700
718
  .command("export <name>")
package/dist/index.d.ts CHANGED
@@ -27,7 +27,7 @@ export { execute } from "./executor.js";
27
27
  export type { ExecuteOptions } from "./executor.js";
28
28
  export { ClaudeClient } from "./claude.js";
29
29
  export type { ClaudeClientOptions } from "./claude.js";
30
- export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredSkills, validateWorkflowSkills, } from "./skills/index.js";
30
+ export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredSkills, validateWorkflowSkills, } from "./skills/index.js";
31
31
  export type { SkillValidationResult } from "./skills/index.js";
32
32
  export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
33
33
  export type { WorkflowError } from "./schema.js";
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ export { execute } from "./executor.js";
27
27
  // Claude client
28
28
  export { ClaudeClient } from "./claude.js";
29
29
  // Skills
30
- export { github, linear, slack, sentry, datadog, notification, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredSkills, validateWorkflowSkills, } from "./skills/index.js";
30
+ export { github, linear, slack, sentry, datadog, notification, supabase, builtinSkills, createSkillMap, allSkills, isSkillConfigured, configuredSkills, validateWorkflowSkills, } from "./skills/index.js";
31
31
  // Schema & validation
32
32
  export { workflowZ, nodeZ, edgeZ, skillZ, parseWorkflow, validateWorkflow, workflowJsonSchema } from "./schema.js";
33
33
  // MCP auto-injection
@@ -11,8 +11,9 @@ import { sentry } from "./sentry.js";
11
11
  import { datadog } from "./datadog.js";
12
12
  import { betterstack } from "./betterstack.js";
13
13
  import { notification } from "./notification.js";
14
+ import { supabase } from "./supabase.js";
14
15
  export declare const builtinSkills: Skill[];
15
- export { github, linear, slack, sentry, datadog, betterstack, notification };
16
+ export { github, linear, slack, sentry, datadog, betterstack, notification, supabase };
16
17
  /**
17
18
  * Build a skill map from an array of skills.
18
19
  * Pass to `execute()` as the `skills` option.
@@ -10,9 +10,10 @@ import { sentry } from "./sentry.js";
10
10
  import { datadog } from "./datadog.js";
11
11
  import { betterstack } from "./betterstack.js";
12
12
  import { notification } from "./notification.js";
13
+ import { supabase } from "./supabase.js";
13
14
  // ─── Built-in skill catalog ─────────────────────────────────────
14
- export const builtinSkills = [github, linear, slack, sentry, datadog, betterstack, notification];
15
- export { github, linear, slack, sentry, datadog, betterstack, notification };
15
+ export const builtinSkills = [github, linear, slack, sentry, datadog, betterstack, notification, supabase];
16
+ export { github, linear, slack, sentry, datadog, betterstack, notification, supabase };
16
17
  // ─── Registry helpers ───────────────────────────────────────────
17
18
  /**
18
19
  * Build a skill map from an array of skills.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Supabase Skill
3
+ *
4
+ * Query, insert, update, and manage data in a Supabase project.
5
+ * Uses the Supabase REST API (PostgREST) for data operations
6
+ * and the Management API for project-level operations.
7
+ */
8
+ import type { Skill } from "../types.js";
9
+ export declare const supabase: Skill;
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Supabase Skill
3
+ *
4
+ * Query, insert, update, and manage data in a Supabase project.
5
+ * Uses the Supabase REST API (PostgREST) for data operations
6
+ * and the Management API for project-level operations.
7
+ */
8
+ async function supabaseRest(path, ctx, init) {
9
+ const url = ctx.config.SUPABASE_URL;
10
+ const key = ctx.config.SUPABASE_SERVICE_ROLE_KEY;
11
+ const res = await fetch(`${url}/rest/v1${path}`, {
12
+ ...init,
13
+ headers: {
14
+ apikey: key,
15
+ Authorization: `Bearer ${key}`,
16
+ "Content-Type": "application/json",
17
+ Prefer: init?.method === "POST" ? "return=representation" : "return=representation",
18
+ ...init?.headers,
19
+ },
20
+ signal: AbortSignal.timeout(30_000),
21
+ });
22
+ if (!res.ok) {
23
+ throw new Error(`[Supabase] REST API failed (HTTP ${res.status}): ${await res.text()}`);
24
+ }
25
+ const text = await res.text();
26
+ return text ? JSON.parse(text) : null;
27
+ }
28
+ async function supabaseEdgeFunction(functionName, body, ctx) {
29
+ const url = ctx.config.SUPABASE_URL;
30
+ const key = ctx.config.SUPABASE_SERVICE_ROLE_KEY;
31
+ const res = await fetch(`${url}/functions/v1/${functionName}`, {
32
+ method: "POST",
33
+ headers: {
34
+ Authorization: `Bearer ${key}`,
35
+ "Content-Type": "application/json",
36
+ },
37
+ body: JSON.stringify(body),
38
+ signal: AbortSignal.timeout(60_000),
39
+ });
40
+ if (!res.ok) {
41
+ throw new Error(`[Supabase] Edge function "${functionName}" failed (HTTP ${res.status}): ${await res.text()}`);
42
+ }
43
+ return res.json();
44
+ }
45
+ export const supabase = {
46
+ id: "supabase",
47
+ name: "Supabase",
48
+ description: "Query, insert, update data and invoke edge functions on a Supabase project",
49
+ category: "general",
50
+ config: {
51
+ SUPABASE_URL: {
52
+ description: "Supabase project URL (e.g., https://xxxx.supabase.co)",
53
+ required: true,
54
+ env: "SUPABASE_URL",
55
+ },
56
+ SUPABASE_SERVICE_ROLE_KEY: {
57
+ description: "Supabase service role key (full access, server-side only)",
58
+ required: true,
59
+ env: "SUPABASE_SERVICE_ROLE_KEY",
60
+ },
61
+ },
62
+ tools: [
63
+ // ─── Read ────────────────────────────────────────────────
64
+ {
65
+ name: "supabase_query",
66
+ description: "Query rows from a Supabase table. Uses PostgREST query syntax. " + "Returns an array of matching rows.",
67
+ input_schema: {
68
+ type: "object",
69
+ properties: {
70
+ table: { type: "string", description: "Table name (e.g., 'games', 'profiles')" },
71
+ select: {
72
+ type: "string",
73
+ description: "Columns to select (PostgREST syntax). Default: '*'. Examples: 'id,title,subject', 'id,title,course_modules(id,title)'",
74
+ },
75
+ filters: {
76
+ type: "array",
77
+ description: "PostgREST filter strings. Examples: ['subject=eq.math', 'difficulty=eq.easy', 'created_at=gte.2026-01-01']",
78
+ items: { type: "string" },
79
+ },
80
+ order: {
81
+ type: "string",
82
+ description: "Order by clause. Examples: 'created_at.desc', 'title.asc'",
83
+ },
84
+ limit: {
85
+ type: "number",
86
+ description: "Maximum rows to return. Default: 100",
87
+ },
88
+ },
89
+ required: ["table"],
90
+ },
91
+ handler: async (input, ctx) => {
92
+ const params = new URLSearchParams();
93
+ params.set("select", input.select || "*");
94
+ if (input.order)
95
+ params.set("order", input.order);
96
+ params.set("limit", String(input.limit || 100));
97
+ let path = `/${input.table}?${params}`;
98
+ if (input.filters) {
99
+ for (const f of input.filters) {
100
+ path += `&${f}`;
101
+ }
102
+ }
103
+ return supabaseRest(path, ctx);
104
+ },
105
+ },
106
+ {
107
+ name: "supabase_count",
108
+ description: "Count rows in a table, optionally with filters.",
109
+ input_schema: {
110
+ type: "object",
111
+ properties: {
112
+ table: { type: "string", description: "Table name" },
113
+ filters: {
114
+ type: "array",
115
+ description: "PostgREST filter strings",
116
+ items: { type: "string" },
117
+ },
118
+ },
119
+ required: ["table"],
120
+ },
121
+ handler: async (input, ctx) => {
122
+ const url = ctx.config.SUPABASE_URL;
123
+ const key = ctx.config.SUPABASE_SERVICE_ROLE_KEY;
124
+ let path = `${url}/rest/v1/${input.table}?select=count`;
125
+ if (input.filters) {
126
+ for (const f of input.filters) {
127
+ path += `&${f}`;
128
+ }
129
+ }
130
+ const res = await fetch(path, {
131
+ headers: {
132
+ apikey: key,
133
+ Authorization: `Bearer ${key}`,
134
+ Prefer: "count=exact",
135
+ },
136
+ signal: AbortSignal.timeout(15_000),
137
+ });
138
+ if (!res.ok) {
139
+ throw new Error(`[Supabase] Count failed: ${await res.text()}`);
140
+ }
141
+ const range = res.headers.get("content-range");
142
+ const count = range ? parseInt(range.split("/")[1], 10) : 0;
143
+ return { table: input.table, count };
144
+ },
145
+ },
146
+ // ─── Write ───────────────────────────────────────────────
147
+ {
148
+ name: "supabase_insert",
149
+ description: "Insert one or more rows into a table. Returns the inserted rows. " +
150
+ "Use upsert=true to update existing rows on conflict.",
151
+ input_schema: {
152
+ type: "object",
153
+ properties: {
154
+ table: { type: "string", description: "Table name" },
155
+ rows: {
156
+ type: "array",
157
+ description: "Array of row objects to insert",
158
+ items: { type: "object" },
159
+ },
160
+ upsert: {
161
+ type: "boolean",
162
+ description: "If true, update on conflict instead of failing. Default: false",
163
+ },
164
+ },
165
+ required: ["table", "rows"],
166
+ },
167
+ handler: async (input, ctx) => {
168
+ const headers = {};
169
+ if (input.upsert) {
170
+ headers["Prefer"] = "resolution=merge-duplicates,return=representation";
171
+ }
172
+ return supabaseRest(`/${input.table}`, ctx, {
173
+ method: "POST",
174
+ headers,
175
+ body: JSON.stringify(input.rows),
176
+ });
177
+ },
178
+ },
179
+ {
180
+ name: "supabase_update",
181
+ description: "Update rows matching filters. Returns the updated rows.",
182
+ input_schema: {
183
+ type: "object",
184
+ properties: {
185
+ table: { type: "string", description: "Table name" },
186
+ filters: {
187
+ type: "array",
188
+ description: "PostgREST filter strings to match rows (REQUIRED — prevents accidental full-table updates)",
189
+ items: { type: "string" },
190
+ },
191
+ data: {
192
+ type: "object",
193
+ description: "Fields to update",
194
+ },
195
+ },
196
+ required: ["table", "filters", "data"],
197
+ },
198
+ handler: async (input, ctx) => {
199
+ if (!input.filters || input.filters.length === 0) {
200
+ throw new Error("Filters are required for update operations to prevent accidental full-table updates");
201
+ }
202
+ let path = `/${input.table}`;
203
+ const filterParts = input.filters.map((f) => f);
204
+ path += `?${filterParts.join("&")}`;
205
+ return supabaseRest(path, ctx, {
206
+ method: "PATCH",
207
+ body: JSON.stringify(input.data),
208
+ });
209
+ },
210
+ },
211
+ {
212
+ name: "supabase_delete",
213
+ description: "Delete rows matching filters. Returns the deleted rows.",
214
+ input_schema: {
215
+ type: "object",
216
+ properties: {
217
+ table: { type: "string", description: "Table name" },
218
+ filters: {
219
+ type: "array",
220
+ description: "PostgREST filter strings (REQUIRED — prevents accidental full-table deletes)",
221
+ items: { type: "string" },
222
+ },
223
+ },
224
+ required: ["table", "filters"],
225
+ },
226
+ handler: async (input, ctx) => {
227
+ if (!input.filters || input.filters.length === 0) {
228
+ throw new Error("Filters are required for delete operations");
229
+ }
230
+ const path = `/${input.table}?${input.filters.join("&")}`;
231
+ return supabaseRest(path, ctx, { method: "DELETE" });
232
+ },
233
+ },
234
+ // ─── RPC ─────────────────────────────────────────────────
235
+ {
236
+ name: "supabase_rpc",
237
+ description: "Call a Supabase RPC (stored procedure / database function).",
238
+ input_schema: {
239
+ type: "object",
240
+ properties: {
241
+ function_name: { type: "string", description: "Name of the database function" },
242
+ params: {
243
+ type: "object",
244
+ description: "Parameters to pass to the function",
245
+ },
246
+ },
247
+ required: ["function_name"],
248
+ },
249
+ handler: async (input, ctx) => {
250
+ return supabaseRest(`/rpc/${input.function_name}`, ctx, {
251
+ method: "POST",
252
+ body: JSON.stringify(input.params || {}),
253
+ });
254
+ },
255
+ },
256
+ // ─── Edge Functions ──────────────────────────────────────
257
+ {
258
+ name: "supabase_invoke_function",
259
+ description: "Invoke a Supabase Edge Function by name with a JSON body.",
260
+ input_schema: {
261
+ type: "object",
262
+ properties: {
263
+ function_name: {
264
+ type: "string",
265
+ description: "Edge function name (e.g., 'agent-gateway', 'seed-content')",
266
+ },
267
+ body: {
268
+ type: "object",
269
+ description: "JSON body to send to the function",
270
+ },
271
+ },
272
+ required: ["function_name", "body"],
273
+ },
274
+ handler: async (input, ctx) => {
275
+ return supabaseEdgeFunction(input.function_name, input.body, ctx);
276
+ },
277
+ },
278
+ // ─── Auth Admin ──────────────────────────────────────────
279
+ {
280
+ name: "supabase_list_users",
281
+ description: "List auth users. Returns user IDs, emails, and metadata.",
282
+ input_schema: {
283
+ type: "object",
284
+ properties: {
285
+ page: { type: "number", description: "Page number (1-based). Default: 1" },
286
+ per_page: { type: "number", description: "Users per page. Default: 50" },
287
+ },
288
+ },
289
+ handler: async (input, ctx) => {
290
+ const url = ctx.config.SUPABASE_URL;
291
+ const key = ctx.config.SUPABASE_SERVICE_ROLE_KEY;
292
+ const page = input.page || 1;
293
+ const perPage = input.per_page || 50;
294
+ const res = await fetch(`${url}/auth/v1/admin/users?page=${page}&per_page=${perPage}`, {
295
+ headers: {
296
+ Authorization: `Bearer ${key}`,
297
+ apikey: key,
298
+ },
299
+ signal: AbortSignal.timeout(15_000),
300
+ });
301
+ if (!res.ok) {
302
+ throw new Error(`[Supabase] List users failed: ${await res.text()}`);
303
+ }
304
+ return res.json();
305
+ },
306
+ },
307
+ // ─── Schema Introspection ────────────────────────────────
308
+ {
309
+ name: "supabase_list_tables",
310
+ description: "List all public tables with row counts. Useful for content auditing.",
311
+ input_schema: {
312
+ type: "object",
313
+ properties: {},
314
+ },
315
+ handler: async (_input, ctx) => {
316
+ return supabaseRest("/rpc/get_table_counts", ctx, { method: "POST", body: JSON.stringify({}) }).catch(async () => {
317
+ // Fallback: query information_schema
318
+ const url = ctx.config.SUPABASE_URL;
319
+ const key = ctx.config.SUPABASE_SERVICE_ROLE_KEY;
320
+ const res = await fetch(`${url}/rest/v1/rpc/`, {
321
+ method: "POST",
322
+ headers: {
323
+ apikey: key,
324
+ Authorization: `Bearer ${key}`,
325
+ "Content-Type": "application/json",
326
+ },
327
+ body: JSON.stringify({}),
328
+ });
329
+ return { note: "get_table_counts RPC not available. Use supabase_query to check specific tables." };
330
+ });
331
+ },
332
+ },
333
+ ],
334
+ };
@@ -1,2 +1,3 @@
1
1
  export { triageWorkflow } from "./triage.js";
2
2
  export { implementWorkflow } from "./implement.js";
3
+ export { seedContentWorkflow } from "./seed-content.js";
@@ -1,2 +1,3 @@
1
1
  export { triageWorkflow } from "./triage.js";
2
2
  export { implementWorkflow } from "./implement.js";
3
+ export { seedContentWorkflow } from "./seed-content.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Content Generation Workflow
3
+ *
4
+ * A DAG that audits a Supabase-backed educational platform for content gaps,
5
+ * generates high-quality content using Claude, validates it, and publishes it.
6
+ *
7
+ * Designed for solo operators bootstrapping educational SaaS platforms.
8
+ * Uses Claude (via your existing subscription) for generation instead of
9
+ * burning API credits on external AI gateways.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { execute, ClaudeClient, createSkillMap, supabase } from '@sweny-ai/core'
14
+ * import { seedContentWorkflow } from '@sweny-ai/core/workflows'
15
+ *
16
+ * const skills = createSkillMap([supabase])
17
+ * const claude = new ClaudeClient()
18
+ *
19
+ * const results = await execute(seedContentWorkflow, {
20
+ * contentType: 'worksheet', // 'worksheet' | 'game' | 'lesson'
21
+ * subject: 'math',
22
+ * topic: 'fractions',
23
+ * difficulty: 'easy',
24
+ * gradeLevel: '3rd-grade',
25
+ * count: 5,
26
+ * }, { skills, claude })
27
+ * ```
28
+ */
29
+ import type { Workflow } from "../types.js";
30
+ export declare const seedContentWorkflow: Workflow;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Content Generation Workflow
3
+ *
4
+ * A DAG that audits a Supabase-backed educational platform for content gaps,
5
+ * generates high-quality content using Claude, validates it, and publishes it.
6
+ *
7
+ * Designed for solo operators bootstrapping educational SaaS platforms.
8
+ * Uses Claude (via your existing subscription) for generation instead of
9
+ * burning API credits on external AI gateways.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { execute, ClaudeClient, createSkillMap, supabase } from '@sweny-ai/core'
14
+ * import { seedContentWorkflow } from '@sweny-ai/core/workflows'
15
+ *
16
+ * const skills = createSkillMap([supabase])
17
+ * const claude = new ClaudeClient()
18
+ *
19
+ * const results = await execute(seedContentWorkflow, {
20
+ * contentType: 'worksheet', // 'worksheet' | 'game' | 'lesson'
21
+ * subject: 'math',
22
+ * topic: 'fractions',
23
+ * difficulty: 'easy',
24
+ * gradeLevel: '3rd-grade',
25
+ * count: 5,
26
+ * }, { skills, claude })
27
+ * ```
28
+ */
29
+ export const seedContentWorkflow = {
30
+ id: "seed-content",
31
+ name: "Content Generation Pipeline",
32
+ description: "Audit content gaps, generate educational content, validate quality, and publish to Supabase",
33
+ entry: "audit",
34
+ nodes: {
35
+ // ─── Step 1: Audit ─────────────────────────────────────
36
+ audit: {
37
+ name: "Audit Content Gaps",
38
+ instruction: `You are auditing an educational platform's content library.
39
+
40
+ Use supabase_count to check how many items exist for each combination of:
41
+ - Content types: worksheets, games, lessons
42
+ - Subjects: math, science, english, history, geography, economics, music, art, life-skills, foreign-language
43
+ - Difficulties: beginner, easy, medium, hard
44
+
45
+ Also check the input parameters — if the user specified a subject, topic, difficulty, and count,
46
+ skip the full audit and just report what they want generated.
47
+
48
+ Return a structured content plan: what to generate, how many, at what difficulty and grade level.
49
+ Prioritize gaps where a subject has ZERO content of a given type.`,
50
+ skills: ["supabase"],
51
+ output: {
52
+ type: "object",
53
+ properties: {
54
+ plan: {
55
+ type: "array",
56
+ items: {
57
+ type: "object",
58
+ properties: {
59
+ contentType: { type: "string" },
60
+ subject: { type: "string" },
61
+ topic: { type: "string" },
62
+ difficulty: { type: "string" },
63
+ gradeLevel: { type: "string" },
64
+ count: { type: "number" },
65
+ reason: { type: "string" },
66
+ },
67
+ required: ["contentType", "subject", "topic", "difficulty", "count"],
68
+ },
69
+ },
70
+ summary: { type: "string" },
71
+ },
72
+ required: ["plan", "summary"],
73
+ },
74
+ },
75
+ // ─── Step 2: Generate ──────────────────────────────────
76
+ generate: {
77
+ name: "Generate Content",
78
+ instruction: `You are an expert educator creating content for children ages 5-14.
79
+
80
+ For each item in the content plan from the previous step, generate the content directly.
81
+ The content must be high-quality, engaging, age-appropriate, and educationally sound.
82
+
83
+ **For worksheets:**
84
+ Generate a JSON array of problems. Each problem needs: "problem", "answer", "format" (answer-line | multiple-choice | write-on).
85
+ Requirements:
86
+ - Every answer MUST be correct. Double-check math, facts, spelling.
87
+ - Problems must progress in difficulty within the set.
88
+ - Use at least 2 different problem structures (e.g., equations + word problems for math).
89
+ - Vary phrasing — no two problems should feel copy-pasted.
90
+ - For the target grade level, use appropriate vocabulary and complexity.
91
+
92
+ **For lessons:**
93
+ Generate complete HTML + CSS + JS for an interactive lesson.
94
+ Requirements:
95
+ - Must have substantial educational content (not just a shell with navigation).
96
+ - Include interactive elements: quizzes, click-to-reveal, drag-and-drop.
97
+ - Must be visually engaging with colors, icons, and clear typography.
98
+ - Include practice exercises with immediate feedback.
99
+ - Minimum 5 teaching sections plus 2 practice activities.
100
+
101
+ **For games:**
102
+ Generate a description and educational requirements only (actual game code is generated
103
+ by the platform's Phaser-based generator).
104
+
105
+ Use supabase_insert to write each piece of content to the appropriate table.
106
+
107
+ Table schemas:
108
+ - worksheets: id (auto), title, subject, topic, difficulty, grade_level, num_questions, problems (jsonb), status='published', source='ai-seed'
109
+ - lessons: id (auto), title, subject, difficulty, grade_level, lesson_type, html_code, css_code, js_code, status='published', source='ai-seed'
110
+ - games: id (auto), title, subject, difficulty, grade_level, game_type, html_code, css_code, js_code, status='published', source='ai-seed'
111
+
112
+ Return a summary of what was generated and inserted.`,
113
+ skills: ["supabase"],
114
+ output: {
115
+ type: "object",
116
+ properties: {
117
+ generated: {
118
+ type: "array",
119
+ items: {
120
+ type: "object",
121
+ properties: {
122
+ contentType: { type: "string" },
123
+ title: { type: "string" },
124
+ subject: { type: "string" },
125
+ id: { type: "string" },
126
+ status: { type: "string" },
127
+ },
128
+ },
129
+ },
130
+ totalGenerated: { type: "number" },
131
+ errors: { type: "array", items: { type: "string" } },
132
+ },
133
+ required: ["generated", "totalGenerated"],
134
+ },
135
+ },
136
+ // ─── Step 3: Validate ──────────────────────────────────
137
+ validate: {
138
+ name: "Validate Content Quality",
139
+ instruction: `Review the content that was just generated and inserted.
140
+
141
+ For each item, use supabase_query to fetch it back and verify:
142
+
143
+ **Worksheets:**
144
+ - Parse the problems JSON — are all answers correct?
145
+ - Are problems diverse (not repetitive)?
146
+ - Is difficulty appropriate for the stated grade level?
147
+ - Are there the right number of questions?
148
+
149
+ **Lessons:**
150
+ - Is the HTML substantial (> 500 characters)?
151
+ - Does it contain actual educational content (not just navigation shells)?
152
+ - Are there interactive elements?
153
+
154
+ **Games:**
155
+ - Does the HTML contain a working game structure?
156
+
157
+ Flag any issues. If critical issues are found (wrong answers, empty content),
158
+ use supabase_update to set status='draft' and add a note.
159
+
160
+ Return a quality report.`,
161
+ skills: ["supabase"],
162
+ output: {
163
+ type: "object",
164
+ properties: {
165
+ totalReviewed: { type: "number" },
166
+ passed: { type: "number" },
167
+ flagged: { type: "number" },
168
+ issues: {
169
+ type: "array",
170
+ items: {
171
+ type: "object",
172
+ properties: {
173
+ id: { type: "string" },
174
+ title: { type: "string" },
175
+ issue: { type: "string" },
176
+ severity: { type: "string" },
177
+ action: { type: "string" },
178
+ },
179
+ },
180
+ },
181
+ summary: { type: "string" },
182
+ },
183
+ required: ["totalReviewed", "passed", "flagged", "summary"],
184
+ },
185
+ },
186
+ // ─── Step 4: Report ────────────────────────────────────
187
+ report: {
188
+ name: "Generate Report",
189
+ instruction: `Compile a final report of the content generation run.
190
+
191
+ Include:
192
+ - Total content generated by type and subject
193
+ - Quality validation results
194
+ - Any issues found and actions taken
195
+ - Recommendations for next content generation run (what gaps remain)
196
+
197
+ Format as a clean markdown summary that a solo operator can scan in 30 seconds.`,
198
+ skills: [],
199
+ output: {
200
+ type: "object",
201
+ properties: {
202
+ markdown: { type: "string" },
203
+ nextPriorities: {
204
+ type: "array",
205
+ items: { type: "string" },
206
+ },
207
+ },
208
+ required: ["markdown"],
209
+ },
210
+ },
211
+ },
212
+ edges: [
213
+ { from: "audit", to: "generate" },
214
+ { from: "generate", to: "validate" },
215
+ { from: "validate", to: "report" },
216
+ ],
217
+ };
@@ -104,9 +104,10 @@ Downstream nodes will act ONLY on novel findings. Duplicates will be +1'd automa
104
104
  4. Link to relevant commits, PRs, or existing issues.
105
105
 
106
106
  **For each DUPLICATE finding** (is_duplicate = true):
107
- 1. Find the existing issue using the issue tracker (check duplicate_of field).
108
- 2. Add a comment: "+1 — SWEny triage confirmed this issue is still active (seen again at {current UTC timestamp}). Latest context: {1-2 sentence summary}."
109
- 3. If the existing issue is closed/done, reopen it or note in the comment that the bug has recurred.
107
+ 1. Find the existing issue (check duplicate_of field).
108
+ 2. Check the issue's comments — if the most recent comment is already from SWEny (contains "+1") within the last 24 hours, skip adding another comment.
109
+ 3. Otherwise add a SHORT comment: "+1 — seen again {UTC timestamp}. {one sentence of new context}." (Keep it under 2 lines. No markdown headers, no emoji, no formatting.)
110
+ 4. If the existing issue is closed/done, reopen it.
110
111
 
111
112
  If context.issueTemplate is provided, use it as the format for new issue bodies. Otherwise use a clear structure with: Summary, Root Cause, Impact, Steps to Reproduce, and Recommended Fix.
112
113
 
@@ -118,9 +119,10 @@ Use whichever issue tracker is available to you. Output the created/updated issu
118
119
  instruction: `Every finding from the investigation was either a duplicate or low-priority. No new issues need to be created.
119
120
 
120
121
  For each **duplicate** finding (check the findings array for items where is_duplicate = true):
121
- 1. Find the existing issue using the issue tracker (check duplicate_of field).
122
- 2. Add a comment: "+1 — SWEny triage confirmed this issue is still active (seen again at {current UTC timestamp}). Latest context: {1-2 sentence summary of what was found this run}."
123
- 3. If the issue is closed/done, reopen it or note in the comment that the bug has recurred.
122
+ 1. Find the existing issue (check duplicate_of field).
123
+ 2. Check the issue's comments — if the most recent comment is already from SWEny (contains "+1") within the last 24 hours, skip adding another comment.
124
+ 3. Otherwise add a SHORT comment: "+1 — seen again {UTC timestamp}. {one sentence of new context}." (Keep it under 2 lines. No markdown headers, no emoji, no formatting.)
125
+ 4. If the issue is closed/done, reopen it.
124
126
 
125
127
  For **low priority** findings, log a brief note about why they were skipped.`,
126
128
  skills: ["linear", "github"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sweny-ai/core",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sweny": "./dist/cli/main.js"