factoryhub 0.0.0-stage → 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.
- package/LICENSE +202 -0
- package/NOTICE +16 -0
- package/README.md +189 -2
- package/factories/README.md +17 -0
- package/factories/client-file-readiness/README.md +3 -0
- package/factories/client-file-readiness/agents/analyst/agent.md +10 -0
- package/factories/client-file-readiness/agents/reviewer/agent.md +10 -0
- package/factories/client-file-readiness/agents/writer/agent.md +11 -0
- package/factories/client-file-readiness/factory.yaml +12 -0
- package/factories/client-file-readiness/flow.yaml +56 -0
- package/factories/competitor-teardown/README.md +3 -0
- package/factories/competitor-teardown/agents/analyst/agent.md +10 -0
- package/factories/competitor-teardown/agents/researcher/agent.md +11 -0
- package/factories/competitor-teardown/agents/reviewer/agent.md +10 -0
- package/factories/competitor-teardown/agents/writer/agent.md +11 -0
- package/factories/competitor-teardown/factory.yaml +11 -0
- package/factories/competitor-teardown/flow.yaml +75 -0
- package/factories/homepage-audit/README.md +3 -0
- package/factories/homepage-audit/agents/analyst/agent.md +10 -0
- package/factories/homepage-audit/agents/researcher/agent.md +11 -0
- package/factories/homepage-audit/agents/reviewer/agent.md +10 -0
- package/factories/homepage-audit/agents/writer/agent.md +11 -0
- package/factories/homepage-audit/factory.yaml +11 -0
- package/factories/homepage-audit/flow.yaml +77 -0
- package/factories/inbox-triage/README.md +3 -0
- package/factories/inbox-triage/agents/analyst/agent.md +10 -0
- package/factories/inbox-triage/agents/reviewer/agent.md +10 -0
- package/factories/inbox-triage/agents/writer/agent.md +11 -0
- package/factories/inbox-triage/factory.yaml +11 -0
- package/factories/inbox-triage/flow.yaml +57 -0
- package/factories/index.json +356 -0
- package/factories/launch-kit/README.md +3 -0
- package/factories/launch-kit/agents/reviewer/agent.md +10 -0
- package/factories/launch-kit/agents/strategist/agent.md +10 -0
- package/factories/launch-kit/agents/writer/agent.md +11 -0
- package/factories/launch-kit/factory.yaml +12 -0
- package/factories/launch-kit/flow.yaml +75 -0
- package/factories/meeting-to-actions/CHANGELOG.md +7 -0
- package/factories/meeting-to-actions/README.md +15 -0
- package/factories/meeting-to-actions/agents/analyst/agent.md +10 -0
- package/factories/meeting-to-actions/agents/reviewer/agent.md +10 -0
- package/factories/meeting-to-actions/agents/writer/agent.md +11 -0
- package/factories/meeting-to-actions/factory.yaml +11 -0
- package/factories/meeting-to-actions/flow.yaml +53 -0
- package/factories/meeting-to-actions/mindburn.requirements.yaml +8 -0
- package/factories/meeting-to-actions/mindburn.tests.yaml +16 -0
- package/factories/outreach-pack/README.md +3 -0
- package/factories/outreach-pack/agents/reviewer/agent.md +5 -0
- package/factories/outreach-pack/agents/writer/agent.md +5 -0
- package/factories/outreach-pack/factory.yaml +9 -0
- package/factories/outreach-pack/flow.yaml +42 -0
- package/factories/prospect-brief/README.md +3 -0
- package/factories/prospect-brief/agents/researcher/agent.md +11 -0
- package/factories/prospect-brief/agents/reviewer/agent.md +10 -0
- package/factories/prospect-brief/agents/strategist/agent.md +10 -0
- package/factories/prospect-brief/agents/writer/agent.md +11 -0
- package/factories/prospect-brief/factory.yaml +11 -0
- package/factories/prospect-brief/flow.yaml +79 -0
- package/package.json +79 -4
- package/packages/cli/bin/factoryhub.mjs +8 -0
- package/packages/cli/package.json +29 -0
- package/packages/cli/src/args.mjs +47 -0
- package/packages/cli/src/commands/export.mjs +55 -0
- package/packages/cli/src/commands/get.mjs +38 -0
- package/packages/cli/src/commands/inspect.mjs +66 -0
- package/packages/cli/src/commands/list.mjs +29 -0
- package/packages/cli/src/commands/mcp.mjs +26 -0
- package/packages/cli/src/commands/new.mjs +148 -0
- package/packages/cli/src/commands/pack.mjs +97 -0
- package/packages/cli/src/commands/resume.mjs +67 -0
- package/packages/cli/src/commands/run.mjs +190 -0
- package/packages/cli/src/commands/search.mjs +31 -0
- package/packages/cli/src/commands/validate.mjs +81 -0
- package/packages/cli/src/main.mjs +68 -0
- package/packages/cli/src/util.mjs +21 -0
- package/packages/engine/lib/catalog.mjs +50 -0
- package/packages/engine/lib/hub.mjs +71 -0
- package/packages/engine/lib/node.mjs +114 -0
- package/packages/engine/lib/providers.mjs +52 -0
- package/packages/engine/lib/run.mjs +131 -0
- package/packages/engine/lib/skill.mjs +229 -0
- package/packages/engine/package.json +39 -0
- package/packages/engine/src/author-tests.mjs +78 -0
- package/packages/engine/src/compose.mjs +150 -0
- package/packages/engine/src/cost.mjs +41 -0
- package/packages/engine/src/diagnostics.mjs +21 -0
- package/packages/engine/src/factory-engine.mjs +1762 -0
- package/packages/engine/src/index.mjs +9 -0
- package/packages/engine/src/markdown.mjs +97 -0
- package/packages/engine/src/package.mjs +116 -0
- package/packages/engine/src/run.mjs +232 -0
- package/packages/mcp/bin/factoryhub-mcp.mjs +6 -0
- package/packages/mcp/package.json +36 -0
- package/packages/mcp/src/server.mjs +280 -0
- package/spec/README.md +36 -0
- package/spec/factory-format.md +621 -0
- package/spec/schema/factory.schema.json +43 -0
- package/spec/schema/flow.schema.json +169 -0
- package/spec/skill-export.md +126 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
flow: mindburn.flow/v1
|
|
2
|
+
title: Outreach pack
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
author: Mindburn Labs
|
|
5
|
+
license: MIT
|
|
6
|
+
tags: [sales, launch]
|
|
7
|
+
inputs:
|
|
8
|
+
research:
|
|
9
|
+
type: longtext
|
|
10
|
+
help: Source-linked market research with uncertainties preserved.
|
|
11
|
+
launch_materials:
|
|
12
|
+
type: longtext
|
|
13
|
+
help: Approved positioning and checked launch materials.
|
|
14
|
+
contact_role:
|
|
15
|
+
type: text
|
|
16
|
+
optional: true
|
|
17
|
+
help: The role you want to reach.
|
|
18
|
+
steps:
|
|
19
|
+
angles:
|
|
20
|
+
title: Find the outreach angle
|
|
21
|
+
agent: writer
|
|
22
|
+
kind: work
|
|
23
|
+
task: |
|
|
24
|
+
Use the supplied launch_materials and research to identify two honest reasons the contact_role might care. Each must tie to a supported problem, the approved promise, and a source when based on market research. Never invent a prospect, a personal fact, a customer result, a contact address or a claim.
|
|
25
|
+
pack:
|
|
26
|
+
title: Write the outreach pack
|
|
27
|
+
agent: writer
|
|
28
|
+
kind: deliver
|
|
29
|
+
needs: [angles]
|
|
30
|
+
task: |
|
|
31
|
+
Produce a reusable outreach pack with: the chosen angle and evidence; two subject lines of at most four words; a first email under 110 words with one small question; a follow-up under 60 words adding a supported point; a short direct message under 60 words; and a before-sending checklist. Use explicit [ADD: ...] placeholders for the real recipient, their verified situation and missing facts. Preserve the approved product promise. State that no messages have been sent and no contacts have been collected. This is a draft for human review, not an active campaign.
|
|
32
|
+
review:
|
|
33
|
+
title: Check the outreach pack
|
|
34
|
+
agent: reviewer
|
|
35
|
+
kind: check
|
|
36
|
+
needs: [pack]
|
|
37
|
+
checks: pack
|
|
38
|
+
task: |
|
|
39
|
+
Every product claim matches launch_materials. Every market claim matches research and keeps its source. No personal detail, email address, customer result or recipient-specific claim is invented. Required sections and word limits are met. Placeholders remain explicit. The pack says messages have not been sent.
|
|
40
|
+
done:
|
|
41
|
+
- Every claim is supported by the supplied research or launch materials.
|
|
42
|
+
- Recipient details that are not known remain explicit placeholders.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Prospect brief and first email
|
|
2
|
+
|
|
3
|
+
Give it a company and what you sell. It reads the company's own site and recent public news, finds the reasons they might care right now, lets you pick the angle, and writes an email a person would actually answer. Nothing is sent: you get the drafts.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Finds and reads the sources, and reports only what they say.
|
|
3
|
+
agentType: CUSTOM
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the researcher. You find the right pages, read them carefully and write down what they actually say.
|
|
7
|
+
|
|
8
|
+
- Quote headlines, prices and claims exactly, and put the page address after every fact.
|
|
9
|
+
- Read the pages you were asked for before looking further. Stay within the page budget you are given.
|
|
10
|
+
- A gap is a finding. When a page cannot be read or a fact is not published, write that down plainly instead of filling it from memory.
|
|
11
|
+
- You do not judge or recommend. The analyst and the writer do that from your notes.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Checks work against stated criteria and sends weak work back.
|
|
3
|
+
agentType: REVIEW
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the reviewer. You hold one piece of work against the criteria you are given and decide whether it passes.
|
|
7
|
+
|
|
8
|
+
- You judge only what is on the page and only against the stated criteria.
|
|
9
|
+
- You fail work for a clear, specific breach you can point to. You never fail it for taste, or for something nobody asked for.
|
|
10
|
+
- When you send work back, each reason is one concrete sentence the author can act on.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Chooses the angle: what to say, to whom, and why it should work.
|
|
3
|
+
agentType: CUSTOM
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the strategist. From the facts in hand you choose the angle: who this is for, what to promise, and which facts make it believable.
|
|
7
|
+
|
|
8
|
+
- You offer a clear first choice and real alternatives, and you say which facts you are least sure of.
|
|
9
|
+
- You use only facts from the inputs and earlier steps. A stronger claim that the facts do not support is not an option.
|
|
10
|
+
- The person decides. You make their decision easy to take.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Writes the finished words the person will use or send.
|
|
3
|
+
agentType: CUSTOM
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the writer. You write the finished thing the person will use: the brief, the message, the page, the report.
|
|
7
|
+
|
|
8
|
+
- Plain words, short sentences, the reader's own vocabulary. No filler and no hype.
|
|
9
|
+
- You keep to the sections, word limits and order the step asks for.
|
|
10
|
+
- You add no fact that is not in the inputs or earlier results. Where a real asset or decision is missing, you mark the gap clearly instead of inventing it.
|
|
11
|
+
- What you write should be usable as it stands.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
schemaVersion: v1alpha1
|
|
2
|
+
name: prospect-brief
|
|
3
|
+
description: |
|
|
4
|
+
Researches one company you want to sell to and writes a one-page brief plus
|
|
5
|
+
a first email and follow-up that mention real, checkable facts about them.
|
|
6
|
+
Use before reaching out to an account that matters.
|
|
7
|
+
repositories: []
|
|
8
|
+
agentDefaults:
|
|
9
|
+
harness:
|
|
10
|
+
type: claude
|
|
11
|
+
model: sonnet
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
flow: mindburn.flow/v1
|
|
2
|
+
title: Prospect brief and first email
|
|
3
|
+
author: Mindburn Labs
|
|
4
|
+
version: "1.1.0"
|
|
5
|
+
license: MIT
|
|
6
|
+
tags: [sales, research]
|
|
7
|
+
time: about 4 minutes
|
|
8
|
+
inputs:
|
|
9
|
+
prospect:
|
|
10
|
+
type: url
|
|
11
|
+
help: "The website of the company you want to reach."
|
|
12
|
+
what_you_sell:
|
|
13
|
+
type: longtext
|
|
14
|
+
help: "What you offer and who it helps, in a few sentences. Include one real result if you have one."
|
|
15
|
+
contact_role:
|
|
16
|
+
type: text
|
|
17
|
+
optional: true
|
|
18
|
+
help: "Who you plan to write to, for example \"head of operations\"."
|
|
19
|
+
steps:
|
|
20
|
+
company_facts:
|
|
21
|
+
title: Read the company's own site
|
|
22
|
+
agent: researcher
|
|
23
|
+
kind: work
|
|
24
|
+
tools: [web_fetch]
|
|
25
|
+
task: |
|
|
26
|
+
Read the prospect's home page and, from its links, the about page and one product or careers page.
|
|
27
|
+
|
|
28
|
+
Write: what the company does and for whom, its size signals (team size, locations, customers named), and what it says it cares about now (launches, hiring, expansion, stated priorities). Put the page address after every fact. If you find little, say so; do not pad.
|
|
29
|
+
recent_news:
|
|
30
|
+
title: Find recent news
|
|
31
|
+
agent: researcher
|
|
32
|
+
kind: work
|
|
33
|
+
tools: [web_search, web_fetch]
|
|
34
|
+
needs: []
|
|
35
|
+
task: |
|
|
36
|
+
Search for news about the prospect company from the last twelve months and read up to two results. Prefer the company's own announcements and established publications.
|
|
37
|
+
|
|
38
|
+
List each item with its date, what happened in one sentence, and the page address. Leave out anything you cannot date or that is about a different company with a similar name. If there is no recent news, say so plainly.
|
|
39
|
+
angles:
|
|
40
|
+
title: Find the angle
|
|
41
|
+
agent: strategist
|
|
42
|
+
kind: work
|
|
43
|
+
needs: [company_facts, recent_news]
|
|
44
|
+
task: |
|
|
45
|
+
From the company facts, the recent news and what_you_sell, propose three angles for a first message. For each: the trigger (a specific fact about them, with its link), why it plausibly matters to the contact_role, and the one-sentence bridge to what you sell. Rank them, strongest first, and say which facts you are least sure of. If the research supports fewer than three honest angles, give fewer.
|
|
46
|
+
approve_angles:
|
|
47
|
+
title: Pick the angle
|
|
48
|
+
kind: approve
|
|
49
|
+
needs: [angles]
|
|
50
|
+
shows: angles
|
|
51
|
+
task: |
|
|
52
|
+
Choose the angle to write from. Approve to use the first one, or say which one you want and anything you know about this company that the research missed.
|
|
53
|
+
email:
|
|
54
|
+
title: Write the email
|
|
55
|
+
agent: writer
|
|
56
|
+
kind: work
|
|
57
|
+
needs: [approve_angles]
|
|
58
|
+
task: |
|
|
59
|
+
Write the first email using the chosen angle. Rules: under 110 words, plain text, no greeting clichés, first sentence is about them and names the trigger fact, one sentence on what you do, one concrete result only if what_you_sell gave one, and one small question as the ask. Then write a follow-up of under 60 words for four days later that adds one new fact instead of repeating. Give two subject lines of four words or fewer.
|
|
60
|
+
brief:
|
|
61
|
+
title: Assemble the brief
|
|
62
|
+
agent: writer
|
|
63
|
+
kind: deliver
|
|
64
|
+
needs: [email]
|
|
65
|
+
task: |
|
|
66
|
+
Write one page with these sections: **Company in three lines**, **Why now** (the chosen angle and its source link), **Email** (subject lines, first email, follow-up, ready to paste), **If they reply** (three likely questions and a one-line honest answer to each, using only what_you_sell), **Sources** (every page used, as links).
|
|
67
|
+
check_email:
|
|
68
|
+
title: Check the brief
|
|
69
|
+
agent: reviewer
|
|
70
|
+
kind: check
|
|
71
|
+
needs: [brief]
|
|
72
|
+
checks: brief
|
|
73
|
+
task: |
|
|
74
|
+
Every fact about the prospect in the entire brief appears in the company facts or recent news and has a source link. Seller claims and answers use only what_you_sell. The chosen angle is preserved. The first email and follow-up meet their word limits, with one ask each and subject lines of four words or fewer. No flattery, greeting cliches or exclamation marks. All requested sections and sources are present.
|
|
75
|
+
|
|
76
|
+
done:
|
|
77
|
+
- "Every fact about the prospect has a source link."
|
|
78
|
+
- "The email could be sent as it stands."
|
|
79
|
+
- "Nothing claims a result the seller did not state."
|
package/package.json
CHANGED
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "factoryhub",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI factories, shared: the open factory format, its engine, a CLI and a local MCP server, with the house factories.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"factoryhub",
|
|
7
|
+
"ai-factories",
|
|
8
|
+
"ai-agents",
|
|
9
|
+
"agent-skills",
|
|
10
|
+
"human-in-the-loop",
|
|
11
|
+
"mcp",
|
|
12
|
+
"mcp-server",
|
|
13
|
+
"model-context-protocol",
|
|
14
|
+
"cli",
|
|
15
|
+
"anthropic",
|
|
16
|
+
"openai",
|
|
17
|
+
"openrouter",
|
|
18
|
+
"ollama"
|
|
19
|
+
],
|
|
20
|
+
"license": "Apache-2.0",
|
|
21
|
+
"author": "Mindburn Labs (https://mindburn.org)",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"homepage": "https://github.com/Mindburn-Labs/factoryhub#readme",
|
|
24
|
+
"bugs": "https://github.com/Mindburn-Labs/factoryhub/issues",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/Mindburn-Labs/factoryhub.git"
|
|
28
|
+
},
|
|
29
|
+
"mcpName": "io.github.Mindburn-Labs/factoryhub",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
33
|
+
"workspaces": [
|
|
34
|
+
"packages/engine",
|
|
35
|
+
"packages/cli",
|
|
36
|
+
"packages/mcp"
|
|
37
|
+
],
|
|
38
|
+
"bin": {
|
|
39
|
+
"factoryhub": "packages/cli/bin/factoryhub.mjs",
|
|
40
|
+
"factoryhub-mcp": "packages/mcp/bin/factoryhub-mcp.mjs"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"packages/engine/package.json",
|
|
44
|
+
"packages/engine/src",
|
|
45
|
+
"packages/engine/lib",
|
|
46
|
+
"packages/cli/package.json",
|
|
47
|
+
"packages/cli/bin",
|
|
48
|
+
"packages/cli/src",
|
|
49
|
+
"packages/mcp/package.json",
|
|
50
|
+
"packages/mcp/bin",
|
|
51
|
+
"packages/mcp/src",
|
|
52
|
+
"factories",
|
|
53
|
+
"NOTICE",
|
|
54
|
+
"spec/*.md",
|
|
55
|
+
"spec/schema"
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"test": "node --test \"packages/*/test/*.test.mjs\" \"spec/test/*.test.mjs\" \"test/*.test.mjs\"",
|
|
59
|
+
"lint": "eslint . && node scripts/check-links.mjs",
|
|
60
|
+
"validate": "node packages/cli/bin/factoryhub.mjs validate factories/*/",
|
|
61
|
+
"generate": "node scripts/generate.mjs",
|
|
62
|
+
"check": "npm run lint && npm test && npm run validate && node scripts/generate.mjs --check"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@modelcontextprotocol/server": "2.4.0",
|
|
66
|
+
"zod": "4.6.7"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@eslint/js": "10.0.1",
|
|
70
|
+
"@modelcontextprotocol/client": "2.4.0",
|
|
71
|
+
"ajv": "8.20.0",
|
|
72
|
+
"eslint": "10.15.0",
|
|
73
|
+
"globals": "17.14.0",
|
|
74
|
+
"yaml": "2.9.1"
|
|
75
|
+
},
|
|
76
|
+
"publishConfig": {
|
|
77
|
+
"access": "public",
|
|
78
|
+
"provenance": true,
|
|
79
|
+
"registry": "https://registry.npmjs.org/"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { main } from '../src/main.mjs';
|
|
4
|
+
|
|
5
|
+
// A reader that stops early, such as `| head`, is not an error.
|
|
6
|
+
process.stdout.on('error', (error) => { if (error.code === 'EPIPE') process.exit(0); throw error; });
|
|
7
|
+
|
|
8
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@factoryhub/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The factoryhub command line: validate, inspect, scaffold, pack, run and export AI factories, and find them on factoryhub.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"private": true,
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=22"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/Mindburn-Labs/factoryhub.git",
|
|
14
|
+
"directory": "packages/cli"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"factoryhub": "bin/factoryhub.mjs"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./src/main.mjs"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin",
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test \"test/*.test.mjs\""
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// A small argument parser: positional words, --name value, --name=value,
|
|
2
|
+
// boolean switches and repeatable options. Unknown options are an error, so a
|
|
3
|
+
// misspelt option never silently changes what a command does.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string[]} argv
|
|
7
|
+
* @param {{ boolean?: string[], string?: string[], repeat?: string[], alias?: Record<string,string> }} spec
|
|
8
|
+
*/
|
|
9
|
+
export function parseArgs(argv, spec = {}) {
|
|
10
|
+
const booleans = new Set(['help', ...(spec.boolean || [])]);
|
|
11
|
+
const strings = new Set(spec.string || []);
|
|
12
|
+
const repeats = new Set(spec.repeat || []);
|
|
13
|
+
const alias = { h: 'help', ...(spec.alias || {}) };
|
|
14
|
+
const flags = {};
|
|
15
|
+
const positional = [];
|
|
16
|
+
for (const name of repeats) flags[name] = [];
|
|
17
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
18
|
+
const arg = argv[i];
|
|
19
|
+
if (arg === '--') { positional.push(...argv.slice(i + 1)); break; }
|
|
20
|
+
if (!arg.startsWith('-') || arg === '-') { positional.push(arg); continue; }
|
|
21
|
+
let name;
|
|
22
|
+
let value;
|
|
23
|
+
if (arg.startsWith('--')) {
|
|
24
|
+
const eq = arg.indexOf('=');
|
|
25
|
+
name = eq > 0 ? arg.slice(2, eq) : arg.slice(2);
|
|
26
|
+
if (eq > 0) value = arg.slice(eq + 1);
|
|
27
|
+
} else {
|
|
28
|
+
name = alias[arg.slice(1)];
|
|
29
|
+
if (!name) throw new Error(`Unknown option ${arg}.`);
|
|
30
|
+
}
|
|
31
|
+
name = alias[name] || name;
|
|
32
|
+
if (booleans.has(name)) {
|
|
33
|
+
if (value !== undefined) throw new Error(`--${name} takes no value.`);
|
|
34
|
+
flags[name] = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (name.startsWith('no-') && booleans.has(name.slice(3))) { flags[name.slice(3)] = false; continue; }
|
|
38
|
+
if (!strings.has(name) && !repeats.has(name)) throw new Error(`Unknown option --${name}.`);
|
|
39
|
+
if (value === undefined) {
|
|
40
|
+
if (i + 1 >= argv.length) throw new Error(`--${name} needs a value.`);
|
|
41
|
+
value = argv[i += 1];
|
|
42
|
+
}
|
|
43
|
+
if (repeats.has(name)) flags[name].push(value);
|
|
44
|
+
else flags[name] = value;
|
|
45
|
+
}
|
|
46
|
+
return { flags, positional };
|
|
47
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
3
|
+
import { resolve, relative, join } from 'node:path';
|
|
4
|
+
import { readFactory, writeFiles, out, err, styles, usage } from '../util.mjs';
|
|
5
|
+
import { exportSkill, checkSkill } from '../../../engine/lib/skill.mjs';
|
|
6
|
+
import { DEFAULT_HUB } from '../../../engine/lib/hub.mjs';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
name: 'export',
|
|
10
|
+
summary: 'Export a factory as an Agent Skill folder (SKILL.md plus the factory).',
|
|
11
|
+
usage: `Usage: factoryhub export <factory> --as skill [--out dir] [--id id] [--force]
|
|
12
|
+
|
|
13
|
+
Writes <dir>/<id>/SKILL.md and <dir>/<id>/factory/, following
|
|
14
|
+
spec/skill-export.md. SKILL.md tells an agent what the factory produces, which
|
|
15
|
+
inputs to collect, every step in order with who does it, where to stop and
|
|
16
|
+
ask the person to approve, and how to run it with this CLI or by hand.
|
|
17
|
+
factory/ holds the factory's files unchanged, plus the foreman written from
|
|
18
|
+
the flow when the author wrote none.
|
|
19
|
+
|
|
20
|
+
Copy the folder into your agent's skills directory, for example
|
|
21
|
+
~/.claude/skills/ for Claude Code.
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--as the export format; "skill" is the only one
|
|
25
|
+
--out the parent folder (default: the current folder)
|
|
26
|
+
--id the skill name and factoryhub id (default: the factory's name)
|
|
27
|
+
--hub the factoryhub address used for the source link (default: ${DEFAULT_HUB})
|
|
28
|
+
--force replace an earlier export of the same factory`,
|
|
29
|
+
options: { boolean: ['force'], string: ['as', 'out', 'id', 'hub'] },
|
|
30
|
+
async run({ positional, flags }, io) {
|
|
31
|
+
const [target] = positional;
|
|
32
|
+
if (!target) throw usage('Name a factory to export.');
|
|
33
|
+
if (flags.as !== 'skill') throw usage('Choose the export format: --as skill');
|
|
34
|
+
const read = await readFactory(target, { cwd: io.cwd, fetchImpl: io.fetch });
|
|
35
|
+
const skill = await exportSkill(read.files, { id: flags.id, hub: flags.hub || DEFAULT_HUB });
|
|
36
|
+
const folder = resolve(io.cwd, flags.out || '.', skill.id);
|
|
37
|
+
const s = styles(io);
|
|
38
|
+
// Checked before anything is written: a failing export leaves no files.
|
|
39
|
+
const problems = await checkSkill(skill.files, { folder: skill.id, hub: flags.hub || DEFAULT_HUB });
|
|
40
|
+
if (problems.length) {
|
|
41
|
+
err(io, `${s.red('not exported')} ${skill.id}: the export fails its checks`);
|
|
42
|
+
for (const problem of problems) err(io, ` - ${problem}`);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
if (existsSync(folder)) {
|
|
46
|
+
if (!flags.force) throw usage(`${folder} already exists. Pass --force to replace it.`);
|
|
47
|
+
// Replace an earlier export as a whole, so no stale file stays behind.
|
|
48
|
+
if (!existsSync(join(folder, 'SKILL.md'))) throw usage(`${folder} is not an earlier skill export, so it was left alone.`);
|
|
49
|
+
await rm(folder, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
await writeFiles(folder, skill.files, { allow: (path) => path === 'SKILL.md' || path.startsWith('factory/') });
|
|
52
|
+
out(io, `${s.green('exported')} ${relative(io.cwd, folder) || folder}/SKILL.md and factory/ (${Object.keys(skill.files).length - 1} files), sha256 ${skill.sha256}`);
|
|
53
|
+
return 0;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve, relative } from 'node:path';
|
|
3
|
+
import { engine, writeFiles, out, styles, usage } from '../util.mjs';
|
|
4
|
+
import { hubClient, DEFAULT_HUB } from '../../../engine/lib/hub.mjs';
|
|
5
|
+
import { HUB_NOTE } from './search.mjs';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
name: 'get',
|
|
9
|
+
summary: 'Download a published factory from factoryhub into a folder, checking every file.',
|
|
10
|
+
usage: `Usage: factoryhub get <id> [--out dir] [--base-url url] [--force]
|
|
11
|
+
|
|
12
|
+
Downloads the factory's transfer bundle, checks every file against its
|
|
13
|
+
recorded SHA-256, and writes the files to <dir>/<id>/.
|
|
14
|
+
|
|
15
|
+
${HUB_NOTE}
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--out the parent folder (default: the current folder)
|
|
19
|
+
--base-url the factoryhub address (default: FACTORYHUB_BASE_URL or ${DEFAULT_HUB})
|
|
20
|
+
--force write into an existing folder`,
|
|
21
|
+
options: { boolean: ['force'], string: ['out', 'base-url'] },
|
|
22
|
+
async run({ positional, flags }, io) {
|
|
23
|
+
const [id] = positional;
|
|
24
|
+
if (!id) throw usage('Name the factory to get: factoryhub get <id>');
|
|
25
|
+
const hub = hubClient({ baseUrl: flags['base-url'], env: io.env, fetchImpl: io.fetch });
|
|
26
|
+
const factory = await hub.get(id);
|
|
27
|
+
const folder = resolve(io.cwd, flags.out || '.', id);
|
|
28
|
+
if (existsSync(folder) && !flags.force) throw usage(`${folder} already exists. Pass --force to write into it.`);
|
|
29
|
+
await writeFiles(folder, factory.files);
|
|
30
|
+
const parsed = engine.parseBundle(factory.files);
|
|
31
|
+
const s = styles(io);
|
|
32
|
+
const state = !parsed.ok ? s.red('invalid') : parsed.factory.runnable ? s.green('runs here') : s.yellow('does not run here');
|
|
33
|
+
out(io, `${s.green('saved')} ${relative(io.cwd, folder) || folder}/ ${parsed.factory.title} v${parsed.factory.version}, ${Object.keys(factory.files).length} files, ${state}`);
|
|
34
|
+
if (factory.leftOut.length) out(io, `Not part of the factory, so not written: ${factory.leftOut.join(', ')}`);
|
|
35
|
+
out(io, `Source: ${factory.source}`);
|
|
36
|
+
return parsed.ok ? 0 : 1;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { engine, readFactory, summarize, out, json, table, plural, styles, usage, canonicalFiles } from '../util.mjs';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'inspect',
|
|
5
|
+
summary: 'Show what a factory does: inputs, steps in order, agents, tools, checks.',
|
|
6
|
+
usage: `Usage: factoryhub inspect <factory> [--json]
|
|
7
|
+
|
|
8
|
+
Prints a factory's inputs, its steps stage by stage (steps in one stage can
|
|
9
|
+
run at the same time), who does each, its agents, tools, and the
|
|
10
|
+
statements its deliverable is checked against. A factory is a folder, a
|
|
11
|
+
bundle .json file, an https address of a bundle, or a house factory id.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--json print the full summary as JSON, including the JSON Schema of the inputs`,
|
|
15
|
+
options: { boolean: ['json'] },
|
|
16
|
+
async run({ positional, flags }, io) {
|
|
17
|
+
const [target] = positional;
|
|
18
|
+
if (!target) throw usage('Name a factory to inspect.');
|
|
19
|
+
const read = await readFactory(target, { cwd: io.cwd, fetchImpl: io.fetch });
|
|
20
|
+
const summary = await summarize(read.files, { id: read.source.id });
|
|
21
|
+
if (flags.json) { json(io, summary); return summary.valid ? 0 : 1; }
|
|
22
|
+
const s = styles(io);
|
|
23
|
+
if (!summary.valid) {
|
|
24
|
+
out(io, `${s.red('invalid')} ${target}`);
|
|
25
|
+
for (const error of summary.errors) out(io, ` - ${error}`);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const { factory } = engine.parseBundle(canonicalFiles(read.files));
|
|
29
|
+
out(io, `${s.bold(summary.title)} ${s.dim(`v${summary.version}${summary.license ? `, ${summary.license}` : ''}${summary.author ? `, by ${summary.author}` : ''}`)}`);
|
|
30
|
+
out(io, summary.description);
|
|
31
|
+
out(io, s.dim(`${summary.name} sha256 ${summary.sha256}${summary.time ? ` ${summary.time}` : ''} ${summary.runnable ? 'runs here' : 'does not run here'}`));
|
|
32
|
+
out(io);
|
|
33
|
+
out(io, s.bold('Inputs'));
|
|
34
|
+
out(io, summary.inputs.length ? table(summary.inputs.map((input) => [` ${input.key}`, input.type, input.required ? 'required' : 'optional', input.help])) : ' none');
|
|
35
|
+
out(io);
|
|
36
|
+
if (factory.steps.length) {
|
|
37
|
+
const { columns } = engine.layout(factory);
|
|
38
|
+
out(io, s.bold(`Steps (${plural(factory.steps.length, 'step')} in ${plural(columns.length, 'stage')})`));
|
|
39
|
+
const rows = [];
|
|
40
|
+
columns.forEach((ids, stage) => {
|
|
41
|
+
for (const id of ids) {
|
|
42
|
+
const step = factory.steps.find((item) => item.id === id);
|
|
43
|
+
const who = step.kind === 'approve' ? 'the person' : step.agent;
|
|
44
|
+
const detail = step.kind === 'check' ? `checks ${step.target}` : step.kind === 'approve' ? `shows ${step.target}, waits for a decision` : step.needs.length ? `after ${step.needs.join(', ')}` : 'first';
|
|
45
|
+
const tools = [...step.tools, ...step.missing];
|
|
46
|
+
rows.push([` ${stage + 1}`, step.id, step.kind, who, `${detail}${tools.length ? `; tools ${tools.join(', ')}` : ''}`]);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
out(io, table(rows));
|
|
50
|
+
} else out(io, `${s.bold('Steps')}\n none: this factory has no flow`);
|
|
51
|
+
out(io);
|
|
52
|
+
out(io, s.bold('Agents'));
|
|
53
|
+
out(io, table(summary.agents.map((agent) => [` ${agent.name}`, agent.type === 'FOREMAN' ? 'coordinates the flow (generated from it)' : agent.description])));
|
|
54
|
+
if (summary.done.length) {
|
|
55
|
+
out(io);
|
|
56
|
+
out(io, s.bold('Done when'));
|
|
57
|
+
for (const line of summary.done) out(io, ` - ${line}`);
|
|
58
|
+
}
|
|
59
|
+
if (summary.blockers.length) {
|
|
60
|
+
out(io);
|
|
61
|
+
out(io, s.bold('Cannot run here'));
|
|
62
|
+
for (const blocker of summary.blockers) out(io, ` - ${blocker}`);
|
|
63
|
+
}
|
|
64
|
+
return 0;
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { readFolder, listFactoryDirs, summarize, matches, out, json, table, HOUSE_FACTORIES } from '../util.mjs';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
name: 'list',
|
|
6
|
+
summary: 'List the house factories, or the factory folders in a directory.',
|
|
7
|
+
usage: `Usage: factoryhub list [directory] [--query words] [--json]
|
|
8
|
+
|
|
9
|
+
Lists the factory folders directly inside a directory. With no directory,
|
|
10
|
+
lists the house factories that ship with factoryhub.
|
|
11
|
+
|
|
12
|
+
Options:
|
|
13
|
+
--query keep factories whose id, title, description or tags contain every word
|
|
14
|
+
--json print the summaries as JSON`,
|
|
15
|
+
options: { boolean: ['json'], string: ['query'] },
|
|
16
|
+
async run({ positional, flags }, io) {
|
|
17
|
+
const dir = positional[0] ? resolve(io.cwd, positional[0]) : HOUSE_FACTORIES;
|
|
18
|
+
const entries = [];
|
|
19
|
+
for (const id of await listFactoryDirs(dir)) {
|
|
20
|
+
const { files } = await readFolder(resolve(dir, id));
|
|
21
|
+
entries.push(await summarize(files, { id }));
|
|
22
|
+
}
|
|
23
|
+
const kept = entries.filter((entry) => matches(entry, flags.query));
|
|
24
|
+
if (flags.json) { json(io, kept); return 0; }
|
|
25
|
+
if (!kept.length) { out(io, entries.length ? 'No factory matches.' : `No factory folders in ${dir}.`); return 0; }
|
|
26
|
+
out(io, table([['ID', 'VERSION', 'STEPS', 'INPUTS', 'TITLE'], ...kept.map((entry) => [entry.id, entry.version, entry.steps.length, entry.inputs.length, entry.valid ? entry.title : `${entry.title} (invalid)`])]));
|
|
27
|
+
return 0;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Starts the local MCP server on stdio: the same server as the factoryhub-mcp bin.
|
|
2
|
+
// It exists so one published package starts its MCP server with
|
|
3
|
+
// `npx factoryhub mcp`, the form MCP registries and clients build from
|
|
4
|
+
// server.json (identifier + version, then the package arguments).
|
|
5
|
+
// stdout carries MCP only; the server logs to stderr.
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
name: 'mcp',
|
|
9
|
+
summary: 'Start the local MCP server on stdio, for MCP clients.',
|
|
10
|
+
usage: `Usage: factoryhub mcp
|
|
11
|
+
|
|
12
|
+
Starts the factoryhub MCP server on stdin and stdout, the same server as the
|
|
13
|
+
factoryhub-mcp command. An MCP client starts it; it is not meant to be typed.
|
|
14
|
+
Model keys and folders come from the environment: ANTHROPIC_API_KEY,
|
|
15
|
+
OPENAI_API_KEY or OPENROUTER_API_KEY (only to run factories), FACTORYHUB_MODEL,
|
|
16
|
+
FACTORYHUB_FACTORIES and FACTORYHUB_RUNS_DIR.`,
|
|
17
|
+
options: {},
|
|
18
|
+
async run() {
|
|
19
|
+
// Loaded here, not at the top, so other commands do not pay for the MCP SDK.
|
|
20
|
+
const { serveStdio } = await import('@modelcontextprotocol/server/stdio');
|
|
21
|
+
const { createServer } = await import('../../../mcp/src/server.mjs');
|
|
22
|
+
serveStdio(() => createServer());
|
|
23
|
+
// The open stdin keeps the process alive until the client closes it.
|
|
24
|
+
return 0;
|
|
25
|
+
},
|
|
26
|
+
};
|