weftens 0.0.1 → 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/README.md +178 -12
- package/agent-cores/amazon-ppc/package.json +17 -0
- package/agent-cores/amazon-ppc/src/cli.mjs +66 -0
- package/agent-cores/amazon-ppc/src/optimize.mjs +272 -0
- package/agent-cores/amazon-ppc/src/parse.mjs +153 -0
- package/agent-cores/amazon-ppc/test/fixtures/search-term-report.csv +8 -0
- package/agent-cores/amazon-ppc/test/fixtures/search-term-report.json +5 -0
- package/agent-cores/amazon-ppc/test/optimize.test.mjs +161 -0
- package/agent-cores/content-ops/package.json +17 -0
- package/agent-cores/content-ops/src/cli.mjs +51 -0
- package/agent-cores/content-ops/src/inventory.mjs +343 -0
- package/agent-cores/content-ops/src/parse.mjs +121 -0
- package/agent-cores/content-ops/test/fixtures/cms-export.csv +13 -0
- package/agent-cores/content-ops/test/fixtures/cms-export.json +31 -0
- package/agent-cores/content-ops/test/inventory.test.mjs +177 -0
- package/agent-cores/meta-ads/package.json +16 -0
- package/agent-cores/meta-ads/src/analyze.mjs +166 -0
- package/agent-cores/meta-ads/src/cli.mjs +39 -0
- package/agent-cores/meta-ads/src/parse.mjs +123 -0
- package/agent-cores/meta-ads/test/analyze.test.js +72 -0
- package/agent-cores/meta-ads/test/fixtures/meta-ads.csv +6 -0
- package/agent-cores/paid-search/package.json +17 -0
- package/agent-cores/paid-search/src/analyze.mjs +303 -0
- package/agent-cores/paid-search/src/cli.mjs +57 -0
- package/agent-cores/paid-search/src/parse.mjs +158 -0
- package/agent-cores/paid-search/test/analyze.test.mjs +120 -0
- package/agent-cores/paid-search/test/fixtures/campaigns.csv +7 -0
- package/agent-cores/paid-search/test/fixtures/sample.json +9 -0
- package/agent-cores/paid-search/test/fixtures/search-terms.csv +11 -0
- package/agent-cores/paid-search/test/parse.test.mjs +56 -0
- package/agent-cores/seo-content/package.json +17 -0
- package/agent-cores/seo-content/src/cli.mjs +41 -0
- package/agent-cores/seo-content/src/parse.mjs +141 -0
- package/agent-cores/seo-content/src/strategy.mjs +266 -0
- package/agent-cores/seo-content/test/fixtures/gsc-sample.csv +11 -0
- package/agent-cores/seo-content/test/parse.test.mjs +59 -0
- package/agent-cores/seo-content/test/strategy.test.mjs +108 -0
- package/agent-cores/seo-technical/package.json +17 -0
- package/agent-cores/seo-technical/src/audit.mjs +233 -0
- package/agent-cores/seo-technical/src/cli.mjs +48 -0
- package/agent-cores/seo-technical/src/fetch.mjs +51 -0
- package/agent-cores/seo-technical/test/audit.test.mjs +73 -0
- package/agent-cores/seo-technical/test/fixtures/sample.html +22 -0
- package/agent-cores/supply-verify/package.json +17 -0
- package/agent-cores/supply-verify/src/cli.mjs +44 -0
- package/agent-cores/supply-verify/src/verify.mjs +303 -0
- package/agent-cores/supply-verify/test/fixtures/placements.csv +8 -0
- package/agent-cores/supply-verify/test/fixtures/placements.json +4 -0
- package/agent-cores/supply-verify/test/verify.test.mjs +148 -0
- package/agents/agent2060.md +99 -0
- package/agents/amazon-ppc.md +42 -0
- package/agents/claims-content.md +35 -0
- package/agents/content-ops.md +46 -0
- package/agents/content-strategy.md +13 -0
- package/agents/disclosure-qc.md +13 -0
- package/agents/feed-optimizer.md +13 -0
- package/agents/growth-analyst.md +24 -0
- package/agents/hilbert.md +55 -0
- package/agents/ideator.md +13 -0
- package/agents/lippmann.md +68 -0
- package/agents/paid-search.md +35 -0
- package/agents/pio.md +34 -0
- package/agents/researcher.md +13 -0
- package/agents/richy.md +72 -0
- package/agents/script-editor.md +13 -0
- package/agents/seo-content.md +57 -0
- package/agents/seo-technical.md +53 -0
- package/agents/showrunner.md +13 -0
- package/agents/social-voice.md +38 -0
- package/agents/supply-verify.md +38 -0
- package/agents/video-editor.md +13 -0
- package/agents/writer.md +45 -0
- package/bin/weftens.js +234 -0
- package/content-org/.preflight/telemetry.jsonl +6 -0
- package/content-org/briefs/BRIEFS.md +47 -0
- package/content-org/config.mjs +78 -0
- package/content-org/cost.mjs +36 -0
- package/content-org/lead.js +64 -0
- package/content-org/metrics.mjs +166 -0
- package/content-org/orchestrator.mjs +246 -0
- package/content-org/package.json +14 -0
- package/content-org/producer.mjs +59 -0
- package/content-org/publish.mjs +34 -0
- package/content-org/review.mjs +32 -0
- package/content-org/studio/README.md +27 -0
- package/content-org/studio/assemble.sh +48 -0
- package/content-org/test.mjs +110 -0
- package/index.js +32 -0
- package/package.json +49 -10
- package/providers.json.example +9 -0
- package/src/agent-router.js +124 -0
- package/src/agent.js +123 -0
- package/src/format.js +66 -0
- package/src/invoke.js +129 -0
- package/src/pio-client.js +142 -0
- package/src/provider.js +81 -0
- package/src/registry.js +160 -0
- package/src/richy-client.js +216 -0
- package/src/router.js +34 -0
- package/src/synthesize.js +111 -0
- package/src/weftens.js +61 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: supply-verify
|
|
3
|
+
description: Run on programmatic campaign data (placement/log-level reports, supply-path data, vendor reports) to find where ad spend actually went — MFA sites, invalid traffic, fee extraction, misreporting. A verification agent, not a buyer. Output is an evidenced waste report with exclusion proposals.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are an ad-spend verification analyst — detection, not media buying. The market fact that defines
|
|
7
|
+
the job: only ~43% of programmatic spend reaches a quality impression (viewable, measurable,
|
|
8
|
+
fraud-free, not made-for-advertising), and ~20% of open-web traffic shows invalid-traffic signals.
|
|
9
|
+
The buyer's dashboards won't tell them this. You will, with evidence.
|
|
10
|
+
|
|
11
|
+
## The work
|
|
12
|
+
1. **Placement forensics.** From placement/log-level reports: where did ads actually serve? Every
|
|
13
|
+
domain/app ranked by spend, then screened.
|
|
14
|
+
2. **MFA detection.** Made-for-advertising sites pass standard fraud filters while delivering
|
|
15
|
+
nothing. Markers: extreme ad density, templated design across "different" sites, content velocity
|
|
16
|
+
no editorial team produces, traffic bought not earned (sourced-traffic patterns), arbitrage
|
|
17
|
+
economics. A site isn't MFA on one marker; it is on three.
|
|
18
|
+
3. **IVT patterns.** Non-human signatures: impossible session behavior, geo anomalies, datacenter
|
|
19
|
+
ranges, spoofed domains (the premium domain serving from a URL that isn't theirs), ad stacking /
|
|
20
|
+
1×1 placements. Note the frontier honestly: agentic-AI bots now mimic scrolling and hesitation —
|
|
21
|
+
flag "consistent with" rather than "proven" where detection is heuristic.
|
|
22
|
+
4. **Supply-path audit.** Who sat between advertiser and impression, and what did each hop take?
|
|
23
|
+
Undisclosed intermediaries and duplicate paths to the same inventory are findings.
|
|
24
|
+
5. **Vendor verification.** Reconcile the platform's/agency's report against raw data. Discrepancies
|
|
25
|
+
are findings with the delta quantified.
|
|
26
|
+
|
|
27
|
+
## Method
|
|
28
|
+
Every finding carries a confidence tier (verified / probable / consistent-with / needs-data) and its
|
|
29
|
+
evidence. Never present a heuristic hit as proof. Quantify: spend touched, estimated waste,
|
|
30
|
+
reclaimable amount.
|
|
31
|
+
|
|
32
|
+
## Output
|
|
33
|
+
1. Waste report: finding | evidence | confidence | spend affected.
|
|
34
|
+
2. Exclusion-list proposal (domains/apps/paths) with the marker evidence per entry.
|
|
35
|
+
3. Reclaimable-spend estimate and the questions to put to the vendor, written to be forwarded as-is.
|
|
36
|
+
|
|
37
|
+
Good output: "these 40 domains took $X and show MFA markers A, B, C — evidence attached; exclusion
|
|
38
|
+
list ready." Bad output: "consider a brand-safety vendor."
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: video-editor
|
|
3
|
+
description: Video Editor seat for Weftens' Media Content team — turns an approved script into a concrete, generation-ready shot list (one entry per 5–10s clip with an AI-video prompt), plus pacing notes and on-screen-text placement. Canonical runtime is the content-org pipeline (`node orchestrator.mjs --seat cut`); this file is a thin pointer. Feeds the Producer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the Video Editor on Weftens' Media Content team — turn the approved script into a generation-ready shot list (one entry per 5–10s clip, ~4–7 clips for a ~30–45s Reel; each with an AI-video-model prompt), plus pacing notes and where each on-screen text lands. Clip generation + assembly downstream is the Producer (`producer.mjs`, FFmpeg), a runtime, not a prompt-agent.
|
|
7
|
+
|
|
8
|
+
Audience/brand (canonical in content-org): older Facebook users; faceless; "declare what's real"; disclosure is a per-piece choice, never moralized; scam content is THEATRE only.
|
|
9
|
+
|
|
10
|
+
**Canonical runtime:** `content-org\orchestrator.mjs --seat cut`.
|
|
11
|
+
**Output schema:** `{ shots: [ { file, seconds, prompt, onScreenText } ], pacingNotes }`.
|
|
12
|
+
|
|
13
|
+
content-org is the single source of truth for this seat's prompt. This file is a pointer — do not add divergent instructions here.
|
package/agents/writer.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writer
|
|
3
|
+
description: The one writing agent (copywriter and script writer merged). Writes scripts for content pieces AND conversion copy (headlines, landing pages, VSLs, ads) — one craft, two output shapes. Mines real language first, writes second. Downstream gates (Script Editor / human approval) still apply.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are the writing seat — the merger of the script writer and the conversion copywriter. One craft:
|
|
7
|
+
words built from found language, aimed at a reader who owes you nothing.
|
|
8
|
+
|
|
9
|
+
## The method spine (both output shapes)
|
|
10
|
+
The writing is not the job — research is. Before drafting, mine the real language:
|
|
11
|
+
- For content pieces: the research packet (Researcher's facts, sources, claims-to-verify) and the
|
|
12
|
+
format brief; the audience's own words from comments/UGC where available.
|
|
13
|
+
- For conversion copy: the corpus (reviews of the product AND competitors, threads, transcripts,
|
|
14
|
+
support logs) → a voice-of-customer map: pains, desired outcomes, objections, the switch moment,
|
|
15
|
+
competitor complaints — verbatim, with frequency and emotional weight per phrase.
|
|
16
|
+
Copy assembled from found language beats invented copy; the customer's words are more specific,
|
|
17
|
+
more emotional, and more believable than anything written at a desk.
|
|
18
|
+
|
|
19
|
+
## Output shape 1 — scripts (content pieces) = the content-org `script` seat
|
|
20
|
+
From brief + beats: the script with VO notes, written to the Showrunner's voice guardrails and the
|
|
21
|
+
format's structure. Hook carries the highest-weight found phrase. Hands off to Script Editor
|
|
22
|
+
(copy gate: tighten, voice, fact-check against Research).
|
|
23
|
+
|
|
24
|
+
In the Media Content pipeline this is the `script` seat: canonical runtime
|
|
25
|
+
`content-org\orchestrator.mjs --seat script`, output schema `{ script, onScreenText, voNotes }`. When
|
|
26
|
+
run in the pipeline, content-org is the single source of truth for that seat's prompt — match its
|
|
27
|
+
schema and do not add divergent script-seat instructions here. This file additionally holds the
|
|
28
|
+
standalone conversion-copy craft below, which the pipeline does not cover.
|
|
29
|
+
|
|
30
|
+
## Output shape 2 — conversion copy (offers, pages, VSLs, ads)
|
|
31
|
+
Headlines, bullets, CTAs, leads from the VOC map, minimally edited, each annotated to its source
|
|
32
|
+
row. One message per asset; problem first in their words; proof before promise. VSL/funnel: open on
|
|
33
|
+
the highest frequency × weight pain, sequence proof against objections in descending order.
|
|
34
|
+
|
|
35
|
+
## Hard rules (both shapes)
|
|
36
|
+
- Banned: marketer vocabulary ("streamline," "unlock," "empower," "seamless," "elevate"), wordplay
|
|
37
|
+
that costs clarity, invented benefits, superlatives without a source.
|
|
38
|
+
- Every claim traces to the corpus, the research packet, or the offer — a caught overclaim kills
|
|
39
|
+
the asset.
|
|
40
|
+
- Voice is locked per surface (Showrunner's guardrails for own channels; the client voice doc for
|
|
41
|
+
client work). Drift is a defect even when the copy is good.
|
|
42
|
+
- Nothing publishes from this seat — Script Editor / Disclosure-QC / human approval sit downstream.
|
|
43
|
+
|
|
44
|
+
Good output: a line the audience could have said themselves, in the locked voice. Bad output:
|
|
45
|
+
brand-voice adjectives arranged pleasingly — reads well, converts nothing, retells as nothing.
|
package/bin/weftens.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Weftens CLI — the agent front door.
|
|
3
|
+
//
|
|
4
|
+
// weftens ask "how do we look online and who came to our events?" --org "Acme" [--website https://…]
|
|
5
|
+
// weftens chat --org "Acme" # interactive conversation
|
|
6
|
+
// weftens route request.json # deterministic router/synthesizer (no LLM, no key)
|
|
7
|
+
// echo '{"org":{...},"question":"..."}' | weftens route -
|
|
8
|
+
//
|
|
9
|
+
// ask/chat need ANTHROPIC_API_KEY (or an `ant auth login` profile). Specialists:
|
|
10
|
+
// WEFTENS_RICHY_URL + WEFTENS_RICHY_KEY → live Richy audit service
|
|
11
|
+
// WEFTENS_PIO_URL + WEFTENS_PIO_MEMBER + WEFTENS_PIO_PASSWORD → live PIO (member session)
|
|
12
|
+
// WEFTENS_PIO_TENANT → tenant slug (hosted PIO only)
|
|
13
|
+
// Without them the specialists run as clearly-flagged fixtures so the agent always works.
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import readline from "node:readline";
|
|
17
|
+
import { orchestrate } from "../src/weftens.js";
|
|
18
|
+
import { isChangeSheet, formatChangeSheet } from "../src/format.js";
|
|
19
|
+
import { askWeftens } from "../src/agent.js";
|
|
20
|
+
import { RichyClient, FixtureRichyClient } from "../src/richy-client.js";
|
|
21
|
+
import { PioClient, FixturePioClient } from "../src/pio-client.js";
|
|
22
|
+
import { route } from "../src/agent-router.js";
|
|
23
|
+
import { invoke } from "../src/invoke.js";
|
|
24
|
+
import { REGISTRY, dispatchableAgents } from "../src/registry.js";
|
|
25
|
+
|
|
26
|
+
// A specialist is live only when its FULL env group is set. A partial group is a
|
|
27
|
+
// hard error, not a silent fixture fallback — an operator who typo'd one variable
|
|
28
|
+
// would otherwise get sample data while believing they're looking at their org.
|
|
29
|
+
function envGroup(label, names) {
|
|
30
|
+
const set = names.filter((n) => process.env[n]);
|
|
31
|
+
if (set.length === 0) return null;
|
|
32
|
+
if (set.length < names.length) {
|
|
33
|
+
const missing = names.filter((n) => !process.env[n]).join(", ");
|
|
34
|
+
throw new Error(`${label} is partially configured — set ${missing} (or unset ${set.join(", ")} to run with sample data)`);
|
|
35
|
+
}
|
|
36
|
+
return Object.fromEntries(names.map((n) => [n, process.env[n]]));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildAgents() {
|
|
40
|
+
const richyEnv = envGroup("Richy", ["WEFTENS_RICHY_URL", "WEFTENS_RICHY_KEY"]);
|
|
41
|
+
const pioEnv = envGroup("PIO", ["WEFTENS_PIO_URL", "WEFTENS_PIO_MEMBER", "WEFTENS_PIO_PASSWORD"]);
|
|
42
|
+
const richy = richyEnv
|
|
43
|
+
? new RichyClient({ baseUrl: richyEnv.WEFTENS_RICHY_URL, key: richyEnv.WEFTENS_RICHY_KEY })
|
|
44
|
+
: new FixtureRichyClient();
|
|
45
|
+
const pio = pioEnv
|
|
46
|
+
? new PioClient({
|
|
47
|
+
baseUrl: pioEnv.WEFTENS_PIO_URL,
|
|
48
|
+
member: pioEnv.WEFTENS_PIO_MEMBER,
|
|
49
|
+
password: pioEnv.WEFTENS_PIO_PASSWORD,
|
|
50
|
+
tenant: process.env.WEFTENS_PIO_TENANT ?? null,
|
|
51
|
+
})
|
|
52
|
+
: new FixturePioClient();
|
|
53
|
+
const fixtures = [!richyEnv && "Richy", !pioEnv && "PIO"].filter(Boolean);
|
|
54
|
+
if (fixtures.length > 0) {
|
|
55
|
+
process.stderr.write(`weftens: ${fixtures.join(" and ")} not configured — using clearly-flagged sample data\n`);
|
|
56
|
+
}
|
|
57
|
+
return { richy, pio };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseFlags(args) {
|
|
61
|
+
const flags = {};
|
|
62
|
+
const rest = [];
|
|
63
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
64
|
+
if (args[i] === "--org") flags.org = args[++i];
|
|
65
|
+
else if (args[i] === "--website") flags.website = args[++i];
|
|
66
|
+
else rest.push(args[i]);
|
|
67
|
+
}
|
|
68
|
+
return { flags, rest };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function orgPreamble(flags) {
|
|
72
|
+
if (!flags.org) return "";
|
|
73
|
+
const site = flags.website ? ` (website: ${flags.website})` : "";
|
|
74
|
+
return `The organization we're discussing is "${flags.org}"${site}. `;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function cmdAsk(args) {
|
|
78
|
+
const { flags, rest } = parseFlags(args);
|
|
79
|
+
const question = rest.join(" ").trim();
|
|
80
|
+
if (!question) {
|
|
81
|
+
process.stderr.write('usage: weftens ask "<question>" [--org NAME] [--website URL]\n');
|
|
82
|
+
process.exitCode = 1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const { text } = await askWeftens({ question: orgPreamble(flags) + question, agents: buildAgents() });
|
|
86
|
+
process.stdout.write(`${text}\n`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function cmdChat(args) {
|
|
90
|
+
const { flags } = parseFlags(args);
|
|
91
|
+
const agents = buildAgents();
|
|
92
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
93
|
+
let history = [];
|
|
94
|
+
let first = true;
|
|
95
|
+
process.stdout.write("Weftens. Ask about how your organization looks online or how it's running. Ctrl+C to exit.\n");
|
|
96
|
+
for (;;) {
|
|
97
|
+
const line = await new Promise((resolve) => rl.question("> ", resolve));
|
|
98
|
+
const question = line.trim();
|
|
99
|
+
if (!question) continue;
|
|
100
|
+
if (question === "exit" || question === "quit") break;
|
|
101
|
+
try {
|
|
102
|
+
const { text, messages } = await askWeftens({
|
|
103
|
+
question: first ? orgPreamble(flags) + question : question,
|
|
104
|
+
history,
|
|
105
|
+
agents,
|
|
106
|
+
});
|
|
107
|
+
history = messages;
|
|
108
|
+
first = false;
|
|
109
|
+
process.stdout.write(`${text}\n`);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
process.stderr.write(`weftens: ${error?.message ?? error}\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
rl.close();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function cmdRoute(arg) {
|
|
118
|
+
const raw = !arg || arg === "-" ? readFileSync(0, "utf8") : readFileSync(arg, "utf8");
|
|
119
|
+
const result = await orchestrate(JSON.parse(raw), buildAgents());
|
|
120
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- Registry front door: route across the whole roster, not just Richy/PIO ---
|
|
124
|
+
// `list` shows the directory (and prints the live count); `send` routes a request
|
|
125
|
+
// (auto or direct-addressed) and
|
|
126
|
+
// invokes the chosen agent. Deterministic routing needs no key; see src/invoke.js for
|
|
127
|
+
// what producing needs. This sits alongside ask/chat (the Richy+PIO conversational
|
|
128
|
+
// surface) — those two agents are also reachable here through the registry.
|
|
129
|
+
|
|
130
|
+
function cmdList() {
|
|
131
|
+
const pad = (s, n) => String(s).padEnd(n);
|
|
132
|
+
process.stdout.write(`Weftens registry — ${REGISTRY.length} agents (${dispatchableAgents().length} dispatchable at the door)\n\n`);
|
|
133
|
+
process.stdout.write(`${pad("AGENT", 18)}${pad("TIER", 6)}${pad("TEAM", 12)}${pad("CORE", 6)}OWNED-BY\n`);
|
|
134
|
+
for (const a of REGISTRY) {
|
|
135
|
+
const owned = a.ownedBy ?? (a.leads ? `(leads ${a.leads})` : "-");
|
|
136
|
+
process.stdout.write(`${pad(a.name, 18)}${pad(a.tier, 6)}${pad(a.team, 12)}${pad(a.core ? "yes" : "-", 6)}${owned}\n`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function cmdSend(args, { routeOnly } = {}) {
|
|
141
|
+
const rest = [];
|
|
142
|
+
let inputFile = null, provider = null, org = null, website = null, asJson = false;
|
|
143
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
144
|
+
if (args[i] === "--input") inputFile = args[++i] ?? null;
|
|
145
|
+
else if (args[i] === "--provider") provider = args[++i] ?? null;
|
|
146
|
+
else if (args[i] === "--json") asJson = true;
|
|
147
|
+
else if (args[i] === "--org") org = args[++i] ?? null;
|
|
148
|
+
else if (args[i] === "--website") website = args[++i] ?? null;
|
|
149
|
+
else rest.push(args[i]);
|
|
150
|
+
}
|
|
151
|
+
const firstIsAgent = rest.length >= 1 && REGISTRY.some((a) => a.name === rest[0].toLowerCase());
|
|
152
|
+
const request = firstIsAgent
|
|
153
|
+
? { agent: rest[0], text: rest.slice(1).join(" ") }
|
|
154
|
+
: { text: rest.join(" ") };
|
|
155
|
+
if (!request.text && !request.agent) {
|
|
156
|
+
process.stderr.write('usage: weftens send "<request>" | weftens send <agent> "<request>"\n');
|
|
157
|
+
process.exitCode = 1;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const decision = route(request);
|
|
161
|
+
if (!decision.agent) {
|
|
162
|
+
process.stdout.write(`→ (no route) — ${decision.reason}\n`);
|
|
163
|
+
process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
process.stdout.write(`→ ${decision.agent.name} [${decision.reason}]${decision.confident ? "" : " (low confidence)"}\n`);
|
|
167
|
+
if (decision.alternatives?.length) {
|
|
168
|
+
process.stdout.write(` also considered: ${decision.alternatives.map((x) => `${x.name}(${x.score})`).join(", ")}\n`);
|
|
169
|
+
}
|
|
170
|
+
if (routeOnly) return;
|
|
171
|
+
|
|
172
|
+
// Richy and PIO are specialist legs: they answer about an ORGANIZATION, not about a
|
|
173
|
+
// bare sentence, so they run through orchestrate() rather than as leaf agents. They
|
|
174
|
+
// used to refuse here with a note about "wire contracts" — true internally, useless
|
|
175
|
+
// to the person typing, and wrong in effect: the legs are reachable (with fixture
|
|
176
|
+
// clients when unconfigured), so refusing categorically made the two headline seats
|
|
177
|
+
// look dead. Now `send richy --org NAME "<question>"` does the real thing.
|
|
178
|
+
if (decision.agent.tier === 1 && decision.agent.wire) {
|
|
179
|
+
// An org is genuinely required — Richy audits a specific organization, PIO reports on one —
|
|
180
|
+
// but demanding a FLAG for it is a door that doesn't open for a plain sentence. Whoever is
|
|
181
|
+
// running this almost always means the same org every time, so it's config, not an argument:
|
|
182
|
+
// set WEFTENS_ORG once and `send richy "audit our listings"` just works.
|
|
183
|
+
org = org ?? process.env.WEFTENS_ORG ?? null;
|
|
184
|
+
website = website ?? process.env.WEFTENS_WEBSITE ?? null;
|
|
185
|
+
if (!org) {
|
|
186
|
+
process.stdout.write(
|
|
187
|
+
`\n[orchestrator] ${decision.agent.name} answers about a specific organization — which one?\n` +
|
|
188
|
+
` once: weftens send ${decision.agent.name} --org "Acme Co" [--website acme.com] "${request.text || "your question"}"\n` +
|
|
189
|
+
` always: set WEFTENS_ORG="Acme Co" (and optionally WEFTENS_WEBSITE) — then this command works as typed\n`
|
|
190
|
+
);
|
|
191
|
+
process.exitCode = 1;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const intent = decision.agent.name === "richy" ? "representation" : "operations";
|
|
195
|
+
const result = await orchestrate(
|
|
196
|
+
{ org: { name: org, ...(website ? { website } : {}) }, intent, question: request.text || undefined },
|
|
197
|
+
buildAgents()
|
|
198
|
+
);
|
|
199
|
+
process.stdout.write(`\n[orchestrator] ran ${decision.agent.name} over ${org} (intent: ${intent})\n`);
|
|
200
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const outcome = await invoke(decision.agent, { text: request.text, inputFile, provider });
|
|
205
|
+
process.stdout.write(`\n[${outcome.path}] ${outcome.note}\n`);
|
|
206
|
+
if (outcome.result !== undefined) {
|
|
207
|
+
// A change sheet renders for a human by default — the whole point of one is that a
|
|
208
|
+
// person can read a line and decide. `--json` returns the raw object for piping.
|
|
209
|
+
if (!asJson && isChangeSheet(outcome.result)) {
|
|
210
|
+
process.stdout.write(`\n${formatChangeSheet(outcome.result)}\n`);
|
|
211
|
+
} else {
|
|
212
|
+
process.stdout.write(`\n--- result ---\n${typeof outcome.result === "string" ? outcome.result : JSON.stringify(outcome.result, null, 2)}\n`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function main() {
|
|
218
|
+
const [command, ...args] = process.argv.slice(2);
|
|
219
|
+
if (command === "ask") return cmdAsk(args);
|
|
220
|
+
if (command === "chat") return cmdChat(args);
|
|
221
|
+
if (command === "list" || command === "--list") return cmdList();
|
|
222
|
+
if (command === "send") return cmdSend(args);
|
|
223
|
+
if (command === "--route") return cmdSend(args, { routeOnly: true });
|
|
224
|
+
if (command === "route" || (command && (command === "-" || command.endsWith(".json")))) {
|
|
225
|
+
return cmdRoute(command === "route" ? args[0] : command);
|
|
226
|
+
}
|
|
227
|
+
process.stderr.write("usage: weftens ask|chat|list|send|--route|route …\n");
|
|
228
|
+
process.exitCode = 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
main().catch((error) => {
|
|
232
|
+
process.stderr.write(`weftens: ${error?.message ?? error}\n`);
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{"ts":"2026-07-19T17:03:20.481Z","tool":"edit_file","source":"raw","status":"pass","counts":{"total":8,"pass":8,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\src\\registry.js","durationMs":3}
|
|
2
|
+
{"ts":"2026-07-19T17:03:27.408Z","tool":"edit_file","source":"raw","status":"pass","counts":{"total":8,"pass":8,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\src\\invoke.js","durationMs":2}
|
|
3
|
+
{"ts":"2026-07-19T17:03:35.845Z","tool":"edit_file","source":"raw","status":"pass","counts":{"total":8,"pass":8,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\src\\invoke.js","durationMs":2}
|
|
4
|
+
{"ts":"2026-07-19T17:03:39.916Z","tool":"edit_file","source":"raw","status":"pass","counts":{"total":8,"pass":8,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\src\\invoke.js","durationMs":2}
|
|
5
|
+
{"ts":"2026-07-19T17:03:56.374Z","tool":"write_file","source":"raw","status":"pass","counts":{"total":7,"pass":7,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\index.js","durationMs":2}
|
|
6
|
+
{"ts":"2026-07-19T17:04:01.490Z","tool":"edit_file","source":"raw","status":"pass","counts":{"total":8,"pass":8,"warn":0,"fail":0},"topRules":[],"findings":[],"pathPreview":"C:\\Users\\teche\\weftens\\package.json","durationMs":2}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Format briefs
|
|
2
|
+
|
|
3
|
+
The five formats the org produces. Each is a **brief the seats consume** — angle patterns, structure,
|
|
4
|
+
and the disclosure stance. `RISK` in `config.mjs` sets which auto-post vs hold. Audience across all:
|
|
5
|
+
**older Facebook users**; brand voice: *declare what's real*; disclosure is a **per-piece call, never an
|
|
6
|
+
always-rule**; the scam guardrail is **theatre** (instant reveal, captures nothing, no real person harmed).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## traveler · auto
|
|
11
|
+
The recurring time-traveler character dropped into a different time+place each episode. **The moat** —
|
|
12
|
+
people subscribe to a *who*, not a vibe. Needs character-consistent clips (Veo/Seedance hero lane).
|
|
13
|
+
- **Hook (3s):** the character mid-scene in a vivid era ("It's Tokyo, 1985, and…").
|
|
14
|
+
- **Structure:** cold open in-scene → one wistful observation that ties past to now → button line.
|
|
15
|
+
- **Disclosure:** "Imagined with AI" when it fits; sometimes the era-craft *is* the point. Kayla's call.
|
|
16
|
+
- **Continuity:** same face, voice, and one signature line across episodes.
|
|
17
|
+
|
|
18
|
+
## scam-reveal · hold
|
|
19
|
+
Nostalgia/relatable hook → theatrical "attempted hack" → **"YOU COULD HAVE BEEN HACKED"** → one defensive tell.
|
|
20
|
+
- **Hook (3s):** a familiar scenario (a text from "your bank," a too-good deal).
|
|
21
|
+
- **Structure:** set the trap → the reveal beat → one concrete thing the viewer can do tomorrow.
|
|
22
|
+
- **HARD LINE:** theatre only. Instant reveal, page/piece captures NOTHING, no real non-consenting person
|
|
23
|
+
is hacked or harmed. QC blocks anything that crosses this.
|
|
24
|
+
- **Disclosure:** the reveal is the disclosure; label as AI where useful.
|
|
25
|
+
|
|
26
|
+
## is-this-real · hold
|
|
27
|
+
Teach older viewers to spot AI fakes / deepfakes / cloned voices. **Most on-brand** — the content *is* the
|
|
28
|
+
"declare what's real" demo.
|
|
29
|
+
- **Hook (3s):** "Is this real? Watch closely." (a clip with one tell).
|
|
30
|
+
- **Structure:** show the fake → reveal the tell(s) → "here's how to check next time."
|
|
31
|
+
- **Disclosure:** always clarify what was AI vs real in-piece — here it's the whole point.
|
|
32
|
+
- **QC:** claim accuracy is critical; never mislabel a real thing as fake or vice-versa.
|
|
33
|
+
|
|
34
|
+
## nostalgia · auto
|
|
35
|
+
Straight "remember when" archival-feeling reach. Pure audience fuel; highest raw engagement with older FB.
|
|
36
|
+
- **Hook (3s):** a specific sensory memory ("If you remember the sound of a rotary phone…").
|
|
37
|
+
- **Structure:** montage of era details → one warm line of meaning → gentle prompt to comment.
|
|
38
|
+
- **Disclosure:** label as AI-imagined where the footage is fabricated (Meta throttles undisclosed
|
|
39
|
+
fake-real-events — proactive labeling protects reach).
|
|
40
|
+
- **Bulk lane** (Wan) is fine; no recurring face needed.
|
|
41
|
+
|
|
42
|
+
## ai-for-seniors · hold
|
|
43
|
+
Calm, plain-language explainers ("what's an agent?", "is the AI listening to me?"). Empowerment, no fear.
|
|
44
|
+
- **Hook (3s):** a real question they've wondered ("Is your phone spying on you? Sort of — here's the truth").
|
|
45
|
+
- **Structure:** the worry → the plain answer → one practical takeaway.
|
|
46
|
+
- **Disclosure:** AI-made, said plainly; builds the brand credibility the flywheel cashes in.
|
|
47
|
+
- **QC:** no false/overblown claims about AI; accuracy is the whole value.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// content-org — central config. Change tiers/models here; nothing else needs editing.
|
|
2
|
+
|
|
3
|
+
export const MODELS = {
|
|
4
|
+
haiku: "claude-haiku-4-5", // $1/$5 per 1M — routine seats
|
|
5
|
+
sonnet: "claude-sonnet-5", // $3/$15 — writing-quality seats
|
|
6
|
+
opus: "claude-opus-4-8", // $5/$25 — only if a real test shows a lift
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Value tier (locked 2026-07-08): pay for quality only where it moves reach/moat.
|
|
10
|
+
// Sonnet on the seats whose prose the viewer feels (Strategy board, Script, Editor);
|
|
11
|
+
// Haiku on the structured hand-offs the viewer never sees.
|
|
12
|
+
export const SEAT_MODEL = {
|
|
13
|
+
showrunner: "haiku",
|
|
14
|
+
strategy: "sonnet",
|
|
15
|
+
ideator: "haiku",
|
|
16
|
+
research: "haiku",
|
|
17
|
+
script: "sonnet",
|
|
18
|
+
editor: "sonnet",
|
|
19
|
+
cut: "sonnet", // Video Editor — the cut decides completion rate; worth Sonnet
|
|
20
|
+
feed: "haiku",
|
|
21
|
+
qc: "haiku",
|
|
22
|
+
growth: "haiku", // Growth/Analyst — reads the deterministic metrics.mjs summary; structured hand-off, viewer never sees it
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Clip lanes — pick per piece. Prices are RANGES (sources disagree; price the exact
|
|
26
|
+
// tier on fal.ai at commit). No flat-unlimited *automated* lane exists (verified 2026-07-08).
|
|
27
|
+
export const CLIP_LANES = {
|
|
28
|
+
manual: { label: "manual flat sub", usdPerSec: 0, note: "hand-generate; ~$0 marginal; cheapest" },
|
|
29
|
+
bulk: { label: "Wan 2.6 (bulk B-roll)", usdPerSec: 0.06, audio: true },
|
|
30
|
+
hero: { label: "Veo 3.1 (hero / recurring face)", usdPerSec: 0.05, audio: true, note: "value king: audio + consistency" },
|
|
31
|
+
// Kling deliberately omitted from defaults — priciest, no consistency edge over Veo/Seedance.
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Approx token cost per finished piece by model mix (for cost.mjs). ~per-piece USD of the
|
|
35
|
+
// 8 text-seat calls; Batch API halves it. Rough, tune against real usage.
|
|
36
|
+
export const TEXT_COST_PER_PIECE = {
|
|
37
|
+
valueTier: 0.20, // Sonnet(strategy/script/editor) + Haiku(rest)
|
|
38
|
+
valueTierBatch: 0.10, // with Batch API (50% off)
|
|
39
|
+
allHaiku: 0.05,
|
|
40
|
+
allOpus: 0.55,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const VOICE = {
|
|
44
|
+
elevenlabs: { label: "ElevenLabs Starter", usdPerMonth: 5, perPieceUsd: 0.03 },
|
|
45
|
+
kokoro: { label: "Kokoro (OSS, local)", usdPerMonth: 0, perPieceUsd: 0 },
|
|
46
|
+
veoNative: { label: "Veo native audio (no separate voice)", usdPerMonth: 0, perPieceUsd: 0 },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Client-facing price tiers (DRAFT — Kayla to confirm numbers). reels = pieces/month.
|
|
50
|
+
export const CLIENT_TIERS = {
|
|
51
|
+
starter: {
|
|
52
|
+
name: "Starter", priceUsd: 399, reels: 8,
|
|
53
|
+
clip: "bulk", script: "sonnet", voice: "elevenlabs",
|
|
54
|
+
recurringCharacter: 0, managedPosting: false, complianceQC: "basic", revisions: 1,
|
|
55
|
+
},
|
|
56
|
+
growth: {
|
|
57
|
+
name: "Growth", priceUsd: 899, reels: 20,
|
|
58
|
+
clip: "hero+bulk", script: "sonnet", voice: "elevenlabs",
|
|
59
|
+
recurringCharacter: 1, managedPosting: true, complianceQC: "disclosed+claim-clean",
|
|
60
|
+
analytics: "monthly", revisions: 2,
|
|
61
|
+
},
|
|
62
|
+
signature: {
|
|
63
|
+
name: "Signature", priceUsd: 1999, reels: 40,
|
|
64
|
+
clip: "hero", script: "sonnet+opus-hero", voice: "elevenlabs",
|
|
65
|
+
recurringCharacter: 2, managedPosting: true, complianceQC: "senior/finance/health compliance pass",
|
|
66
|
+
analytics: "full+strategy", revisions: "generous",
|
|
67
|
+
},
|
|
68
|
+
alacarte: { name: "À la carte", priceUsd: 100, reels: 1, note: "$75–150/Reel for one-offs & tests" },
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Format → risk tier (tiered approval). Sensitive formats HOLD for Kayla.
|
|
72
|
+
export const RISK = {
|
|
73
|
+
"traveler": "auto",
|
|
74
|
+
"nostalgia": "auto",
|
|
75
|
+
"scam-reveal": "hold",
|
|
76
|
+
"is-this-real": "hold",
|
|
77
|
+
"ai-for-seniors": "hold",
|
|
78
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// content-org — COGS + margin calculator. `node cost.mjs [piecesPerMonth]`
|
|
2
|
+
// No dependencies. Prices are estimates from config.mjs — tune against real usage.
|
|
3
|
+
import { CLIENT_TIERS, TEXT_COST_PER_PIECE, CLIP_LANES, VOICE } from "./config.mjs";
|
|
4
|
+
|
|
5
|
+
const usd = (n) => "$" + n.toFixed(2);
|
|
6
|
+
const REEL_SECONDS = 30; // ~30s Reel
|
|
7
|
+
|
|
8
|
+
// per-finished-Reel COGS given a clip lane + text tier + voice
|
|
9
|
+
function cogsPerReel({ clip = "hero", textTier = "valueTierBatch", voice = "elevenlabs" } = {}) {
|
|
10
|
+
const text = TEXT_COST_PER_PIECE[textTier] ?? TEXT_COST_PER_PIECE.valueTierBatch;
|
|
11
|
+
const clipUsd = (CLIP_LANES[clip]?.usdPerSec ?? 0) * REEL_SECONDS;
|
|
12
|
+
const voiceUsd = VOICE[voice]?.perPieceUsd ?? 0;
|
|
13
|
+
return text + clipUsd + voiceUsd;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
console.log("=== per-Reel COGS by clip lane (value text tier + Batch, ElevenLabs voice) ===");
|
|
17
|
+
for (const lane of Object.keys(CLIP_LANES)) {
|
|
18
|
+
console.log(` ${lane.padEnd(7)} ${usd(cogsPerReel({ clip: lane }))} (${CLIP_LANES[lane].label})`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log("\n=== client tiers (DRAFT prices) ===");
|
|
22
|
+
for (const key of ["starter", "growth", "signature"]) {
|
|
23
|
+
const t = CLIENT_TIERS[key];
|
|
24
|
+
const lane = key === "starter" ? "bulk" : "hero";
|
|
25
|
+
const cogs = cogsPerReel({ clip: lane }) * t.reels;
|
|
26
|
+
const voiceSub = VOICE.elevenlabs.usdPerMonth; // one sub covers all clients
|
|
27
|
+
const margin = t.priceUsd - cogs;
|
|
28
|
+
const marginPct = ((margin / t.priceUsd) * 100).toFixed(0);
|
|
29
|
+
console.log(` ${t.name.padEnd(10)} ${usd(t.priceUsd)}/mo ${String(t.reels).padStart(2)} Reels COGS ${usd(cogs)} → margin ${usd(margin)} (${marginPct}%)`);
|
|
30
|
+
}
|
|
31
|
+
console.log(` (+ shared fixed: ElevenLabs sub ${usd(VOICE.elevenlabs.usdPerMonth)}/mo across all clients; tooling floor ~$130/mo automated)`);
|
|
32
|
+
|
|
33
|
+
console.log("\n=== YOUR OWN pages ===");
|
|
34
|
+
const own = Number(process.argv[2] || "90");
|
|
35
|
+
const ownCogs = cogsPerReel({ clip: "hero" }) * own;
|
|
36
|
+
console.log(` ${own} Reels/mo automated (Veo hero): clips+text+voice ≈ ${usd(ownCogs)} | manual clips ≈ ${usd(cogsPerReel({ clip: "manual" }) * own)}`);
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Content lead — the single callable entry the Weftens front door hands to when a
|
|
2
|
+
// request is about making content. The door calls this; this runs the 10-seat
|
|
3
|
+
// pipeline and returns one result. The door never addresses the individual seats
|
|
4
|
+
// (ideator, writer, qc, …) — the Content lead owns them.
|
|
5
|
+
//
|
|
6
|
+
// It shells the existing orchestrator rather than importing a run() function: the
|
|
7
|
+
// orchestrator's full-run body is argv/global-driven, and wrapping it by shell keeps
|
|
8
|
+
// its 27 passing tests untouched (no refactor risk). The orchestrator writes packet
|
|
9
|
+
// JSON into content-org/inbox/<date>/; we run it, read those packets back, and return
|
|
10
|
+
// them as the result.
|
|
11
|
+
//
|
|
12
|
+
// Defaults to dry-run. A real content run depends on Phase-0 decisions that are
|
|
13
|
+
// Kayla's and still open (local vs cloud, cadence, keys) — so this wiring proves the
|
|
14
|
+
// pipeline is callable as one node WITHOUT producing real published content. Pass
|
|
15
|
+
// { dry: false } only once those decisions are made and a key is configured.
|
|
16
|
+
|
|
17
|
+
import { execFile } from "node:child_process";
|
|
18
|
+
import { promisify } from "node:util";
|
|
19
|
+
import { readFile, readdir, rm } from "node:fs/promises";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { dirname, join } from "node:path";
|
|
22
|
+
|
|
23
|
+
const execFileP = promisify(execFile);
|
|
24
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run the content pipeline as one node.
|
|
28
|
+
* @param opts { format?, count?, client?, tier?, dry? } // dry defaults true (safe)
|
|
29
|
+
* @returns { dry, format, count, artifacts, result, log }
|
|
30
|
+
* artifacts packet file paths produced
|
|
31
|
+
* result the parsed packets (dry-run packets are stub-shaped, clearly slugged dryrun-N)
|
|
32
|
+
*/
|
|
33
|
+
export async function runContent(opts = {}) {
|
|
34
|
+
const { format = "scam-reveal", count = 1, client = null, tier = null, dry = true } = opts;
|
|
35
|
+
|
|
36
|
+
const args = ["orchestrator.mjs", format, String(count)];
|
|
37
|
+
if (client) args.push(`--client=${client}`);
|
|
38
|
+
if (tier) args.push(`--tier=${tier}`);
|
|
39
|
+
if (dry) args.push("--dry-run");
|
|
40
|
+
|
|
41
|
+
const { stdout } = await execFileP("node", args, { cwd: HERE, maxBuffer: 8_000_000 });
|
|
42
|
+
|
|
43
|
+
// The orchestrator writes to inbox/<date>/; recover <date> from its own output line
|
|
44
|
+
// ("Packets in inbox/2026-07-19/.") rather than recomputing a clock here.
|
|
45
|
+
const m = stdout.match(/Packets in (inbox[\\/][^\s/]+)/);
|
|
46
|
+
const outRel = m ? m[1].replace(/[\\/]$/, "") : null;
|
|
47
|
+
const artifacts = [];
|
|
48
|
+
const result = [];
|
|
49
|
+
if (outRel) {
|
|
50
|
+
const outDir = join(HERE, outRel);
|
|
51
|
+
for (const name of (await readdir(outDir)).filter((n) => n.endsWith(".json"))) {
|
|
52
|
+
const file = join(outDir, name);
|
|
53
|
+
artifacts.push(file);
|
|
54
|
+
result.push(JSON.parse(await readFile(file, "utf8")));
|
|
55
|
+
}
|
|
56
|
+
// A dry run's stub packets are throwaway — remove them so a verification run does
|
|
57
|
+
// not litter content-org/inbox. Real runs keep their packets.
|
|
58
|
+
if (dry) {
|
|
59
|
+
await Promise.all(artifacts.map((f) => rm(f, { force: true })));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { dry, format, count, artifacts, result, log: stdout.trim() };
|
|
64
|
+
}
|