weftens 0.1.3 → 0.1.5
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/ARCHITECTURE.md +66 -0
- package/CHANGELOG.md +63 -0
- package/README.md +12 -4
- package/agents/agent2060.md +9 -9
- package/agents/amazon-ppc.md +42 -42
- package/agents/claims-content.md +35 -35
- package/agents/content-ops.md +46 -46
- package/agents/growth-marketing.md +40 -0
- package/agents/hilbert.md +1 -1
- package/agents/lippmann.md +8 -8
- package/agents/meta-ads.md +38 -0
- package/agents/paid-search.md +34 -35
- package/agents/pio.md +4 -4
- package/agents/richy.md +11 -11
- package/agents/security-writer.md +64 -0
- package/agents/seo-content.md +57 -57
- package/agents/seo-technical.md +53 -53
- package/agents/social-voice.md +38 -38
- package/agents/supply-verify.md +38 -38
- package/agents/writer.md +6 -6
- package/bin/weftens.js +8 -1
- package/package.json +6 -3
- package/src/format.js +77 -0
package/agents/writer.md
CHANGED
|
@@ -9,10 +9,10 @@ words built from found language, aimed at a reader who owes you nothing.
|
|
|
9
9
|
## The method spine (both output shapes)
|
|
10
10
|
The writing is not the job — research is. Before drafting, mine the real language:
|
|
11
11
|
- For content pieces: the research packet (Researcher's facts, sources, claims-to-verify) and the
|
|
12
|
-
|
|
12
|
+
format brief; the audience's own words from comments/UGC where available.
|
|
13
13
|
- For conversion copy: the corpus (reviews of the product AND competitors, threads, transcripts,
|
|
14
|
-
|
|
15
|
-
|
|
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
16
|
Copy assembled from found language beats invented copy; the customer's words are more specific,
|
|
17
17
|
more emotional, and more believable than anything written at a desk.
|
|
18
18
|
|
|
@@ -34,11 +34,11 @@ the highest frequency × weight pain, sequence proof against objections in desce
|
|
|
34
34
|
|
|
35
35
|
## Hard rules (both shapes)
|
|
36
36
|
- Banned: marketer vocabulary ("streamline," "unlock," "empower," "seamless," "elevate"), wordplay
|
|
37
|
-
|
|
37
|
+
that costs clarity, invented benefits, superlatives without a source.
|
|
38
38
|
- Every claim traces to the corpus, the research packet, or the offer — a caught overclaim kills
|
|
39
|
-
|
|
39
|
+
the asset.
|
|
40
40
|
- Voice is locked per surface (Showrunner's guardrails for own channels; the client voice doc for
|
|
41
|
-
|
|
41
|
+
client work). Drift is a defect even when the copy is good.
|
|
42
42
|
- Nothing publishes from this seat — Script Editor / Disclosure-QC / human approval sit downstream.
|
|
43
43
|
|
|
44
44
|
Good output: a line the audience could have said themselves, in the locked voice. Bad output:
|
package/bin/weftens.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
16
16
|
import readline from "node:readline";
|
|
17
17
|
import { orchestrate } from "../src/weftens.js";
|
|
18
|
-
import { isChangeSheet, formatChangeSheet } from "../src/format.js";
|
|
18
|
+
import { isChangeSheet, formatChangeSheet, looksUnparsed, unparsedNotice } from "../src/format.js";
|
|
19
19
|
import { askWeftens } from "../src/agent.js";
|
|
20
20
|
import { buildSpecialistClients } from "../src/specialists.js";
|
|
21
21
|
import { route } from "../src/agent-router.js";
|
|
@@ -153,6 +153,13 @@ async function cmdSend(args, { routeOnly } = {}) {
|
|
|
153
153
|
if (outcome.result !== undefined) {
|
|
154
154
|
// A change sheet renders for a human by default — the whole point of one is that a
|
|
155
155
|
// person can read a line and decide. `--json` returns the raw object for piping.
|
|
156
|
+
if (!asJson && looksUnparsed(outcome.result)) {
|
|
157
|
+
process.stdout.write(`
|
|
158
|
+
${unparsedNotice(outcome.result)}
|
|
159
|
+
`);
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
156
163
|
if (!asJson && isChangeSheet(outcome.result)) {
|
|
157
164
|
process.stdout.write(`\n${formatChangeSheet(outcome.result)}\n`);
|
|
158
165
|
} else {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weftens",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Weftens
|
|
5
|
+
"description": "Weftens — one front door that routes a marketing/representation request to the right agent and runs it on your own AI subscription (bring-your-own-model). Usable as a CLI or an SDK.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.js",
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"providers.json.example",
|
|
24
24
|
"README.md",
|
|
25
25
|
"LICENSE",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"ARCHITECTURE.md",
|
|
26
28
|
"!**/.preflight",
|
|
27
29
|
"!**/.preflight/**"
|
|
28
30
|
],
|
|
@@ -39,7 +41,8 @@
|
|
|
39
41
|
"vitest": "^2.0.0"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
42
|
-
"@anthropic-ai/sdk": "^0.110.0"
|
|
44
|
+
"@anthropic-ai/sdk": "^0.110.0",
|
|
45
|
+
"weftens": "^0.1.4"
|
|
43
46
|
},
|
|
44
47
|
"repository": {
|
|
45
48
|
"type": "git",
|
package/src/format.js
CHANGED
|
@@ -35,6 +35,80 @@ export function isChangeSheet(result) {
|
|
|
35
35
|
return Boolean(result && typeof result === "object" && Array.isArray(result.changes));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
|
|
39
|
+
// A core that was handed the WRONG report does not usually error — it parses the rows, finds none of
|
|
40
|
+
// the columns it wants, and returns a well-formed sheet with zero changes. Rendered normally that
|
|
41
|
+
// reads as "your account is clean", which is the most expensive lie this tool could tell: a user
|
|
42
|
+
// exports the wrong file and is told there is nothing to fix. Five of seven cores did exactly this.
|
|
43
|
+
//
|
|
44
|
+
// The signal is that rows were read but every money/volume figure came back zero or null. A real
|
|
45
|
+
// account under audit always has spend. An account with genuinely zero activity is indistinguishable
|
|
46
|
+
// — and for that one, "this doesn't look like the right export, or there was no activity" is also
|
|
47
|
+
// the truthful answer.
|
|
48
|
+
// Two different kinds of number, and conflating them is what let four cores slip past the first two
|
|
49
|
+
// versions of this check. ROW fields count what was READ — a non-zero row count proves the file was
|
|
50
|
+
// opened, not that it was understood. VALUE fields are what the report is FOR. The signal for "wrong
|
|
51
|
+
// file" is: rows were read, and every value came back zero.
|
|
52
|
+
const ROW_FIELDS = ["rows", "terms", "placements", "urls", "pages", "items", "searchTermRows", "campaignRows"];
|
|
53
|
+
const VALUE_FIELDS = [
|
|
54
|
+
"totalCost", "totalSpend", "totalClicks", "totalImpressions", "totalConversions",
|
|
55
|
+
"totalResults", "spend", "cost", "clicks", "impressions", "conversions", "sales", "orders",
|
|
56
|
+
"flaggedSpend", "reclaimableSpend", "totalViews", "views",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export function looksUnparsed(result) {
|
|
60
|
+
if (!result || typeof result !== "object") return false;
|
|
61
|
+
if (Array.isArray(result.changes) && result.changes.length > 0) return false;
|
|
62
|
+
// Cores don't share one result shape: some report totals under `account`, others under `summary`,
|
|
63
|
+
// others at the top level. Check wherever the volume figures actually live rather than assuming a
|
|
64
|
+
// shape — assuming `account` is why four of the five cores slipped past the first version of this.
|
|
65
|
+
// Every core names its totals block differently — `account`, `summary`, `totals` — and the first
|
|
66
|
+
// two versions of this guard each missed the cores they didn't happen to name. Scan any one-level
|
|
67
|
+
// block that carries volume fields instead of guessing at keys; a new core is covered for free.
|
|
68
|
+
const blocks = [result, ...Object.values(result).filter((v) => v && typeof v === "object" && !Array.isArray(v))];
|
|
69
|
+
const zero = (v) => v === 0 || v === null || v === undefined;
|
|
70
|
+
|
|
71
|
+
// Strongest signal first: a core that reports it read ZERO rows read nothing at all. Whatever the
|
|
72
|
+
// file was, it was not this report.
|
|
73
|
+
for (const b of blocks) {
|
|
74
|
+
for (const f of ROW_FIELDS) if (b[f] === 0) return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const b of blocks) {
|
|
78
|
+
const values = VALUE_FIELDS.filter((f) => f in b && typeof b[f] !== "object");
|
|
79
|
+
if (values.length === 0) continue;
|
|
80
|
+
if (!values.every((f) => zero(b[f]))) continue;
|
|
81
|
+
// Every value is zero. If rows were read, the file parsed as text but not as this report.
|
|
82
|
+
// If no row count is reported either, an all-zero result is still not a clean bill of health.
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Row count from wherever a core reports it — used only to make the refusal message concrete. */
|
|
89
|
+
function rowsRead(result) {
|
|
90
|
+
if (!result || typeof result !== "object") return null;
|
|
91
|
+
const blocks = [result, ...Object.values(result).filter((v) => v && typeof v === "object" && !Array.isArray(v))];
|
|
92
|
+
for (const b of blocks) {
|
|
93
|
+
for (const f of ROW_FIELDS) if (typeof b[f] === "number" && b[f] > 0) return b[f];
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
/** The one message for "this file did not parse", shared by every core shape. */
|
|
100
|
+
export function unparsedNotice(result) {
|
|
101
|
+
const rows = rowsRead(result);
|
|
102
|
+
return [
|
|
103
|
+
"Could not read this as the report this agent expects.",
|
|
104
|
+
typeof rows === "number" && rows > 0
|
|
105
|
+
? ` ${rows} row(s) were read, but none of the expected columns (spend, clicks, results) had values.`
|
|
106
|
+
: " No rows with the expected columns were found.",
|
|
107
|
+
" This is NOT a finding that your account is clean — it means the file did not parse.",
|
|
108
|
+
" Check you exported the right report for this agent, then run it again.",
|
|
109
|
+
].join("\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
38
112
|
/**
|
|
39
113
|
* Render a change sheet as plain text. Never invents a field: anything missing is simply
|
|
40
114
|
* not printed, so a core that doesn't produce risk/evidence still renders cleanly.
|
|
@@ -50,6 +124,9 @@ export function formatChangeSheet(result) {
|
|
|
50
124
|
const changes = result.changes.filter((c) => c && typeof c === "object");
|
|
51
125
|
const dropped = result.changes.length - changes.length;
|
|
52
126
|
|
|
127
|
+
// Refuse to present an unreadable file as a clean bill of health.
|
|
128
|
+
if (looksUnparsed(result)) return unparsedNotice(result);
|
|
129
|
+
|
|
53
130
|
const flagged = money(result.wastedSpendRecoverable, currency);
|
|
54
131
|
const header = [`${changes.length} proposed change${changes.length === 1 ? "" : "s"}`];
|
|
55
132
|
// "flagged", not "recoverable": the tool measured spend against a threshold, it did not
|