factoryhub 0.0.0-stage → 0.1.1
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 +195 -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 +587 -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/newsletter-note/README.md +11 -0
- package/factories/newsletter-note/agents/reviewer/agent.md +10 -0
- package/factories/newsletter-note/agents/writer/agent.md +11 -0
- package/factories/newsletter-note/factory.yaml +12 -0
- package/factories/newsletter-note/flow.yaml +45 -0
- package/factories/newsletter-note/mindburn.tests.yaml +80 -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/factories/release-desk/README.md +13 -0
- package/factories/release-desk/agents/editor/agent.md +10 -0
- package/factories/release-desk/agents/reviewer/agent.md +10 -0
- package/factories/release-desk/factory.yaml +13 -0
- package/factories/release-desk/flow.yaml +67 -0
- package/factories/release-desk/mindburn.tests.yaml +163 -0
- package/factories/short-video-cut/README.md +11 -0
- package/factories/short-video-cut/agents/reviewer/agent.md +10 -0
- package/factories/short-video-cut/agents/writer/agent.md +11 -0
- package/factories/short-video-cut/factory.yaml +13 -0
- package/factories/short-video-cut/flow.yaml +46 -0
- package/factories/short-video-cut/mindburn.tests.yaml +80 -0
- package/factories/video-facts/README.md +11 -0
- package/factories/video-facts/agents/analyst/agent.md +10 -0
- package/factories/video-facts/agents/reviewer/agent.md +10 -0
- package/factories/video-facts/factory.yaml +13 -0
- package/factories/video-facts/flow.yaml +39 -0
- package/factories/video-facts/mindburn.tests.yaml +34 -0
- package/factories/video-to-posts/README.md +35 -0
- package/factories/video-to-posts/agents/analyst/agent.md +10 -0
- package/factories/video-to-posts/agents/reviewer/agent.md +10 -0
- package/factories/video-to-posts/agents/writer/agent.md +11 -0
- package/factories/video-to-posts/factory.yaml +12 -0
- package/factories/video-to-posts/flow.yaml +62 -0
- package/factories/video-to-posts/mindburn.tests.yaml +34 -0
- package/factories/week-of-posts/README.md +11 -0
- package/factories/week-of-posts/agents/reviewer/agent.md +10 -0
- package/factories/week-of-posts/agents/writer/agent.md +11 -0
- package/factories/week-of-posts/factory.yaml +12 -0
- package/factories/week-of-posts/flow.yaml +46 -0
- package/factories/week-of-posts/mindburn.tests.yaml +80 -0
- package/package.json +92 -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 +54 -0
- package/packages/cli/src/commands/get.mjs +37 -0
- package/packages/cli/src/commands/inspect.mjs +65 -0
- package/packages/cli/src/commands/list.mjs +24 -0
- package/packages/cli/src/commands/login.mjs +161 -0
- package/packages/cli/src/commands/logout.mjs +43 -0
- package/packages/cli/src/commands/mcp.mjs +25 -0
- package/packages/cli/src/commands/new.mjs +147 -0
- package/packages/cli/src/commands/pack.mjs +96 -0
- package/packages/cli/src/commands/publish.mjs +62 -0
- package/packages/cli/src/commands/pull.mjs +59 -0
- package/packages/cli/src/commands/push.mjs +115 -0
- package/packages/cli/src/commands/resume.mjs +66 -0
- package/packages/cli/src/commands/run.mjs +352 -0
- package/packages/cli/src/commands/runs.mjs +52 -0
- package/packages/cli/src/commands/search.mjs +30 -0
- package/packages/cli/src/commands/validate.mjs +79 -0
- package/packages/cli/src/commands/whoami.mjs +34 -0
- package/packages/cli/src/hosted.mjs +159 -0
- package/packages/cli/src/main.mjs +134 -0
- package/packages/cli/src/tool-sources.mjs +272 -0
- package/packages/cli/src/util.mjs +58 -0
- package/packages/engine/lib/catalog.mjs +50 -0
- package/packages/engine/lib/credentials.mjs +121 -0
- package/packages/engine/lib/hosted.mjs +119 -0
- package/packages/engine/lib/hub.mjs +423 -0
- package/packages/engine/lib/nango.mjs +157 -0
- package/packages/engine/lib/node.mjs +242 -0
- package/packages/engine/lib/providers.mjs +57 -0
- package/packages/engine/lib/run.mjs +192 -0
- package/packages/engine/lib/skill.mjs +229 -0
- package/packages/engine/lib/text.mjs +5 -0
- package/packages/engine/package.json +45 -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 +2109 -0
- package/packages/engine/src/flow-sources.mjs +135 -0
- package/packages/engine/src/index.mjs +9 -0
- package/packages/engine/src/markdown.mjs +97 -0
- package/packages/engine/src/mcp-client.mjs +429 -0
- package/packages/engine/src/package.mjs +116 -0
- package/packages/engine/src/run.mjs +308 -0
- package/packages/engine/src/tool-sources.mjs +566 -0
- package/packages/mcp/bin/factoryhub-mcp.mjs +6 -0
- package/packages/mcp/package.json +36 -0
- package/packages/mcp/src/server.mjs +310 -0
- package/packages/sdk/package.json +28 -0
- package/packages/sdk/src/index.d.ts +163 -0
- package/packages/sdk/src/index.mjs +266 -0
- package/spec/README.md +42 -0
- package/spec/factory-format.md +866 -0
- package/spec/schema/factory.schema.json +43 -0
- package/spec/schema/factory.yaml.json +815 -0
- package/spec/schema/flow.schema.json +191 -0
- package/spec/skill-export.md +129 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# One video, a week of posts
|
|
2
|
+
|
|
3
|
+
Paste the transcript of a video, podcast or talk. You get a week of posts for X, LinkedIn, a short video and a newsletter, each built on something you actually said, with the timestamp it came from. A checker holds every number and quote against the transcript and sends back any line it does not support. Nothing is final until you approve it, and nothing is posted for you: you copy what you approve.
|
|
4
|
+
|
|
5
|
+
## Try it on the example
|
|
6
|
+
|
|
7
|
+
The example is invented: Maya, her flat and her video do not exist. Her figures are easy to round up, such as "two weekends. Maybe three" and "about four, no, three pounds", so the check has something real to hold the posts against. Paste the transcript below, and the line under "About you" if you want it.
|
|
8
|
+
|
|
9
|
+
Transcript:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
[0:00] So this is the story of how a frying pan made me build an app.
|
|
13
|
+
[0:06] Our flat group chat had 212 messages in one month. I counted. Most of them were about one frying pan that nobody would wash.
|
|
14
|
+
[0:18] There are five of us in the flat. Everyone thought they did the most dishes. Everyone was wrong, including me.
|
|
15
|
+
[0:31] I'm not a programmer. I did one coding course at school and forgot most of it. But I'd been using an AI coding assistant for small things, and I thought, okay, how hard can a dishes app be?
|
|
16
|
+
[0:47] It took me two weekends. Maybe three, if you count the weekend I spent on the logo.
|
|
17
|
+
[0:55] And that's my first lesson. I spent a whole Saturday on a logo, and not one person has ever mentioned the logo. Not one.
|
|
18
|
+
[1:06] Here's how it works, and honestly the trick is how little it does. When something needs washing, you tap one button. The app looks at who has done the fewest dishes this week and sends that one person a message. That's it. No points, no leaderboard, no badges.
|
|
19
|
+
[1:27] I tried points first. Points made it worse. One flatmate started washing single teaspoons to collect points.
|
|
20
|
+
[1:38] So here's my hot take: most chore apps fail because they turn chores into a game. Nobody in my flat wanted a game. We wanted the arguing to stop.
|
|
21
|
+
[1:50] Did it work? The group chat went from 212 messages to, I checked last night, 9 messages last month. And three of those were about pizza.
|
|
22
|
+
[2:04] Then a friend posted about it, and people from other flats started asking for it. 38 people signed up in the first week. Mostly friends of friends, to be fair. 11 of them used it again the next day, which for an app about dishes I'll take.
|
|
23
|
+
[2:22] It costs me about four, no, three pounds a month to run. Three.
|
|
24
|
+
[2:28] What I'd do differently: skip the logo, and ask my flatmates what annoyed them before writing a single line. I guessed it was fairness. It was the frying pan. It's always one frying pan.
|
|
25
|
+
[2:42] If you build something like this, start with the one message the app sends. Write that message first. Everything else is decoration.
|
|
26
|
+
[2:51] Anyway. The pan is clean. For now.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
About you:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
I'm Maya. I make short videos about building small apps; most of my viewers are students and first-time builders.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The same example is in `mindburn.tests.yaml`. With the downloaded folder, `node run.mjs --test` runs it and checks that the posts keep the sections and none of the easy roundings. Passing checks are a repeatable regression signal, not proof that every transcript will be handled correctly. Read the posts against your transcript before you copy them.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Turns raw material into structured findings that can be checked.
|
|
3
|
+
agentType: CUSTOM
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the analyst. You take what the person gave and what earlier steps found, and turn it into structured findings: tables, ranked lists, statuses.
|
|
7
|
+
|
|
8
|
+
- Every finding points at the exact words, file or fact it rests on.
|
|
9
|
+
- You never assume what the material does not show. Where something cannot be told, you say what is missing.
|
|
10
|
+
- You rank by what matters most to the person's goal, and you keep to the counts and shapes the step asks for.
|
|
@@ -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,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,12 @@
|
|
|
1
|
+
schemaVersion: v1alpha1
|
|
2
|
+
name: video-to-posts
|
|
3
|
+
description: |
|
|
4
|
+
Turns the transcript of one video, podcast or talk into a week of posts for
|
|
5
|
+
X, LinkedIn, a short video and a newsletter, using only what the speaker
|
|
6
|
+
said. Use after you put out a video on YouTube or TikTok, or a podcast
|
|
7
|
+
episode, and want posts that say nothing you did not say.
|
|
8
|
+
repositories: []
|
|
9
|
+
agentDefaults:
|
|
10
|
+
harness:
|
|
11
|
+
type: claude
|
|
12
|
+
model: sonnet
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
flow: mindburn.flow/v1
|
|
2
|
+
title: One video, a week of posts
|
|
3
|
+
author: Mindburn Labs
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
license: MIT
|
|
6
|
+
tags: [social, marketing]
|
|
7
|
+
time: about 3 minutes
|
|
8
|
+
inputs:
|
|
9
|
+
transcript:
|
|
10
|
+
type: longtext
|
|
11
|
+
help: "The transcript of your video, podcast or talk, pasted as it is. Keep the timestamps if it has them: each post then points back to the moment it came from."
|
|
12
|
+
about_you:
|
|
13
|
+
type: text
|
|
14
|
+
optional: true
|
|
15
|
+
help: "Who you are and who watches, for example \"I make short videos about building small apps; most of my viewers are students\". It sets the voice, not the facts."
|
|
16
|
+
steps:
|
|
17
|
+
moments:
|
|
18
|
+
title: Find the moments worth posting
|
|
19
|
+
agent: analyst
|
|
20
|
+
kind: work
|
|
21
|
+
task: |
|
|
22
|
+
Read the whole transcript. First write a fact sheet: every number, name, date, result and claim the speaker states, each with the speaker's exact words and its timestamp, if the transcript has timestamps. Keep every qualifier the speaker attaches to a fact, such as "maybe", "about" or "mostly". Where the speaker corrects themselves, the corrected figure is the fact; note the correction. Where the transcript is garbled, or a figure is said two ways without a correction, mark it "unclear" instead of choosing.
|
|
23
|
+
|
|
24
|
+
Then pick the five moments most worth posting: a surprising number or result, a short story with a turn, a strong opinion, a how-to someone could copy, a mistake and its lesson. For each give a one-line title, the exact quote (under 40 words), its timestamp, and why a stranger would stop scrolling for it. Rank them, strongest first. Do not pick a moment whose point the transcript does not make. If the transcript supports fewer than five honest moments, give fewer.
|
|
25
|
+
posts:
|
|
26
|
+
title: Write the week of posts
|
|
27
|
+
agent: writer
|
|
28
|
+
kind: deliver
|
|
29
|
+
needs: [moments]
|
|
30
|
+
task: |
|
|
31
|
+
Write a week of posts from the moments, in the speaker's voice and, wherever you can, in the speaker's own words. Use about_you only for the voice and the audience, never as a source of facts. Sections, in this order:
|
|
32
|
+
|
|
33
|
+
1. **The week at a glance**: a table with columns Day (1 to 7), Channel, Moment, First line.
|
|
34
|
+
2. **X**: one thread of 4 to 6 posts built on the strongest moment, and two single posts on other moments. No post is longer than 280 characters.
|
|
35
|
+
3. **LinkedIn**: one post under 200 words whose first line works on its own.
|
|
36
|
+
4. **Short video**: a 30-second script for TikTok, Reels or Shorts: the hook to say in the first two seconds, three beats and a closing line. For each beat, give the timestamp of the original clip to cut in or, if the transcript has no timestamps, the clip's first words.
|
|
37
|
+
5. **Newsletter**: a note under 120 words that points readers to the full video, with "[ADD: link to the video]" where the link goes.
|
|
38
|
+
6. **Before you post**: every factual claim the posts make, each with the quote and timestamp it rests on; every person or company the posts name other than the speaker, so you can check they are happy to be named; and anything the speaker should double-check.
|
|
39
|
+
|
|
40
|
+
Every fact comes from the transcript, as the fact sheet records it. Never round a number, shorten a time span, make a result bigger or more certain, drop a qualifier that keeps a claim modest, name a person or quote words the speaker did not say. Where a post would be stronger with a real asset, such as a screenshot or a link, write "[ADD: what belongs here]" instead of inventing it. Add no hype the speaker did not use, and no exclamation marks or emojis unless the transcript has them. Use at most two hashtags a post.
|
|
41
|
+
check_posts:
|
|
42
|
+
title: Check every line against the transcript
|
|
43
|
+
agent: reviewer
|
|
44
|
+
kind: check
|
|
45
|
+
needs: [posts]
|
|
46
|
+
checks: posts
|
|
47
|
+
task: |
|
|
48
|
+
Compare every line of every post with the transcript and the fact sheet. Each number, time span, name, result and quote appears in the transcript with the same meaning. Nothing is rounded, shortened, made bigger or more certain, or moved to a different context, and quotes are word for word. A qualifier the speaker attached stays wherever leaving it out would make the claim sound bigger or more certain. A self-corrected figure uses the corrected value; an unclear one is not stated as fact. Send back any line the transcript does not support.
|
|
49
|
+
|
|
50
|
+
Then check the shape: all six sections are present and in order, no X post is longer than 280 characters, the LinkedIn post and the newsletter note keep to their word limits, each beat of the short-video script points to a timestamp or to the first words of a clip, and every factual claim appears in "Before you post". When you send work back, quote the post's words and the transcript's words side by side so the writer can correct them.
|
|
51
|
+
approve_posts:
|
|
52
|
+
title: Approve the posts
|
|
53
|
+
kind: approve
|
|
54
|
+
needs: [check_posts]
|
|
55
|
+
shows: posts
|
|
56
|
+
task: |
|
|
57
|
+
These go out under your name. Approve them, or say what to change: a moment to drop, a line that does not sound like you, a fact that is wrong. Nothing is posted for you: you copy what you approve.
|
|
58
|
+
|
|
59
|
+
done:
|
|
60
|
+
- "Every number, time span, name and quote in the posts is in the transcript, with the same meaning."
|
|
61
|
+
- "Every post can be copied as it stands, or marks plainly what to add."
|
|
62
|
+
- "Every factual claim is listed under \"Before you post\" with the words it rests on."
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
cases:
|
|
3
|
+
frying_pan_app:
|
|
4
|
+
name: Keeps every figure exactly as the speaker said it
|
|
5
|
+
inputs:
|
|
6
|
+
transcript: |
|
|
7
|
+
[0:00] So this is the story of how a frying pan made me build an app.
|
|
8
|
+
[0:06] Our flat group chat had 212 messages in one month. I counted. Most of them were about one frying pan that nobody would wash.
|
|
9
|
+
[0:18] There are five of us in the flat. Everyone thought they did the most dishes. Everyone was wrong, including me.
|
|
10
|
+
[0:31] I'm not a programmer. I did one coding course at school and forgot most of it. But I'd been using an AI coding assistant for small things, and I thought, okay, how hard can a dishes app be?
|
|
11
|
+
[0:47] It took me two weekends. Maybe three, if you count the weekend I spent on the logo.
|
|
12
|
+
[0:55] And that's my first lesson. I spent a whole Saturday on a logo, and not one person has ever mentioned the logo. Not one.
|
|
13
|
+
[1:06] Here's how it works, and honestly the trick is how little it does. When something needs washing, you tap one button. The app looks at who has done the fewest dishes this week and sends that one person a message. That's it. No points, no leaderboard, no badges.
|
|
14
|
+
[1:27] I tried points first. Points made it worse. One flatmate started washing single teaspoons to collect points.
|
|
15
|
+
[1:38] So here's my hot take: most chore apps fail because they turn chores into a game. Nobody in my flat wanted a game. We wanted the arguing to stop.
|
|
16
|
+
[1:50] Did it work? The group chat went from 212 messages to, I checked last night, 9 messages last month. And three of those were about pizza.
|
|
17
|
+
[2:04] Then a friend posted about it, and people from other flats started asking for it. 38 people signed up in the first week. Mostly friends of friends, to be fair. 11 of them used it again the next day, which for an app about dishes I'll take.
|
|
18
|
+
[2:22] It costs me about four, no, three pounds a month to run. Three.
|
|
19
|
+
[2:28] What I'd do differently: skip the logo, and ask my flatmates what annoyed them before writing a single line. I guessed it was fairness. It was the frying pan. It's always one frying pan.
|
|
20
|
+
[2:42] If you build something like this, start with the one message the app sends. Write that message first. Everything else is decoration.
|
|
21
|
+
[2:51] Anyway. The pan is clean. For now.
|
|
22
|
+
# Invented sample: Maya, her flat and this video do not exist. The
|
|
23
|
+
# disclosure stays out of the inputs so it does not leak into the posts;
|
|
24
|
+
# the README carries it.
|
|
25
|
+
about_you: "I'm Maya. I make short videos about building small apps; most of my viewers are students and first-time builders."
|
|
26
|
+
# Contains: the sections the flow asks for and the subject of the video.
|
|
27
|
+
# Excludes: the easy roundings of what Maya said (two weekends, maybe
|
|
28
|
+
# three; one coding course; 38 sign-ups; three pounds after a correction)
|
|
29
|
+
# and a result she never claims.
|
|
30
|
+
expect:
|
|
31
|
+
contains: ["LinkedIn", "Short video", "Newsletter", "Before you post", "frying pan"]
|
|
32
|
+
excludes: ["went viral", "in one weekend", "in a weekend", "40 people", "nearly 40", "almost 40", "never coded", "£4", "four pounds a month"]
|
|
33
|
+
min_length: 1500
|
|
34
|
+
max_length: 16000
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# X and LinkedIn posts from the facts
|
|
2
|
+
|
|
3
|
+
Paste the facts sheet of a video, podcast or talk, and the transcript if you have it. You get an X thread of 4 to 6 posts, two single posts and a LinkedIn post, in your voice and wherever possible in your own words, with a list of every fact they use and the words it rests on. A checker holds every line against the facts sheet and sends back anything stronger than what you said: a rounded number, a dropped "maybe", a result made bigger. Nothing is posted for you.
|
|
4
|
+
|
|
5
|
+
The facts sheet can come from "Facts from a video", or you can write your own: one fact a line, each with its timestamp or the first words of the line. "Release desk" puts these posts together with the other pieces for one approval.
|
|
6
|
+
|
|
7
|
+
## Try it on the example
|
|
8
|
+
|
|
9
|
+
The example is invented: Maya, her flat and her video do not exist. Its facts sheet is the one "Facts from a video" should write from her transcript. On the factory page, "Fill in the example" pastes it for you.
|
|
10
|
+
|
|
11
|
+
The example is in `mindburn.tests.yaml`. With the downloaded folder, `node run.mjs --test` runs it and checks that the posts keep the sections and none of the easy roundings. Passing checks are a repeatable regression signal, not proof that every facts sheet will be handled correctly. Read the posts against your facts before you copy them.
|
|
@@ -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,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,12 @@
|
|
|
1
|
+
schemaVersion: v1alpha1
|
|
2
|
+
name: week-of-posts
|
|
3
|
+
description: |
|
|
4
|
+
Writes an X thread, two single posts and a LinkedIn post from the facts
|
|
5
|
+
sheet of one video, podcast or talk, in the speaker's voice, saying nothing
|
|
6
|
+
the facts sheet does not say. Use after "Facts from a video", or with your
|
|
7
|
+
own list of what you said, when you want posts you can put your name to.
|
|
8
|
+
repositories: []
|
|
9
|
+
agentDefaults:
|
|
10
|
+
harness:
|
|
11
|
+
type: claude
|
|
12
|
+
model: sonnet
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
flow: mindburn.flow/v1
|
|
2
|
+
title: X and LinkedIn posts from the facts
|
|
3
|
+
author: Mindburn Labs
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
license: MIT
|
|
6
|
+
tags: [social, marketing]
|
|
7
|
+
time: about 2 minutes
|
|
8
|
+
inputs:
|
|
9
|
+
facts:
|
|
10
|
+
type: longtext
|
|
11
|
+
help: "The facts sheet from \"Facts from a video\", or your own list of what you said: one fact a line, each with its timestamp or the first words of the line."
|
|
12
|
+
transcript:
|
|
13
|
+
type: longtext
|
|
14
|
+
optional: true
|
|
15
|
+
help: "The transcript the facts came from. With it, the posts can use your exact wording and the checker can quote it when it sends a line back."
|
|
16
|
+
about_you:
|
|
17
|
+
type: text
|
|
18
|
+
optional: true
|
|
19
|
+
help: "Who you are and who watches, for example \"I make short videos about building small apps; most of my viewers are students\". It sets the voice, not the facts."
|
|
20
|
+
steps:
|
|
21
|
+
posts:
|
|
22
|
+
title: Write the X thread and the LinkedIn post
|
|
23
|
+
agent: writer
|
|
24
|
+
kind: deliver
|
|
25
|
+
task: |
|
|
26
|
+
Write posts from the facts sheet, in the speaker's voice and, wherever you can, in the speaker's own words as the sheet quotes them. Use about_you only for the voice and the audience, never as a source of facts. Sections, in this order:
|
|
27
|
+
|
|
28
|
+
1. **X**: one thread of 4 to 6 numbered posts built on the strongest fact or story on the sheet, then two single posts on other facts. No post is longer than 280 characters.
|
|
29
|
+
2. **LinkedIn**: one post under 200 words whose first line works on its own.
|
|
30
|
+
3. **Facts used**: every factual claim the posts make, each with the facts-sheet ID if the sheet has IDs, its timestamp or first words, and the exact words it rests on.
|
|
31
|
+
|
|
32
|
+
Every fact comes from the facts sheet. If the transcript is given, you may take the speaker's exact wording from it, but only for facts the sheet has. Never round a number, shorten or lengthen a time span, make a result bigger or more certain, drop a qualifier that keeps a claim modest, state an opinion as a fact, work out a figure the sheet does not state, such as a percentage, or name a person the sheet does not name. A fact the sheet marks "unclear" is not stated as settled. Where a post would be stronger with a real asset, such as a screenshot or a link, write "[ADD: what belongs here]" instead of inventing it. Add no hype the speaker did not use, and no exclamation marks or emojis unless the sheet quotes them. Use at most two hashtags a post.
|
|
33
|
+
check_posts:
|
|
34
|
+
title: Check every line against the facts sheet
|
|
35
|
+
agent: reviewer
|
|
36
|
+
kind: check
|
|
37
|
+
needs: [posts]
|
|
38
|
+
checks: posts
|
|
39
|
+
task: |
|
|
40
|
+
Compare every line of every post with the facts sheet, and with the transcript if it is given. Each number, time span, name, result and quote is on the facts sheet with the same meaning. Send back any line that says more than the source: a rounded or worked-out figure, a shorter or longer time span, a result made bigger or more certain, a dropped qualifier such as "maybe", "about" or "mostly", an opinion stated as fact, a fact marked "unclear" stated as settled, or a quote that is not word for word. Send back any fact the sheet does not have, even one that sounds harmless.
|
|
41
|
+
|
|
42
|
+
Then check the shape: the three sections are present and in order, the thread has 4 to 6 numbered posts and there are two single posts, no X post is longer than 280 characters, the LinkedIn post is under 200 words, and every factual claim appears under "Facts used". When you send work back, quote the post's words and the source's words side by side so the writer can correct them.
|
|
43
|
+
done:
|
|
44
|
+
- "Every number, time span, name and quote in the posts is on the facts sheet, with the same meaning."
|
|
45
|
+
- "No post says more than the source: nothing rounded, worked out or made more certain, and no qualifier dropped."
|
|
46
|
+
- "Every factual claim is listed under \"Facts used\" with the words it rests on."
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
cases:
|
|
3
|
+
frying_pan_posts:
|
|
4
|
+
name: Writes posts that say no more than the facts sheet
|
|
5
|
+
inputs:
|
|
6
|
+
facts: |
|
|
7
|
+
## Facts
|
|
8
|
+
|
|
9
|
+
| ID | Where | Fact | Exact words |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| F1 | [0:00] | The video is the story of how a frying pan made the speaker build an app. | "So this is the story of how a frying pan made me build an app." |
|
|
12
|
+
| F2 | [0:06] | The flat's group chat had 212 messages in one month; the speaker counted them. | "Our flat group chat had 212 messages in one month. I counted." |
|
|
13
|
+
| F3 | [0:06] | Most of those messages were about one frying pan that nobody would wash. | "Most of them were about one frying pan that nobody would wash." |
|
|
14
|
+
| F4 | [0:18] | Five people live in the flat. | "There are five of us in the flat." |
|
|
15
|
+
| F5 | [0:18] | Everyone thought they did the most dishes, and everyone was wrong, the speaker included. | "Everyone thought they did the most dishes. Everyone was wrong, including me." |
|
|
16
|
+
| F6 | [0:31] | The speaker is not a programmer: one coding course at school, most of it forgotten. | "I'm not a programmer. I did one coding course at school and forgot most of it." |
|
|
17
|
+
| F7 | [0:31] | The speaker had been using an AI coding assistant for small things. | "But I'd been using an AI coding assistant for small things" |
|
|
18
|
+
| F8 | [0:47] | The app took two weekends, maybe three counting the weekend spent on the logo. | "It took me two weekends. Maybe three, if you count the weekend I spent on the logo." |
|
|
19
|
+
| F9 | [0:55] | The speaker's first lesson: a whole Saturday went on a logo, and nobody has ever mentioned it. | "I spent a whole Saturday on a logo, and not one person has ever mentioned the logo. Not one." |
|
|
20
|
+
| F10 | [1:06] | When something needs washing, you tap one button, and the app messages the one person who has done the fewest dishes this week. | "When something needs washing, you tap one button. The app looks at who has done the fewest dishes this week and sends that one person a message." |
|
|
21
|
+
| F11 | [1:06] | The app has no points, no leaderboard and no badges. | "No points, no leaderboard, no badges." |
|
|
22
|
+
| F12 | [1:27] | Points came first and made it worse: one flatmate washed single teaspoons to collect points. | "I tried points first. Points made it worse. One flatmate started washing single teaspoons to collect points." |
|
|
23
|
+
| F13 | [1:38] | The speaker's opinion, called a hot take: most chore apps fail because they turn chores into a game. | "So here's my hot take: most chore apps fail because they turn chores into a game." |
|
|
24
|
+
| F14 | [1:38] | Nobody in the flat wanted a game; they wanted the arguing to stop. | "Nobody in my flat wanted a game. We wanted the arguing to stop." |
|
|
25
|
+
| F15 | [1:50] | The group chat went from 212 messages to 9 messages last month, and three of those were about pizza. | "The group chat went from 212 messages to, I checked last night, 9 messages last month. And three of those were about pizza." |
|
|
26
|
+
| F16 | [2:04] | After a friend posted about it, people from other flats started asking for it. | "Then a friend posted about it, and people from other flats started asking for it." |
|
|
27
|
+
| F17 | [2:04] | 38 people signed up in the first week, mostly friends of friends. | "38 people signed up in the first week. Mostly friends of friends, to be fair." |
|
|
28
|
+
| F18 | [2:04] | 11 of them used it again the next day. | "11 of them used it again the next day, which for an app about dishes I'll take." |
|
|
29
|
+
| F19 | [2:22] | Running it costs three pounds a month, after a correction. | "It costs me about four, no, three pounds a month to run. Three." |
|
|
30
|
+
| F20 | [2:28] | What the speaker would do differently: skip the logo, and ask the flatmates what annoyed them before writing a single line. They guessed fairness; it was the frying pan. | "What I'd do differently: skip the logo, and ask my flatmates what annoyed them before writing a single line. I guessed it was fairness. It was the frying pan." |
|
|
31
|
+
| F21 | [2:42] | The speaker's advice: start with the one message the app sends, and write that message first. | "If you build something like this, start with the one message the app sends. Write that message first. Everything else is decoration." |
|
|
32
|
+
| F22 | [2:51] | The pan is clean, for now. | "The pan is clean. For now." |
|
|
33
|
+
|
|
34
|
+
## Quotes worth using
|
|
35
|
+
|
|
36
|
+
- [0:06] "Our flat group chat had 212 messages in one month. I counted." (F2)
|
|
37
|
+
- [0:55] "not one person has ever mentioned the logo. Not one." (F9)
|
|
38
|
+
- [1:06] "No points, no leaderboard, no badges." (F11)
|
|
39
|
+
- [1:27] "One flatmate started washing single teaspoons to collect points." (F12)
|
|
40
|
+
- [1:38] "We wanted the arguing to stop." (F14)
|
|
41
|
+
- [2:28] "I guessed it was fairness. It was the frying pan." (F20)
|
|
42
|
+
- [2:42] "Write that message first. Everything else is decoration." (F21)
|
|
43
|
+
- [2:51] "The pan is clean. For now." (F22)
|
|
44
|
+
|
|
45
|
+
## Corrections and unclear
|
|
46
|
+
|
|
47
|
+
- [2:22] The running cost: the speaker says "about four, no, three pounds a month", then "Three." The corrected figure, three pounds a month, is the fact (F19).
|
|
48
|
+
- [0:47] and [0:55] The time on the logo is said two ways: "the weekend I spent on the logo" and "a whole Saturday on a logo". Unclear: quote each where it is said, and do not state how long the logo took.
|
|
49
|
+
|
|
50
|
+
## Names
|
|
51
|
+
|
|
52
|
+
No one other than the speaker is named.
|
|
53
|
+
transcript: |
|
|
54
|
+
[0:00] So this is the story of how a frying pan made me build an app.
|
|
55
|
+
[0:06] Our flat group chat had 212 messages in one month. I counted. Most of them were about one frying pan that nobody would wash.
|
|
56
|
+
[0:18] There are five of us in the flat. Everyone thought they did the most dishes. Everyone was wrong, including me.
|
|
57
|
+
[0:31] I'm not a programmer. I did one coding course at school and forgot most of it. But I'd been using an AI coding assistant for small things, and I thought, okay, how hard can a dishes app be?
|
|
58
|
+
[0:47] It took me two weekends. Maybe three, if you count the weekend I spent on the logo.
|
|
59
|
+
[0:55] And that's my first lesson. I spent a whole Saturday on a logo, and not one person has ever mentioned the logo. Not one.
|
|
60
|
+
[1:06] Here's how it works, and honestly the trick is how little it does. When something needs washing, you tap one button. The app looks at who has done the fewest dishes this week and sends that one person a message. That's it. No points, no leaderboard, no badges.
|
|
61
|
+
[1:27] I tried points first. Points made it worse. One flatmate started washing single teaspoons to collect points.
|
|
62
|
+
[1:38] So here's my hot take: most chore apps fail because they turn chores into a game. Nobody in my flat wanted a game. We wanted the arguing to stop.
|
|
63
|
+
[1:50] Did it work? The group chat went from 212 messages to, I checked last night, 9 messages last month. And three of those were about pizza.
|
|
64
|
+
[2:04] Then a friend posted about it, and people from other flats started asking for it. 38 people signed up in the first week. Mostly friends of friends, to be fair. 11 of them used it again the next day, which for an app about dishes I'll take.
|
|
65
|
+
[2:22] It costs me about four, no, three pounds a month to run. Three.
|
|
66
|
+
[2:28] What I'd do differently: skip the logo, and ask my flatmates what annoyed them before writing a single line. I guessed it was fairness. It was the frying pan. It's always one frying pan.
|
|
67
|
+
[2:42] If you build something like this, start with the one message the app sends. Write that message first. Everything else is decoration.
|
|
68
|
+
[2:51] Anyway. The pan is clean. For now.
|
|
69
|
+
# Invented sample: Maya, her flat and this video do not exist. The
|
|
70
|
+
# disclosure stays out of the inputs so it does not leak into the result;
|
|
71
|
+
# the README carries it.
|
|
72
|
+
about_you: "I'm Maya. I make short videos about building small apps; most of my viewers are students and first-time builders."
|
|
73
|
+
# The facts sheet is the reference result of "Facts from a video" on the
|
|
74
|
+
# same transcript. Excludes: the roundings it invites, the running cost before
|
|
75
|
+
# the correction, a result she never claims and percentages she never said.
|
|
76
|
+
expect:
|
|
77
|
+
contains: ["LinkedIn", "Facts used", "frying pan"]
|
|
78
|
+
excludes: ["went viral", "in one weekend", "in a weekend", "40 people", "nearly 40", "almost 40", "never coded", "£4", "four pounds a month", "96%", "29%"]
|
|
79
|
+
min_length: 900
|
|
80
|
+
max_length: 9000
|
package/package.json
CHANGED
|
@@ -1,6 +1,94 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "factoryhub",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./packages/sdk/src/index.d.ts",
|
|
33
|
+
"default": "./packages/sdk/src/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./engine": "./packages/engine/src/index.mjs",
|
|
36
|
+
"./package.json": "./package.json",
|
|
37
|
+
"./*": "./*"
|
|
38
|
+
},
|
|
39
|
+
"types": "./packages/sdk/src/index.d.ts",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22"
|
|
42
|
+
},
|
|
43
|
+
"workspaces": [
|
|
44
|
+
"packages/engine",
|
|
45
|
+
"packages/cli",
|
|
46
|
+
"packages/mcp"
|
|
47
|
+
],
|
|
48
|
+
"bin": {
|
|
49
|
+
"factoryhub": "packages/cli/bin/factoryhub.mjs",
|
|
50
|
+
"factoryhub-mcp": "packages/mcp/bin/factoryhub-mcp.mjs"
|
|
51
|
+
},
|
|
52
|
+
"files": [
|
|
53
|
+
"packages/engine/package.json",
|
|
54
|
+
"packages/engine/src",
|
|
55
|
+
"packages/engine/lib",
|
|
56
|
+
"packages/cli/package.json",
|
|
57
|
+
"packages/cli/bin",
|
|
58
|
+
"packages/cli/src",
|
|
59
|
+
"packages/mcp/package.json",
|
|
60
|
+
"packages/mcp/bin",
|
|
61
|
+
"packages/mcp/src",
|
|
62
|
+
"packages/sdk/package.json",
|
|
63
|
+
"packages/sdk/src",
|
|
64
|
+
"factories",
|
|
65
|
+
"NOTICE",
|
|
66
|
+
"spec/*.md",
|
|
67
|
+
"spec/schema"
|
|
68
|
+
],
|
|
69
|
+
"scripts": {
|
|
70
|
+
"test": "node --test \"packages/*/test/*.test.mjs\" \"spec/test/*.test.mjs\" \"test/*.test.mjs\"",
|
|
71
|
+
"lint": "eslint . && node scripts/check-links.mjs",
|
|
72
|
+
"validate": "node packages/cli/bin/factoryhub.mjs validate factories/*/",
|
|
73
|
+
"generate": "node scripts/generate.mjs",
|
|
74
|
+
"check": "npm run lint && npm test && npm run validate && node scripts/generate.mjs --check && npm run check:rust",
|
|
75
|
+
"check:rust": "cargo fmt --all --check && cargo clippy --all-targets --locked -- -D warnings && cargo test --locked && cargo build --locked --target wasm32-unknown-unknown -p factoryhub-core"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"@modelcontextprotocol/server": "2.4.0",
|
|
79
|
+
"zod": "4.6.7"
|
|
80
|
+
},
|
|
81
|
+
"devDependencies": {
|
|
82
|
+
"@eslint/js": "10.0.1",
|
|
83
|
+
"@modelcontextprotocol/client": "2.4.0",
|
|
84
|
+
"ajv": "8.20.0",
|
|
85
|
+
"eslint": "10.15.0",
|
|
86
|
+
"globals": "17.14.0",
|
|
87
|
+
"yaml": "2.9.1"
|
|
88
|
+
},
|
|
89
|
+
"publishConfig": {
|
|
90
|
+
"access": "public",
|
|
91
|
+
"provenance": true,
|
|
92
|
+
"registry": "https://registry.npmjs.org/"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -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.1",
|
|
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,54 @@
|
|
|
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
|
+
usage: `Usage: factoryhub export <factory> --as skill [--out dir] [--id id] [--force]
|
|
11
|
+
|
|
12
|
+
Writes <dir>/<id>/SKILL.md and <dir>/<id>/factory/, following
|
|
13
|
+
spec/skill-export.md. SKILL.md tells an agent what the factory produces, which
|
|
14
|
+
inputs to collect, every step in order with who does it, where to stop and
|
|
15
|
+
ask the person to approve, and how to run it with this CLI or by hand.
|
|
16
|
+
factory/ holds the factory's files unchanged, plus the foreman written from
|
|
17
|
+
the flow when the author wrote none.
|
|
18
|
+
|
|
19
|
+
Copy the folder into your agent's skills directory, for example
|
|
20
|
+
~/.claude/skills/ for Claude Code.
|
|
21
|
+
|
|
22
|
+
Options:
|
|
23
|
+
--as the export format; "skill" is the only one
|
|
24
|
+
--out the parent folder (default: the current folder)
|
|
25
|
+
--id the skill name and factoryhub id (default: the factory's name)
|
|
26
|
+
--hub the factoryhub address used for the source link (default: ${DEFAULT_HUB})
|
|
27
|
+
--force replace an earlier export of the same factory`,
|
|
28
|
+
options: { boolean: ['force'], string: ['as', 'out', 'id', 'hub'] },
|
|
29
|
+
async run({ positional, flags }, io) {
|
|
30
|
+
const [target] = positional;
|
|
31
|
+
if (!target) throw usage('Name a factory to export.');
|
|
32
|
+
if (flags.as !== 'skill') throw usage('Choose the export format: --as skill');
|
|
33
|
+
const read = await readFactory(target, { cwd: io.cwd, fetchImpl: io.fetch });
|
|
34
|
+
const skill = await exportSkill(read.files, { id: flags.id, hub: flags.hub || DEFAULT_HUB });
|
|
35
|
+
const folder = resolve(io.cwd, flags.out || '.', skill.id);
|
|
36
|
+
const s = styles(io);
|
|
37
|
+
// Checked before anything is written: a failing export leaves no files.
|
|
38
|
+
const problems = await checkSkill(skill.files, { folder: skill.id, hub: flags.hub || DEFAULT_HUB });
|
|
39
|
+
if (problems.length) {
|
|
40
|
+
err(io, `${s.red('not exported')} ${skill.id}: the export fails its checks`);
|
|
41
|
+
for (const problem of problems) err(io, ` - ${problem}`);
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
if (existsSync(folder)) {
|
|
45
|
+
if (!flags.force) throw usage(`${folder} already exists. Pass --force to replace it.`);
|
|
46
|
+
// Replace an earlier export as a whole, so no stale file stays behind.
|
|
47
|
+
if (!existsSync(join(folder, 'SKILL.md'))) throw usage(`${folder} is not an earlier skill export, so it was left alone.`);
|
|
48
|
+
await rm(folder, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
await writeFiles(folder, skill.files, { allow: (path) => path === 'SKILL.md' || path.startsWith('factory/') });
|
|
51
|
+
out(io, `${s.green('exported')} ${relative(io.cwd, folder) || folder}/SKILL.md and factory/ (${Object.keys(skill.files).length - 1} files), sha256 ${skill.sha256}`);
|
|
52
|
+
return 0;
|
|
53
|
+
},
|
|
54
|
+
};
|