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.
- package/README.md +316 -2
- package/bin/maestro.ts +5 -0
- package/dist/maestro +0 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/Connections-BMA04Ycg.js +11 -0
- package/dist/web/assets/GanttView-DXjh0gxg.js +49 -0
- package/dist/web/assets/Home-Ct3Ho0Qt.js +1 -0
- package/dist/web/assets/HooksCrons--0kyVJcR.js +11 -0
- package/dist/web/assets/ProjectDetail-B_IqEpFu.js +1 -0
- package/dist/web/assets/Roles-D1tIQzto.js +24 -0
- package/dist/web/assets/Settings-yts4LUmH.js +11 -0
- package/dist/web/assets/Skills-DbuNLjIV.js +12 -0
- package/dist/web/assets/Wizard-vJol8-Y4.js +11 -0
- package/dist/web/assets/WorkspaceChat-DrsLs4m2.js +56 -0
- package/dist/web/assets/WorkspaceDashboard-B9vgrd2Z.js +6 -0
- package/dist/web/assets/WorkspaceNew-DoNGYHCG.js +1 -0
- package/dist/web/assets/WorkspaceProjects-DDp3mUse.js +6 -0
- package/dist/web/assets/WorkspaceSchedules-BTjmCbYG.js +1 -0
- package/dist/web/assets/WorkspaceTasks-mPU-bhKR.js +41 -0
- package/dist/web/assets/activity-CIA8bIA4.js +6 -0
- package/dist/web/assets/addon-fit-BlxrFPDK.js +1 -0
- package/dist/web/assets/arrow-right-S7ID7nDp.js +6 -0
- package/dist/web/assets/badge-DDTUzWIi.js +1 -0
- package/dist/web/assets/circle-check-B3P1qK0Z.js +6 -0
- package/dist/web/assets/clock-f9aYZox0.js +6 -0
- package/dist/web/assets/index-BRo4Du_s.js +11 -0
- package/dist/web/assets/index-C7kx39S9.js +196 -0
- package/dist/web/assets/index-D6LSdZea.css +1 -0
- package/dist/web/assets/plus-BHnOxbns.js +6 -0
- package/dist/web/assets/refresh-cw-BWX04Hg3.js +6 -0
- package/dist/web/assets/save-BLbb_9xz.js +6 -0
- package/dist/web/assets/sparkles-CDr6Dw1e.js +6 -0
- package/dist/web/assets/trash-2-9-ThEdey.js +6 -0
- package/dist/web/assets/useEventStream-DXt2Hmei.js +1 -0
- package/dist/web/assets/x-DVdKPXXy.js +6 -0
- package/dist/web/assets/xterm-DYP7pi_n.css +32 -0
- package/dist/web/assets/xterm-DlVFs1Kw.js +9 -0
- package/dist/web/favicon-512.png +0 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/index.html +15 -0
- package/package.json +49 -6
- package/src/api/agents.ts +76 -0
- package/src/api/audit.ts +19 -0
- package/src/api/autopilot.ts +73 -0
- package/src/api/chat.ts +801 -0
- package/src/api/chief.ts +84 -0
- package/src/api/config.ts +39 -0
- package/src/api/gantt.ts +72 -0
- package/src/api/hooks.ts +54 -0
- package/src/api/inbox.ts +125 -0
- package/src/api/lark.ts +32 -0
- package/src/api/memory.ts +37 -0
- package/src/api/ops.ts +89 -0
- package/src/api/projects.ts +105 -0
- package/src/api/roles.ts +123 -0
- package/src/api/runtimes.ts +62 -0
- package/src/api/scheduled-tasks.ts +203 -0
- package/src/api/sessions.ts +479 -0
- package/src/api/skills.ts +386 -0
- package/src/api/tasks.ts +457 -0
- package/src/api/telegram.ts +94 -0
- package/src/api/templates.ts +36 -0
- package/src/api/webhooks.ts +20 -0
- package/src/api/workspaces.ts +150 -0
- package/src/bridges/lark/index.ts +213 -0
- package/src/bridges/telegram/index.ts +273 -0
- package/src/bridges/telegram/polling.ts +185 -0
- package/src/chat/index.ts +86 -0
- package/src/chief/index.ts +461 -0
- package/src/core/cli.ts +333 -0
- package/src/core/db.ts +53 -0
- package/src/core/event-bus.ts +33 -0
- package/src/core/index.ts +6 -0
- package/src/core/migrations.ts +303 -0
- package/src/core/router.ts +69 -0
- package/src/core/schema.sql +232 -0
- package/src/core/server.ts +308 -0
- package/src/core/validate.ts +22 -0
- package/src/discovery/index.ts +194 -0
- package/src/gateway/adapters/telegram.ts +148 -0
- package/src/gateway/index.ts +31 -0
- package/src/gateway/manager.ts +176 -0
- package/src/gateway/types.ts +77 -0
- package/src/inbox/index.ts +500 -0
- package/src/ops/artifact-sync.ts +65 -0
- package/src/ops/autopilot.ts +338 -0
- package/src/ops/gc.ts +252 -0
- package/src/ops/index.ts +226 -0
- package/src/ops/project-serial.ts +52 -0
- package/src/ops/role-dispatch.ts +111 -0
- package/src/ops/runtime-scheduler.ts +447 -0
- package/src/ops/task-blocking.ts +65 -0
- package/src/ops/task-deps.ts +37 -0
- package/src/ops/task-workspace.ts +60 -0
- package/src/roles/index.ts +258 -0
- package/src/roles/prompt-assembler.ts +85 -0
- package/src/roles/workspace-role.ts +155 -0
- package/src/scheduler/index.ts +461 -0
- package/src/session/output-parser.ts +75 -0
- package/src/session/realtime-parser.ts +40 -0
- package/src/skills/builtin.ts +155 -0
- package/src/skills/skill-extractor.ts +452 -0
- package/src/skills/skill-md.ts +282 -0
- package/src/transport/http-api.ts +75 -0
- package/src/transport/index.ts +4 -0
- package/src/transport/local-pty.ts +119 -0
- package/src/transport/ssh.ts +176 -0
- package/src/transport/types.ts +20 -0
- package/src/workflows/index.ts +231 -0
- package/index.js +0 -1
- 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
|
+
|