weftens 0.1.0 → 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 +56 -16
- package/bin/weftens.js +234 -226
- package/package.json +1 -1
- package/src/format.js +66 -0
package/README.md
CHANGED
|
@@ -1,18 +1,57 @@
|
|
|
1
1
|
# Weftens
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
**Where did the ad money go, and what should change?** Give Weftens an exported report from
|
|
4
|
+
Google Ads, Meta, Amazon, or an SEO crawl, and it gives you back a ranked change sheet — each
|
|
5
|
+
recommendation with where it applies, why it's proposed, what it's worth, and what could go
|
|
6
|
+
wrong if you're wrong.
|
|
7
|
+
|
|
8
|
+
4 proposed changes | $604.27 of spend flagged
|
|
9
|
+
account: spend $910.00, 208 clicks, 11 conversions, CPA $82.73
|
|
10
|
+
|
|
11
|
+
1. Bid down or restrict "premium widget wholesale bulk" $417.27
|
|
12
|
+
where campaign: Brand / Brand - Exact
|
|
13
|
+
why CPA $500 vs account $82.73 (6x); $500 for 1 conv
|
|
14
|
+
risk Converting term — restricting too hard can cut real volume; prefer a bid cut first.
|
|
15
|
+
|
|
16
|
+
2. Add "free blue widgets" as a negative keyword $90.00
|
|
17
|
+
where campaign: Generic / Widgets - Broad
|
|
18
|
+
why 60 clicks, $90 spent, 0 conversions (expected ~3.2 at the 5.3% account rate)
|
|
19
|
+
risk If the query is actually relevant, you forfeit those impressions.
|
|
20
|
+
|
|
21
|
+
Proposals only — nothing was sent to your ad account. Apply what you agree with.
|
|
22
|
+
|
|
23
|
+
That is the actual output of the sample export included in this repo, produced by:
|
|
24
|
+
|
|
25
|
+
npm i weftens
|
|
26
|
+
npx weftens send paid-search audit --input agent-cores/paid-search/test/fixtures/search-terms.csv
|
|
27
|
+
|
|
28
|
+
**Same file in, same change sheet out, every time.** The ad and SEO auditors are arithmetic, not
|
|
29
|
+
a model — nothing is generated, so nothing can be invented, and two runs can be diffed to prove
|
|
30
|
+
nothing moved. They read a file you already have and **never sign in to your ad account**. Note
|
|
31
|
+
the second line of the first recommendation: that keyword *converted*. "Flagged" means the numbers
|
|
32
|
+
crossed a threshold, not that the money is guaranteed back — the risk line is there because the
|
|
33
|
+
call is still yours.
|
|
34
|
+
|
|
35
|
+
**Not technical?** You don't have to run this. Send the repo link and your exported report to
|
|
36
|
+
whoever manages your ads, or paste this to an AI assistant that can use your computer
|
|
37
|
+
(Claude, ChatGPT/Codex, Gemini): *"install weftens and run the paid-search audit on my export."*
|
|
38
|
+
|
|
39
|
+
## What's finished, and what isn't
|
|
40
|
+
|
|
41
|
+
- **Finished and deterministic — seven auditors:** paid search (Google Ads), paid social (Meta),
|
|
42
|
+
Amazon PPC, technical SEO, content SEO, supply-path verification, content operations. No model,
|
|
43
|
+
no key, propose-only.
|
|
44
|
+
- **Model-backed seats** run on an AI CLI *you* already have and are logged into — your
|
|
45
|
+
subscription, no API key, nobody is billed through Weftens. They are only as good as the model
|
|
46
|
+
you point them at. `claude` and `codex` are verified; `gemini` and `grok` are configured but have
|
|
47
|
+
never been run.
|
|
48
|
+
- **Two seats are not in this repo.** `richy` (representation) and `pio` (prospect research) are
|
|
49
|
+
clients for separate services that aren't open source. Without those services configured they
|
|
50
|
+
return sample data that says so in its own output.
|
|
51
|
+
- **The content pipeline is a placeholder.** Ten seats exist; it emits stub output and does not yet
|
|
52
|
+
take a free-text brief.
|
|
53
|
+
|
|
54
|
+
Free, MIT licensed, no account, no billing.
|
|
16
55
|
|
|
17
56
|
## Install
|
|
18
57
|
|
|
@@ -116,9 +155,10 @@ CLI prints which words matched, so you always see why a request went where it di
|
|
|
116
155
|
- **The router refuses rather than guesses.** Generic words alone ("site", "listing", "people") no
|
|
117
156
|
longer dispatch — a confident wrong route in front of a customer costs more than a reprompt.
|
|
118
157
|
- **Licensed MIT.** Free to use, including commercially.
|
|
119
|
-
- **
|
|
120
|
-
|
|
121
|
-
|
|
158
|
+
- **Published:** `npm i weftens` installs the real thing (`weftens@0.1.0`, MIT). Verified from the
|
|
159
|
+
registry by a clean third-party install — `npx weftens list` prints the registry, and a
|
|
160
|
+
deterministic core runs a real audit from inside `node_modules`. (`0.0.1` was an empty name-holder;
|
|
161
|
+
ignore it.)
|
|
122
162
|
- **Two providers verified, not one:** `claude` and `codex` both produce end-to-end through the
|
|
123
163
|
user's own subscription (codex confirmed 2026-07-19). `gemini` and `grok` remain config-only — the
|
|
124
164
|
shape is right, but neither CLI is installed here, so neither has been run. A missing or broken
|
package/bin/weftens.js
CHANGED
|
@@ -1,226 +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 {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (set.length
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
else
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
process.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
if (question
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
// (
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
process.stdout.write(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
else if (args[i] === "--
|
|
146
|
-
else if (args[i] === "--
|
|
147
|
-
else
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
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
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weftens",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Weftens \u2014 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",
|
package/src/format.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Human-readable rendering of an auditor's change sheet.
|
|
2
|
+
//
|
|
3
|
+
// The auditors always emitted correct JSON, and JSON is the right thing for a program to
|
|
4
|
+
// consume — but it is the wrong thing to put in front of the person deciding whether to
|
|
5
|
+
// act. The whole value of a change sheet is that a human can read one line and know what
|
|
6
|
+
// to do, why, and what it costs to be wrong; a wall of braces hides exactly that. So the
|
|
7
|
+
// CLI renders by default and `--json` returns the raw object for piping.
|
|
8
|
+
//
|
|
9
|
+
// Deliberately plain text: no colour, no box-drawing, no spinners. This output gets pasted
|
|
10
|
+
// into an email to a client or a ticket, and it has to survive being pasted.
|
|
11
|
+
|
|
12
|
+
const money = (n) =>
|
|
13
|
+
typeof n === "number" && Number.isFinite(n)
|
|
14
|
+
? "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
|
15
|
+
: null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* True when a result looks like an auditor change sheet (rather than, say, a model answer
|
|
19
|
+
* or an orchestrator payload). Kept structural rather than name-based so a new core gets
|
|
20
|
+
* the rendering for free.
|
|
21
|
+
*/
|
|
22
|
+
export function isChangeSheet(result) {
|
|
23
|
+
return Boolean(result && typeof result === "object" && Array.isArray(result.changes));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Render a change sheet as plain text. Never invents a field: anything missing is simply
|
|
28
|
+
* not printed, so a core that doesn't produce risk/evidence still renders cleanly.
|
|
29
|
+
*/
|
|
30
|
+
export function formatChangeSheet(result) {
|
|
31
|
+
const out = [];
|
|
32
|
+
const changes = result.changes ?? [];
|
|
33
|
+
|
|
34
|
+
const flagged = money(result.wastedSpendRecoverable);
|
|
35
|
+
const header = [`${changes.length} proposed change${changes.length === 1 ? "" : "s"}`];
|
|
36
|
+
// "flagged", not "recoverable": the tool measured spend against a threshold, it did not
|
|
37
|
+
// establish that the money comes back. Some flagged terms convert — see the risk line.
|
|
38
|
+
if (flagged) header.push(`${flagged} of spend flagged`);
|
|
39
|
+
out.push(header.join(" | "));
|
|
40
|
+
|
|
41
|
+
const a = result.account;
|
|
42
|
+
if (a) {
|
|
43
|
+
const bits = [];
|
|
44
|
+
if (typeof a.totalCost === "number") bits.push(`spend ${money(a.totalCost)}`);
|
|
45
|
+
if (typeof a.totalClicks === "number") bits.push(`${a.totalClicks} clicks`);
|
|
46
|
+
if (typeof a.totalConversions === "number") bits.push(`${a.totalConversions} conversions`);
|
|
47
|
+
if (typeof a.accountCPA === "number") bits.push(`CPA ${money(a.accountCPA)}`);
|
|
48
|
+
if (bits.length) out.push(`account: ${bits.join(", ")}`);
|
|
49
|
+
}
|
|
50
|
+
out.push("");
|
|
51
|
+
|
|
52
|
+
for (const c of changes) {
|
|
53
|
+
const amount = money(c.dollars);
|
|
54
|
+
out.push(`${c.rank ?? "-"}. ${c.change}${amount ? ` ${amount}` : ""}`);
|
|
55
|
+
if (c.where) out.push(` where ${c.where}`);
|
|
56
|
+
if (c.why) out.push(` why ${c.why}`);
|
|
57
|
+
if (c.expectedEffect) out.push(` effect ${c.expectedEffect}`);
|
|
58
|
+
if (c.risk) out.push(` risk ${c.risk}`);
|
|
59
|
+
out.push("");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (result.proposeOnly) {
|
|
63
|
+
out.push("Proposals only — nothing was sent to your ad account. Apply what you agree with.");
|
|
64
|
+
}
|
|
65
|
+
return out.join("\n");
|
|
66
|
+
}
|