maestro-agent 0.0.1 → 0.0.3

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 +316 -2
  2. package/bin/maestro.ts +5 -0
  3. package/dist/maestro +0 -0
  4. package/dist/web/apple-touch-icon.png +0 -0
  5. package/dist/web/assets/Connections-BMA04Ycg.js +11 -0
  6. package/dist/web/assets/GanttView-DXjh0gxg.js +49 -0
  7. package/dist/web/assets/Home-Ct3Ho0Qt.js +1 -0
  8. package/dist/web/assets/HooksCrons--0kyVJcR.js +11 -0
  9. package/dist/web/assets/ProjectDetail-B_IqEpFu.js +1 -0
  10. package/dist/web/assets/Roles-D1tIQzto.js +24 -0
  11. package/dist/web/assets/Settings-yts4LUmH.js +11 -0
  12. package/dist/web/assets/Skills-DbuNLjIV.js +12 -0
  13. package/dist/web/assets/Wizard-vJol8-Y4.js +11 -0
  14. package/dist/web/assets/WorkspaceChat-DrsLs4m2.js +56 -0
  15. package/dist/web/assets/WorkspaceDashboard-B9vgrd2Z.js +6 -0
  16. package/dist/web/assets/WorkspaceNew-DoNGYHCG.js +1 -0
  17. package/dist/web/assets/WorkspaceProjects-DDp3mUse.js +6 -0
  18. package/dist/web/assets/WorkspaceSchedules-BTjmCbYG.js +1 -0
  19. package/dist/web/assets/WorkspaceTasks-mPU-bhKR.js +41 -0
  20. package/dist/web/assets/activity-CIA8bIA4.js +6 -0
  21. package/dist/web/assets/addon-fit-BlxrFPDK.js +1 -0
  22. package/dist/web/assets/arrow-right-S7ID7nDp.js +6 -0
  23. package/dist/web/assets/badge-DDTUzWIi.js +1 -0
  24. package/dist/web/assets/circle-check-B3P1qK0Z.js +6 -0
  25. package/dist/web/assets/clock-f9aYZox0.js +6 -0
  26. package/dist/web/assets/index-BRo4Du_s.js +11 -0
  27. package/dist/web/assets/index-C7kx39S9.js +196 -0
  28. package/dist/web/assets/index-D6LSdZea.css +1 -0
  29. package/dist/web/assets/plus-BHnOxbns.js +6 -0
  30. package/dist/web/assets/refresh-cw-BWX04Hg3.js +6 -0
  31. package/dist/web/assets/save-BLbb_9xz.js +6 -0
  32. package/dist/web/assets/sparkles-CDr6Dw1e.js +6 -0
  33. package/dist/web/assets/trash-2-9-ThEdey.js +6 -0
  34. package/dist/web/assets/useEventStream-DXt2Hmei.js +1 -0
  35. package/dist/web/assets/x-DVdKPXXy.js +6 -0
  36. package/dist/web/assets/xterm-DYP7pi_n.css +32 -0
  37. package/dist/web/assets/xterm-DlVFs1Kw.js +9 -0
  38. package/dist/web/favicon-512.png +0 -0
  39. package/dist/web/favicon.png +0 -0
  40. package/dist/web/index.html +15 -0
  41. package/package.json +49 -6
  42. package/src/api/agents.ts +76 -0
  43. package/src/api/audit.ts +19 -0
  44. package/src/api/autopilot.ts +73 -0
  45. package/src/api/chat.ts +801 -0
  46. package/src/api/chief.ts +84 -0
  47. package/src/api/config.ts +39 -0
  48. package/src/api/gantt.ts +72 -0
  49. package/src/api/hooks.ts +54 -0
  50. package/src/api/inbox.ts +125 -0
  51. package/src/api/lark.ts +32 -0
  52. package/src/api/memory.ts +37 -0
  53. package/src/api/ops.ts +89 -0
  54. package/src/api/projects.ts +105 -0
  55. package/src/api/roles.ts +123 -0
  56. package/src/api/runtimes.ts +62 -0
  57. package/src/api/scheduled-tasks.ts +203 -0
  58. package/src/api/sessions.ts +479 -0
  59. package/src/api/skills.ts +386 -0
  60. package/src/api/tasks.ts +457 -0
  61. package/src/api/telegram.ts +94 -0
  62. package/src/api/templates.ts +36 -0
  63. package/src/api/webhooks.ts +20 -0
  64. package/src/api/workspaces.ts +150 -0
  65. package/src/bridges/lark/index.ts +213 -0
  66. package/src/bridges/telegram/index.ts +273 -0
  67. package/src/bridges/telegram/polling.ts +185 -0
  68. package/src/chat/index.ts +86 -0
  69. package/src/chief/index.ts +461 -0
  70. package/src/core/cli.ts +333 -0
  71. package/src/core/db.ts +53 -0
  72. package/src/core/event-bus.ts +33 -0
  73. package/src/core/index.ts +6 -0
  74. package/src/core/migrations.ts +303 -0
  75. package/src/core/router.ts +69 -0
  76. package/src/core/schema.sql +232 -0
  77. package/src/core/server.ts +308 -0
  78. package/src/core/validate.ts +22 -0
  79. package/src/discovery/index.ts +194 -0
  80. package/src/gateway/adapters/telegram.ts +148 -0
  81. package/src/gateway/index.ts +31 -0
  82. package/src/gateway/manager.ts +176 -0
  83. package/src/gateway/types.ts +77 -0
  84. package/src/inbox/index.ts +500 -0
  85. package/src/ops/artifact-sync.ts +65 -0
  86. package/src/ops/autopilot.ts +338 -0
  87. package/src/ops/gc.ts +252 -0
  88. package/src/ops/index.ts +226 -0
  89. package/src/ops/project-serial.ts +52 -0
  90. package/src/ops/role-dispatch.ts +111 -0
  91. package/src/ops/runtime-scheduler.ts +447 -0
  92. package/src/ops/task-blocking.ts +65 -0
  93. package/src/ops/task-deps.ts +37 -0
  94. package/src/ops/task-workspace.ts +60 -0
  95. package/src/roles/index.ts +258 -0
  96. package/src/roles/prompt-assembler.ts +85 -0
  97. package/src/roles/workspace-role.ts +155 -0
  98. package/src/scheduler/index.ts +461 -0
  99. package/src/session/output-parser.ts +75 -0
  100. package/src/session/realtime-parser.ts +40 -0
  101. package/src/skills/builtin.ts +155 -0
  102. package/src/skills/skill-extractor.ts +452 -0
  103. package/src/skills/skill-md.ts +282 -0
  104. package/src/transport/http-api.ts +75 -0
  105. package/src/transport/index.ts +4 -0
  106. package/src/transport/local-pty.ts +119 -0
  107. package/src/transport/ssh.ts +176 -0
  108. package/src/transport/types.ts +20 -0
  109. package/src/workflows/index.ts +231 -0
  110. package/index.js +0 -1
  111. package/maestro-agent-0.0.1.tgz +0 -0
@@ -0,0 +1,155 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { now } from "../core/db";
5
+ import { syncAllSkills } from "../api/skills";
6
+
7
+ const ALL_AGENT_PLATFORMS = {
8
+ claude: true,
9
+ codex: true,
10
+ gemini: true,
11
+ opencode: true,
12
+ hermes: true,
13
+ };
14
+
15
+ const TASK_WORKFLOW_ID = "skill_task_workflow";
16
+ const TASK_WORKFLOW_NAME = "task-workflow";
17
+ const TASK_WORKFLOW_DESCRIPTION =
18
+ "Common workflow operating method for agents: inspect task context, discover valid actions, and transition through Maestro instead of assuming fixed statuses or actions.";
19
+
20
+ const TASK_WORKFLOW_MD = `# task-workflow
21
+
22
+ Use this skill whenever you are working on a Maestro task.
23
+
24
+ This skill teaches the workflow method. It does not define a fixed workflow. Do not assume any particular status names or action names. Each workspace or project may define its own workflow, and the valid next actions can change by task state.
25
+
26
+ ## Operating Loop
27
+
28
+ 1. Inspect the current task before acting:
29
+
30
+ \`\`\`bash
31
+ maestro inbox task show "$MAESTRO_TASK_ID" --json
32
+ \`\`\`
33
+
34
+ 2. Before changing state, discover the valid workflow actions:
35
+
36
+ \`\`\`bash
37
+ maestro inbox task actions "$MAESTRO_TASK_ID" --json
38
+ \`\`\`
39
+
40
+ 3. Pick the action whose meaning matches your real outcome, then transition through Maestro:
41
+
42
+ \`\`\`bash
43
+ maestro inbox task transition "$MAESTRO_TASK_ID" "<action_id>" --summary "What changed and why"
44
+ \`\`\`
45
+
46
+ 4. If no valid action matches the situation, add a task note with the current evidence and escalate instead of inventing a status.
47
+
48
+ ## Rules
49
+
50
+ - Treat the task's workflow definition as the source of truth.
51
+ - Never update task status by writing directly to the database or by assuming a hardcoded state.
52
+ - Never use old fixed lifecycle commands; always use actions and transition.
53
+ - Prefer concise summaries that explain what was completed, what remains, and any review or handoff context.
54
+ - If workflow commands are unavailable, report that limitation in the task thread and continue with the closest supported Maestro command.
55
+ `;
56
+
57
+ interface BuiltinSkillResult {
58
+ installed: { id: string; name: string }[];
59
+ updated: { id: string; name: string }[];
60
+ sync?: { synced: number; errors: string[] };
61
+ }
62
+
63
+ interface EnsureBuiltinSkillsOptions {
64
+ sync?: boolean;
65
+ }
66
+
67
+ export function ensureBuiltinSkills(
68
+ db: Database,
69
+ hubDir: string,
70
+ options: EnsureBuiltinSkillsOptions = {}
71
+ ): BuiltinSkillResult {
72
+ const result: BuiltinSkillResult = { installed: [], updated: [] };
73
+ const sync = options.sync ?? true;
74
+
75
+ ensureTaskWorkflowSkill(db, hubDir, result);
76
+
77
+ if (sync) {
78
+ result.sync = syncAllSkills(db, hubDir);
79
+ }
80
+
81
+ return result;
82
+ }
83
+
84
+ function ensureTaskWorkflowSkill(db: Database, hubDir: string, result: BuiltinSkillResult): void {
85
+ const skillsDir = join(hubDir, "skills");
86
+ mkdirSync(skillsDir, { recursive: true });
87
+
88
+ const existing = db
89
+ .query("SELECT * FROM skill WHERE id = ? OR name = ?")
90
+ .get(TASK_WORKFLOW_ID, TASK_WORKFLOW_NAME) as any | null;
91
+ const id = existing?.id || TASK_WORKFLOW_ID;
92
+ const skillDir = join(skillsDir, id);
93
+ const skillPath = join(skillDir, "SKILL.md");
94
+ const ts = now();
95
+
96
+ mkdirSync(skillDir, { recursive: true });
97
+
98
+ const currentContent = existsSync(skillPath) ? readFileSync(skillPath, "utf-8") : "";
99
+ const contentChanged = currentContent !== TASK_WORKFLOW_MD;
100
+ if (contentChanged) {
101
+ writeFileSync(skillPath, TASK_WORKFLOW_MD, "utf-8");
102
+ }
103
+
104
+ if (!existing) {
105
+ db.run(
106
+ `INSERT INTO skill (id, name, description, source, source_url, platforms_json, enabled, config_json, installed_at, updated_at)
107
+ VALUES (?, ?, ?, 'local', NULL, ?, 1, ?, ?, ?)`,
108
+ [
109
+ id,
110
+ TASK_WORKFLOW_NAME,
111
+ TASK_WORKFLOW_DESCRIPTION,
112
+ JSON.stringify(ALL_AGENT_PLATFORMS),
113
+ JSON.stringify({ builtin: true }),
114
+ ts,
115
+ ts,
116
+ ]
117
+ );
118
+ result.installed.push({ id, name: TASK_WORKFLOW_NAME });
119
+ return;
120
+ }
121
+
122
+ const expectedPlatformsJson = JSON.stringify(ALL_AGENT_PLATFORMS);
123
+ const expectedConfigJson = JSON.stringify({ builtin: true });
124
+ const needsDbUpdate =
125
+ existing.name !== TASK_WORKFLOW_NAME ||
126
+ existing.description !== TASK_WORKFLOW_DESCRIPTION ||
127
+ existing.source !== "local" ||
128
+ existing.platforms_json !== expectedPlatformsJson ||
129
+ existing.enabled !== 1 ||
130
+ existing.config_json !== expectedConfigJson ||
131
+ contentChanged;
132
+
133
+ if (needsDbUpdate) {
134
+ db.run(
135
+ `UPDATE skill
136
+ SET name = ?,
137
+ description = ?,
138
+ source = 'local',
139
+ platforms_json = ?,
140
+ enabled = 1,
141
+ config_json = ?,
142
+ updated_at = ?
143
+ WHERE id = ?`,
144
+ [
145
+ TASK_WORKFLOW_NAME,
146
+ TASK_WORKFLOW_DESCRIPTION,
147
+ expectedPlatformsJson,
148
+ expectedConfigJson,
149
+ ts,
150
+ id,
151
+ ]
152
+ );
153
+ result.updated.push({ id, name: TASK_WORKFLOW_NAME });
154
+ }
155
+ }
@@ -0,0 +1,452 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { getSkillsDir } from "../api/skills";
4
+ import { generateId, now } from "../core/db";
5
+ import type { HubContext } from "../core/server";
6
+ import type { SkillFrontmatter } from "./skill-md";
7
+ import { serializeSkillMd } from "./skill-md";
8
+
9
+ // ─── Types ───────────────────────────────────────────────────────────────────
10
+
11
+ export interface SkillExtractionResult {
12
+ extracted: boolean;
13
+ skill_id?: string;
14
+ skill_name?: string;
15
+ reason?: string;
16
+ }
17
+
18
+ export interface SkillExtractionConfig {
19
+ enabled: boolean;
20
+ min_transcript_length: number; // Minimum transcript length to consider
21
+ max_transcript_length: number; // Truncate beyond this
22
+ auto_enable: boolean; // Auto-enable extracted skills
23
+ require_approval: boolean; // Mark as draft, require human approval
24
+ }
25
+
26
+ const DEFAULT_CONFIG: SkillExtractionConfig = {
27
+ enabled: true,
28
+ min_transcript_length: 500,
29
+ max_transcript_length: 50_000,
30
+ auto_enable: false,
31
+ require_approval: true,
32
+ };
33
+
34
+ // ─── Core Extraction ─────────────────────────────────────────────────────────
35
+
36
+ /**
37
+ * Analyze a session transcript and extract a reusable skill if a clear
38
+ * repeatable pattern is detected.
39
+ *
40
+ * Uses heuristic analysis (no LLM dependency) to identify:
41
+ * - Multi-step procedures that could be automated
42
+ * - File manipulation patterns
43
+ * - Command sequences
44
+ * - Code generation templates
45
+ */
46
+ export function extractSkillFromTranscript(
47
+ ctx: HubContext,
48
+ sessionId: string,
49
+ config: Partial<SkillExtractionConfig> = {}
50
+ ): SkillExtractionResult {
51
+ const cfg = { ...DEFAULT_CONFIG, ...config };
52
+ if (!cfg.enabled) return { extracted: false, reason: "extraction_disabled" };
53
+
54
+ // Load session and transcript
55
+ const session = ctx.db.query("SELECT * FROM session WHERE id = ?").get(sessionId) as any;
56
+ if (!session) return { extracted: false, reason: "session_not_found" };
57
+ if (!session.transcript_path || !existsSync(session.transcript_path)) {
58
+ return { extracted: false, reason: "no_transcript" };
59
+ }
60
+
61
+ const transcript = readFileSync(session.transcript_path, "utf-8");
62
+ if (transcript.length < cfg.min_transcript_length) {
63
+ return { extracted: false, reason: "transcript_too_short" };
64
+ }
65
+
66
+ // Get task context for naming
67
+ const task = session.task_id
68
+ ? ctx.db.query("SELECT title, project_id FROM task WHERE id = ?").get(session.task_id) as any
69
+ : null;
70
+
71
+ // Analyze transcript for extractable patterns
72
+ const analysis = analyzeTranscript(transcript.slice(0, cfg.max_transcript_length));
73
+ if (!analysis.extractable) {
74
+ return { extracted: false, reason: analysis.reason };
75
+ }
76
+
77
+ // Check for duplicate skill
78
+ const existingSkill = ctx.db.query(
79
+ "SELECT id FROM skill WHERE name = ?"
80
+ ).get(analysis.skillName) as any;
81
+ if (existingSkill) {
82
+ return { extracted: false, reason: "duplicate_skill", skill_id: existingSkill.id };
83
+ }
84
+
85
+ // Generate SKILL.md content
86
+ const frontmatter: SkillFrontmatter = {
87
+ name: analysis.skillName,
88
+ description: analysis.description,
89
+ version: "1.0.0",
90
+ metadata: {
91
+ author: "auto-extractor",
92
+ source_session: sessionId,
93
+ source_task: session.task_id || null,
94
+ tags: analysis.tags,
95
+ extracted_at: new Date().toISOString(),
96
+ },
97
+ platforms: { claude: true, codex: true, gemini: true, opencode: true },
98
+ };
99
+
100
+ const body = buildSkillBody(analysis);
101
+ const skillContent = serializeSkillMd(frontmatter, body);
102
+
103
+ // Write skill to filesystem
104
+ const skillsDir = getSkillsDir(ctx.hubDir);
105
+ const skillDir = join(skillsDir, analysis.skillName);
106
+ mkdirSync(skillDir, { recursive: true });
107
+ writeFileSync(join(skillDir, "SKILL.md"), skillContent);
108
+
109
+ // Insert into DB
110
+ const skillId = generateId("skill");
111
+ const ts = now();
112
+ ctx.db.run(
113
+ `INSERT INTO skill (id, name, description, source, platforms_json, enabled, config_json, installed_at, updated_at)
114
+ VALUES (?, ?, ?, 'local', ?, ?, ?, ?, ?)`,
115
+ [
116
+ skillId,
117
+ analysis.skillName,
118
+ analysis.description,
119
+ JSON.stringify({ claude: true, codex: true, gemini: true, opencode: true }),
120
+ cfg.auto_enable && !cfg.require_approval ? 1 : 0,
121
+ JSON.stringify({ auto_extracted: true, source_session: sessionId }),
122
+ ts,
123
+ ts,
124
+ ]
125
+ );
126
+
127
+ ctx.bus.publish("skill.extracted", {
128
+ skill_id: skillId,
129
+ skill_name: analysis.skillName,
130
+ session_id: sessionId,
131
+ task_id: session.task_id,
132
+ });
133
+
134
+ return { extracted: true, skill_id: skillId, skill_name: analysis.skillName };
135
+ }
136
+
137
+ // ─── Transcript Analysis ─────────────────────────────────────────────────────
138
+
139
+ interface TranscriptAnalysis {
140
+ extractable: boolean;
141
+ reason?: string;
142
+ skillName: string;
143
+ description: string;
144
+ tags: string[];
145
+ steps: string[];
146
+ commands: string[];
147
+ filePatterns: string[];
148
+ codeTemplates: string[];
149
+ }
150
+
151
+ function analyzeTranscript(transcript: string): TranscriptAnalysis {
152
+ // Skip transcripts that are primarily maestro agent-hub communication
153
+ const maestroMentions = (transcript.match(/maestro\s+(chat|inbox|task|session|heartbeat|agent|poll)/gi) || []).length;
154
+ const totalLines = transcript.split("\n").length;
155
+ if (maestroMentions > 3 && maestroMentions / totalLines > 0.1) {
156
+ return {
157
+ extractable: false,
158
+ reason: "infrastructure_only",
159
+ skillName: "",
160
+ description: "",
161
+ tags: [],
162
+ steps: [],
163
+ commands: [],
164
+ filePatterns: [],
165
+ codeTemplates: [],
166
+ };
167
+ }
168
+
169
+ const commands = extractCommands(transcript);
170
+ const filePatterns = extractFilePatterns(transcript);
171
+ const steps = extractProcedureSteps(transcript);
172
+ const codeTemplates = extractCodeTemplates(transcript);
173
+
174
+ // Determine if there's a repeatable pattern worth extracting
175
+ const hasSubstantialSteps = steps.length >= 3;
176
+ const hasCommands = commands.length >= 2;
177
+ const hasFileOps = filePatterns.length >= 2;
178
+ const hasCodeGen = codeTemplates.length >= 1;
179
+
180
+ const extractable = hasSubstantialSteps || (hasCommands && hasFileOps) || hasCodeGen;
181
+
182
+ if (!extractable) {
183
+ return {
184
+ extractable: false,
185
+ reason: "no_repeatable_pattern",
186
+ skillName: "",
187
+ description: "",
188
+ tags: [],
189
+ steps: [],
190
+ commands: [],
191
+ filePatterns: [],
192
+ codeTemplates: [],
193
+ };
194
+ }
195
+
196
+ // Generate skill name from patterns
197
+ const skillName = inferSkillName(steps, commands, filePatterns);
198
+ const description = inferDescription(steps, commands, filePatterns);
199
+ const tags = inferTags(commands, filePatterns, codeTemplates);
200
+
201
+ return {
202
+ extractable: true,
203
+ skillName,
204
+ description,
205
+ tags,
206
+ steps,
207
+ commands,
208
+ filePatterns,
209
+ codeTemplates,
210
+ };
211
+ }
212
+
213
+ // ─── Pattern Extraction ──────────────────────────────────────────────────────
214
+
215
+ // Maestro infrastructure commands that should never be extracted as skills
216
+ const INFRA_COMMAND_PATTERNS = [
217
+ /^maestro\s/,
218
+ /^hub\s/,
219
+ /maestro\s+chat\s+poll/,
220
+ /maestro\s+inbox/,
221
+ /maestro\s+task\s+transition/,
222
+ /maestro\s+session/,
223
+ /maestro\s+heartbeat/,
224
+ /maestro\s+start/,
225
+ /maestro\s+stop/,
226
+ /maestro\s+agent/,
227
+ ];
228
+
229
+ function isInfraCommand(cmd: string): boolean {
230
+ const lower = cmd.toLowerCase();
231
+ return INFRA_COMMAND_PATTERNS.some((p) => p.test(lower));
232
+ }
233
+
234
+ function extractCommands(transcript: string): string[] {
235
+ const commands: string[] = [];
236
+ const patterns = [
237
+ /(?:RunCommand|run_command|Execute)\s*\(\s*(?:command\s*[:=]\s*)?["']([^"']+)["']/g,
238
+ /\$\s+(.+)/gm,
239
+ /```(?:bash|sh|shell|zsh)\n([\s\S]*?)```/g,
240
+ ];
241
+
242
+ for (const pattern of patterns) {
243
+ for (const m of transcript.matchAll(pattern)) {
244
+ const cmd = m[1].trim();
245
+ if (cmd && cmd.length < 200 && !cmd.toLowerCase().includes("password") && !cmd.toLowerCase().includes("token") && !isInfraCommand(cmd)) {
246
+ commands.push(cmd);
247
+ }
248
+ }
249
+ }
250
+
251
+ return [...new Set(commands)].slice(0, 20);
252
+ }
253
+
254
+ function extractFilePatterns(transcript: string): string[] {
255
+ const files = new Set<string>();
256
+ const patterns = [
257
+ /(?:Write|Edit|Read)\s*\(\s*(?:file_path\s*[:=]\s*)?["']([^"']+)["']/g,
258
+ /(?:created?|modified?|updated?|wrote)\s+(?:file\s+)?[`"]([^`"]+)[`"]/gi,
259
+ ];
260
+
261
+ for (const pattern of patterns) {
262
+ for (const m of transcript.matchAll(pattern)) {
263
+ if (m[1]) files.add(m[1]);
264
+ }
265
+ }
266
+
267
+ return [...files].slice(0, 20);
268
+ }
269
+
270
+ function extractProcedureSteps(transcript: string): string[] {
271
+ const steps: string[] = [];
272
+
273
+ // Look for numbered steps
274
+ const numberedPattern = /(?:^|\n)\s*(\d+[.)]\s+.+)/g;
275
+ for (const m of transcript.matchAll(numberedPattern)) {
276
+ steps.push(m[1].trim());
277
+ }
278
+
279
+ // Look for bullet-point procedures
280
+ const bulletPattern = /(?:^|\n)\s*[-*]\s+((?:First|Then|Next|Finally|After|Before|Now|Create|Add|Update|Install|Configure|Set up|Run|Execute|Build|Deploy).+)/gi;
281
+ for (const m of transcript.matchAll(bulletPattern)) {
282
+ steps.push(m[1].trim());
283
+ }
284
+
285
+ // Look for action descriptions in context
286
+ const actionPattern = /(?:I'll|Let me|Going to|We need to|I will)\s+(.+?)(?:\.|$)/gm;
287
+ for (const m of transcript.matchAll(actionPattern)) {
288
+ const step = m[1].trim();
289
+ if (step.length > 10 && step.length < 200) {
290
+ steps.push(step);
291
+ }
292
+ }
293
+
294
+ return [...new Set(steps)].slice(0, 15);
295
+ }
296
+
297
+ function extractCodeTemplates(transcript: string): string[] {
298
+ const templates: string[] = [];
299
+ const codeBlockPattern = /```(\w+)?\n([\s\S]*?)```/g;
300
+
301
+ for (const m of transcript.matchAll(codeBlockPattern)) {
302
+ const lang = m[1];
303
+ const code = m[2].trim();
304
+ // Only extract substantial code blocks that look like templates
305
+ if (code.length > 50 && code.length < 5000 && lang) {
306
+ templates.push(`\`\`\`${lang}\n${code}\n\`\`\``);
307
+ }
308
+ }
309
+
310
+ return templates.slice(0, 5);
311
+ }
312
+
313
+ // ─── Inference ───────────────────────────────────────────────────────────────
314
+
315
+ function inferSkillName(steps: string[], commands: string[], files: string[]): string {
316
+ // Try to infer from common patterns
317
+ const allText = [...steps, ...commands].join(" ").toLowerCase();
318
+
319
+ // Check for common task patterns
320
+ const patterns: [RegExp, string][] = [
321
+ [/docker|container|compose/, "docker-setup"],
322
+ [/test|spec|jest|vitest|bun test/, "run-tests"],
323
+ [/lint|eslint|prettier|format/, "lint-and-format"],
324
+ [/deploy|publish|release/, "deploy"],
325
+ [/build|compile|bundle/, "build-project"],
326
+ [/migrate|migration|schema/, "db-migration"],
327
+ [/refactor/, "refactor"],
328
+ [/review|cr\b/, "code-review"],
329
+ [/setup|init|scaffold|create.*project/, "project-setup"],
330
+ [/api|endpoint|route/, "api-endpoint"],
331
+ [/component|widget|ui/, "ui-component"],
332
+ [/auth|login|jwt|oauth/, "auth-setup"],
333
+ ];
334
+
335
+ for (const [regex, name] of patterns) {
336
+ if (regex.test(allText)) {
337
+ // Make unique with a short hash from content
338
+ const hash = simpleHash(allText).slice(0, 4);
339
+ return `${name}-${hash}`;
340
+ }
341
+ }
342
+
343
+ // Fallback: generate from first meaningful step
344
+ if (steps.length > 0) {
345
+ const slug = steps[0]
346
+ .toLowerCase()
347
+ .replace(/[^a-z0-9]+/g, "-")
348
+ .replace(/^-|-$/g, "")
349
+ .slice(0, 30);
350
+ return slug || `extracted-skill-${simpleHash(allText).slice(0, 6)}`;
351
+ }
352
+
353
+ return `extracted-skill-${simpleHash(allText).slice(0, 6)}`;
354
+ }
355
+
356
+ function inferDescription(steps: string[], commands: string[], files: string[]): string {
357
+ if (steps.length > 0) {
358
+ // Use first 2-3 steps as description
359
+ const desc = steps.slice(0, 3).join("; ");
360
+ return desc.length > 200 ? desc.slice(0, 197) + "..." : desc;
361
+ }
362
+
363
+ if (commands.length > 0) {
364
+ return `Automates: ${commands.slice(0, 3).join(", ")}`;
365
+ }
366
+
367
+ return `Extracted procedure operating on ${files.length} files`;
368
+ }
369
+
370
+ function inferTags(commands: string[], files: string[], codeTemplates: string[]): string[] {
371
+ const tags = new Set<string>();
372
+
373
+ // Infer from file extensions
374
+ for (const f of files) {
375
+ if (f.endsWith(".ts") || f.endsWith(".tsx")) tags.add("typescript");
376
+ if (f.endsWith(".js") || f.endsWith(".jsx")) tags.add("javascript");
377
+ if (f.endsWith(".py")) tags.add("python");
378
+ if (f.endsWith(".go")) tags.add("go");
379
+ if (f.endsWith(".rs")) tags.add("rust");
380
+ if (f.endsWith(".sql")) tags.add("sql");
381
+ if (f.endsWith(".css") || f.endsWith(".scss")) tags.add("styling");
382
+ if (f.includes("test") || f.includes("spec")) tags.add("testing");
383
+ }
384
+
385
+ // Infer from commands
386
+ const cmdStr = commands.join(" ").toLowerCase();
387
+ if (cmdStr.includes("npm") || cmdStr.includes("bun") || cmdStr.includes("yarn")) tags.add("nodejs");
388
+ if (cmdStr.includes("git")) tags.add("git");
389
+ if (cmdStr.includes("docker")) tags.add("docker");
390
+
391
+ return [...tags].slice(0, 8);
392
+ }
393
+
394
+ // ─── Skill Body Builder ──────────────────────────────────────────────────────
395
+
396
+ function buildSkillBody(analysis: TranscriptAnalysis): string {
397
+ const sections: string[] = [];
398
+
399
+ sections.push(`# ${analysis.skillName}\n`);
400
+ sections.push(`${analysis.description}\n`);
401
+
402
+ if (analysis.steps.length > 0) {
403
+ sections.push("## Procedure\n");
404
+ for (let i = 0; i < analysis.steps.length; i++) {
405
+ sections.push(`${i + 1}. ${analysis.steps[i]}`);
406
+ }
407
+ sections.push("");
408
+ }
409
+
410
+ if (analysis.commands.length > 0) {
411
+ sections.push("## Commands\n");
412
+ sections.push("```bash");
413
+ for (const cmd of analysis.commands.slice(0, 10)) {
414
+ sections.push(cmd);
415
+ }
416
+ sections.push("```\n");
417
+ }
418
+
419
+ if (analysis.filePatterns.length > 0) {
420
+ sections.push("## Files Involved\n");
421
+ for (const f of analysis.filePatterns) {
422
+ sections.push(`- \`${f}\``);
423
+ }
424
+ sections.push("");
425
+ }
426
+
427
+ if (analysis.codeTemplates.length > 0) {
428
+ sections.push("## Code Templates\n");
429
+ for (const tpl of analysis.codeTemplates.slice(0, 3)) {
430
+ sections.push(tpl);
431
+ sections.push("");
432
+ }
433
+ }
434
+
435
+ return sections.join("\n");
436
+ }
437
+
438
+ // ─── Utilities ───────────────────────────────────────────────────────────────
439
+
440
+ function simpleHash(str: string): string {
441
+ let hash = 0;
442
+ for (let i = 0; i < str.length; i++) {
443
+ const char = str.charCodeAt(i);
444
+ hash = ((hash << 5) - hash) + char;
445
+ hash = hash & hash; // Convert to 32bit integer
446
+ }
447
+ return Math.abs(hash).toString(36);
448
+ }
449
+
450
+ // ─── Exported for testing ────────────────────────────────────────────────────
451
+ export { analyzeTranscript, extractCodeTemplates, extractCommands, extractFilePatterns, extractProcedureSteps };
452
+