@veleads/gtm 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/cli.js +200 -34
- package/kit/AGENTS.md +14 -7
- package/kit/schemas/account.schema.json +0 -5
- package/kit/schemas/contact.schema.json +0 -5
- package/kit/schemas/lead.schema.json +0 -5
- package/kit/schemas/signal.schema.json +0 -5
- package/kit/skills/02-tam/SKILL.md +4 -3
- package/kit/stack/docker-compose.yml +66 -0
- package/kit/stack/init/01-hatchet-db.sql +2 -0
- package/kit/templates/repo/README.md +1 -0
- package/kit/templates/repo/env.example +3 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to `@veleads/gtm`. Each entry is tagged: **no action**, **op
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.0] - 2026-10-24
|
|
9
|
+
- **opt-in** `gtm stack up | down [--wipe] | status`: each project repo runs its own stack (Postgres +
|
|
10
|
+
hatchet-lite + gtm worker) on Docker, as compose project `gtm-<project_id>` with its own volumes. The
|
|
11
|
+
worker is the image `ghcr.io/veleads/gtm-worker:<version>` matching the pinned core; it applies
|
|
12
|
+
database migrations when it starts. Connection settings go to `~/.gtm/stacks/<project_id>.env`
|
|
13
|
+
(used by `gtm job`), provider keys to `~/.gtm/stacks/<project_id>.secrets.env`; nothing is written
|
|
14
|
+
into the repo. Ports are configurable (`GTM_PG_PORT`, `GTM_HATCHET_UI_PORT`, `GTM_HATCHET_GRPC_PORT`)
|
|
15
|
+
and bound to localhost only. Releases now also publish the worker image. The gtm-core development
|
|
16
|
+
stack (`pnpm stack:up` + `pnpm worker`) still works as a fallback for `gtm job`.
|
|
17
|
+
- **action** One database per project: each project repo runs its own stack (Postgres + Hatchet +
|
|
18
|
+
workers) and core holds no project state. Rows no longer carry `project_id`; unique keys are now
|
|
19
|
+
per database (`accounts` on `tam_id` + `domain`, `contacts` on `linkedin_url`, …). A new
|
|
20
|
+
single-row `project` table binds the database to one `project_id` on the first job, and workers
|
|
21
|
+
refuse jobs for any other project. The `account`, `contact`, `lead` and `signal` schemas drop
|
|
22
|
+
`project_id`; `project.yaml` and job payloads keep it. DB migration
|
|
23
|
+
`20261025000000_database_per_project` binds an existing single-project database automatically and
|
|
24
|
+
**refuses to run on a database that holds more than one project**: give each project its own
|
|
25
|
+
database (copy its rows, filtered by `project_id`) before upgrading. Point each project's
|
|
26
|
+
`DATABASE_URL` at its own database.
|
|
27
|
+
|
|
8
28
|
## [0.6.0] - 2026-10-24
|
|
9
29
|
- **no action** Layered ICP: `context/icp.yaml` can have an optional `core.firmographics` that every
|
|
10
30
|
segment shares; segments then hold only what differs, and a segment field **replaces** the same core
|
package/dist/cli.js
CHANGED
|
@@ -280,7 +280,6 @@ var CampaignConfig = z5.object({
|
|
|
280
280
|
// ../schemas/src/account.ts
|
|
281
281
|
import { z as z6 } from "zod";
|
|
282
282
|
var Account = z6.object({
|
|
283
|
-
project_id: Slug,
|
|
284
283
|
tam_id: Slug,
|
|
285
284
|
domain: Domain,
|
|
286
285
|
name: z6.string().min(1),
|
|
@@ -297,7 +296,6 @@ var Account = z6.object({
|
|
|
297
296
|
import { z as z7 } from "zod";
|
|
298
297
|
var EmailStatus = z7.enum(["valid", "catch_all", "invalid", "unknown", "not_found"]);
|
|
299
298
|
var Contact = z7.object({
|
|
300
|
-
project_id: Slug,
|
|
301
299
|
account_domain: Domain,
|
|
302
300
|
first_name: z7.string().min(1),
|
|
303
301
|
last_name: z7.string().min(1),
|
|
@@ -314,7 +312,6 @@ var Contact = z7.object({
|
|
|
314
312
|
// ../schemas/src/signal.ts
|
|
315
313
|
import { z as z8 } from "zod";
|
|
316
314
|
var SignalEvent = z8.object({
|
|
317
|
-
project_id: Slug,
|
|
318
315
|
signal_key: Slug,
|
|
319
316
|
account_domain: Domain,
|
|
320
317
|
contact_linkedin_url: z8.string().url().nullable(),
|
|
@@ -327,7 +324,6 @@ var SignalEvent = z8.object({
|
|
|
327
324
|
// ../schemas/src/lead.ts
|
|
328
325
|
import { z as z9 } from "zod";
|
|
329
326
|
var Lead = z9.object({
|
|
330
|
-
project_id: Slug,
|
|
331
327
|
campaign_id: Slug,
|
|
332
328
|
run_id: z9.uuid(),
|
|
333
329
|
email: z9.email(),
|
|
@@ -467,9 +463,9 @@ function githubToken() {
|
|
|
467
463
|
}
|
|
468
464
|
var login = (handle) => handle.replace(/^@/, "").toLowerCase();
|
|
469
465
|
var GitHubError = class extends Error {
|
|
470
|
-
constructor(
|
|
466
|
+
constructor(status2, message) {
|
|
471
467
|
super(message);
|
|
472
|
-
this.status =
|
|
468
|
+
this.status = status2;
|
|
473
469
|
this.name = "GitHubError";
|
|
474
470
|
}
|
|
475
471
|
status;
|
|
@@ -1299,15 +1295,62 @@ import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
|
|
|
1299
1295
|
import { homedir } from "os";
|
|
1300
1296
|
import { join as join9 } from "path";
|
|
1301
1297
|
var STACK_ENV_FILE = process.env.GTM_STACK_ENV ?? join9(homedir(), ".gtm", "stack.env");
|
|
1302
|
-
|
|
1303
|
-
|
|
1298
|
+
var STACKS_DIR = process.env.GTM_STACKS_DIR ?? join9(homedir(), ".gtm", "stacks");
|
|
1299
|
+
var projectStackEnvFile = (projectId) => join9(STACKS_DIR, `${projectId}.env`);
|
|
1300
|
+
var projectSecretsFile = (projectId) => join9(STACKS_DIR, `${projectId}.secrets.env`);
|
|
1301
|
+
function readEnvFile(file) {
|
|
1302
|
+
const out = {};
|
|
1303
|
+
if (!existsSync7(file)) return out;
|
|
1304
1304
|
for (const line of readFileSync8(file, "utf8").split("\n")) {
|
|
1305
1305
|
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
1306
|
-
if (m
|
|
1306
|
+
if (m) out[m[1]] = m[2].replace(/^["']|["']$/g, "");
|
|
1307
1307
|
}
|
|
1308
|
+
return out;
|
|
1309
|
+
}
|
|
1310
|
+
function loadStackEnv(file = STACK_ENV_FILE) {
|
|
1311
|
+
if (!existsSync7(file)) return false;
|
|
1312
|
+
for (const [k, v] of Object.entries(readEnvFile(file))) if (process.env[k] === void 0) process.env[k] = v;
|
|
1308
1313
|
return true;
|
|
1309
1314
|
}
|
|
1310
1315
|
|
|
1316
|
+
// ../core/src/local-stack.ts
|
|
1317
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1318
|
+
async function waitFor(what, check, seconds = 120) {
|
|
1319
|
+
for (let i = 0; i < seconds; i++) {
|
|
1320
|
+
if (await check()) return;
|
|
1321
|
+
await sleep(1e3);
|
|
1322
|
+
}
|
|
1323
|
+
throw new Error(`timed out waiting for ${what}`);
|
|
1324
|
+
}
|
|
1325
|
+
async function hatchetReady(url) {
|
|
1326
|
+
try {
|
|
1327
|
+
return (await fetch(`${url}/api/ready`)).ok;
|
|
1328
|
+
} catch {
|
|
1329
|
+
return false;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async function createHatchetToken(exec, name) {
|
|
1333
|
+
let tenant = "";
|
|
1334
|
+
await waitFor("hatchet tenant", () => {
|
|
1335
|
+
try {
|
|
1336
|
+
tenant = exec("postgres", ["psql", "-U", "gtm", "-d", "hatchet", "-tAc", `select id from "Tenant" where slug = 'default'`]);
|
|
1337
|
+
} catch {
|
|
1338
|
+
tenant = "";
|
|
1339
|
+
}
|
|
1340
|
+
return /^[0-9a-f-]{36}$/.test(tenant);
|
|
1341
|
+
});
|
|
1342
|
+
let token = "";
|
|
1343
|
+
await waitFor("hatchet token", () => {
|
|
1344
|
+
try {
|
|
1345
|
+
token = exec("hatchet-lite", ["/hatchet-admin", "token", "create", "--config", "/config", "--tenant-id", tenant, "--name", name]).split("\n").at(-1).trim();
|
|
1346
|
+
} catch {
|
|
1347
|
+
token = "";
|
|
1348
|
+
}
|
|
1349
|
+
return /^eyJ[\w-]+\.[\w-]+\.[\w-]+$/.test(token);
|
|
1350
|
+
}, 60);
|
|
1351
|
+
return token;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1311
1354
|
// ../core/src/adapters/mock-account-source.ts
|
|
1312
1355
|
import { createHash as createHash3 } from "crypto";
|
|
1313
1356
|
|
|
@@ -1644,9 +1687,10 @@ async function runJob(root2, args2) {
|
|
|
1644
1687
|
console.error(input);
|
|
1645
1688
|
return false;
|
|
1646
1689
|
}
|
|
1690
|
+
loadStackEnv(projectStackEnvFile(input.ctx.project_id));
|
|
1647
1691
|
loadStackEnv();
|
|
1648
1692
|
if (!process.env.HATCHET_CLIENT_TOKEN) {
|
|
1649
|
-
console.error(`No Hatchet connection.
|
|
1693
|
+
console.error(`No Hatchet connection. Run \`npx gtm stack up\` in this project repo (writes ${projectStackEnvFile(input.ctx.project_id)}). Deployed: set HATCHET_CLIENT_TOKEN.`);
|
|
1650
1694
|
return false;
|
|
1651
1695
|
}
|
|
1652
1696
|
const { HatchetClient } = await import("@hatchet-dev/typescript-sdk/v1");
|
|
@@ -1655,7 +1699,7 @@ async function runJob(root2, args2) {
|
|
|
1655
1699
|
throw new Error("runs on the gtm worker");
|
|
1656
1700
|
} });
|
|
1657
1701
|
console.log(`\u2022 running tam for ${input.segments.length} segment(s) via ${input.source} (budget $${input.budget_usd})`);
|
|
1658
|
-
const hint = setTimeout(() => console.log(" \u2026 still waiting. Is the worker running? (`
|
|
1702
|
+
const hint = setTimeout(() => console.log(" \u2026 still waiting. Is the worker running? (`npx gtm stack status`)"), 2e4);
|
|
1659
1703
|
try {
|
|
1660
1704
|
const output = await tam.run(input);
|
|
1661
1705
|
printTam(TamJobResult.parse(output), campaign);
|
|
@@ -1695,30 +1739,136 @@ function runNewCampaign(root2, campaign) {
|
|
|
1695
1739
|
return true;
|
|
1696
1740
|
}
|
|
1697
1741
|
|
|
1698
|
-
// src/commands/
|
|
1699
|
-
import {
|
|
1742
|
+
// src/commands/stack.ts
|
|
1743
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
1744
|
+
import { chmodSync, existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync13, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
1700
1745
|
import { join as join14 } from "path";
|
|
1701
1746
|
import { parse as parse8 } from "yaml";
|
|
1747
|
+
var WORKER_IMAGE = "ghcr.io/veleads/gtm-worker";
|
|
1748
|
+
var PORTS = { GTM_PG_PORT: 5432, GTM_HATCHET_UI_PORT: 8888, GTM_HATCHET_GRPC_PORT: 7077 };
|
|
1749
|
+
function stackSettings(projectId, env = process.env) {
|
|
1750
|
+
const envFile = projectStackEnvFile(projectId);
|
|
1751
|
+
const saved = readEnvFile(envFile);
|
|
1752
|
+
const ports = Object.fromEntries(
|
|
1753
|
+
Object.keys(PORTS).map((k) => [k, Number(env[k] ?? saved[k] ?? PORTS[k])])
|
|
1754
|
+
);
|
|
1755
|
+
for (const [k, v] of Object.entries(ports)) if (!Number.isInteger(v) || v < 1 || v > 65535) throw new Error(`${k} must be a port number, got "${v}"`);
|
|
1756
|
+
return {
|
|
1757
|
+
projectId,
|
|
1758
|
+
composeProject: `gtm-${projectId}`,
|
|
1759
|
+
composeFile: join14(KIT_DIR, "stack", "docker-compose.yml"),
|
|
1760
|
+
envFile,
|
|
1761
|
+
secretsFile: projectSecretsFile(projectId),
|
|
1762
|
+
workerImage: env.GTM_WORKER_IMAGE ?? `${WORKER_IMAGE}:${GTM_VERSION}`,
|
|
1763
|
+
ports
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
function composeEnv(s, token = "") {
|
|
1767
|
+
return {
|
|
1768
|
+
...Object.fromEntries(Object.entries(s.ports).map(([k, v]) => [k, String(v)])),
|
|
1769
|
+
GTM_WORKER_IMAGE: s.workerImage,
|
|
1770
|
+
GTM_SECRETS_FILE: s.secretsFile,
|
|
1771
|
+
HATCHET_CLIENT_TOKEN: token
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
function stackEnvText(s, token) {
|
|
1775
|
+
return [
|
|
1776
|
+
`# Written by \`gtm stack up\` for project ${s.projectId}. Local stack only; never commit.`,
|
|
1777
|
+
`DATABASE_URL=postgresql://gtm:gtm@localhost:${s.ports.GTM_PG_PORT}/gtm`,
|
|
1778
|
+
`HATCHET_CLIENT_TOKEN=${token}`,
|
|
1779
|
+
"HATCHET_CLIENT_TLS_STRATEGY=none",
|
|
1780
|
+
...Object.entries(s.ports).map(([k, v]) => `${k}=${v}`),
|
|
1781
|
+
""
|
|
1782
|
+
].join("\n");
|
|
1783
|
+
}
|
|
1784
|
+
function compose(s, args2, opts = {}) {
|
|
1785
|
+
const res = spawnSync4("docker", ["compose", "-f", s.composeFile, "-p", s.composeProject, ...args2], {
|
|
1786
|
+
env: { ...process.env, ...composeEnv(s, opts.token) },
|
|
1787
|
+
encoding: "utf8",
|
|
1788
|
+
stdio: opts.quiet ? "pipe" : ["ignore", "inherit", "inherit"]
|
|
1789
|
+
});
|
|
1790
|
+
if (res.error) throw new Error(`docker is not available (${res.error.message}). Install Docker Desktop or Docker Engine first.`);
|
|
1791
|
+
if (res.status !== 0) throw new Error(`docker compose ${args2.join(" ")} failed${opts.quiet ? `: ${res.stderr || res.stdout}` : ""}`);
|
|
1792
|
+
return (res.stdout ?? "").trim();
|
|
1793
|
+
}
|
|
1794
|
+
async function up(s) {
|
|
1795
|
+
const ui = `http://localhost:${s.ports.GTM_HATCHET_UI_PORT}`;
|
|
1796
|
+
console.log(`\u2022 starting postgres + hatchet-lite (${s.composeProject})`);
|
|
1797
|
+
compose(s, ["up", "-d", "--wait", "postgres"]);
|
|
1798
|
+
compose(s, ["up", "-d", "hatchet-lite"]);
|
|
1799
|
+
await waitFor("hatchet-lite", () => hatchetReady(ui));
|
|
1800
|
+
console.log("\u2022 creating a Hatchet API token");
|
|
1801
|
+
const token = await createHatchetToken((service, args2) => compose(s, ["exec", "-T", service, ...args2], { quiet: true }), `gtm-${s.projectId}-${Date.now()}`);
|
|
1802
|
+
mkdirSync4(STACKS_DIR, { recursive: true, mode: 448 });
|
|
1803
|
+
writeFileSync6(s.envFile, stackEnvText(s, token));
|
|
1804
|
+
chmodSync(s.envFile, 384);
|
|
1805
|
+
console.log(`\u2022 starting the worker (${s.workerImage})`);
|
|
1806
|
+
compose(s, ["up", "-d", "worker"], { token });
|
|
1807
|
+
console.log(`
|
|
1808
|
+
Stack for ${s.projectId} is up.
|
|
1809
|
+
Hatchet UI ${ui} (local login admin@example.com / Admin123!!)
|
|
1810
|
+
Postgres postgresql://gtm:gtm@localhost:${s.ports.GTM_PG_PORT}/gtm (this project's database)
|
|
1811
|
+
Settings ${s.envFile} (used by \`npx gtm job\`)
|
|
1812
|
+
Secrets ${s.secretsFile} (provider API keys for the worker; ${existsSync12(s.secretsFile) ? "found" : "not created yet"})
|
|
1813
|
+
Next: \`npx gtm job tam <campaign>\`.`);
|
|
1814
|
+
}
|
|
1815
|
+
function down(s, wipe) {
|
|
1816
|
+
compose(s, ["down", ...wipe ? ["-v"] : []]);
|
|
1817
|
+
if (wipe) rmSync2(s.envFile, { force: true });
|
|
1818
|
+
console.log(wipe ? `Stack stopped; this project's database, Hatchet state and ${s.envFile} are deleted (secrets file kept).` : "Stack stopped; data kept (`npx gtm stack up` to resume).");
|
|
1819
|
+
}
|
|
1820
|
+
async function status(s) {
|
|
1821
|
+
compose(s, ["ps"]);
|
|
1822
|
+
const ready = await hatchetReady(`http://localhost:${s.ports.GTM_HATCHET_UI_PORT}`);
|
|
1823
|
+
console.log(`hatchet: ${ready ? "ready" : "not ready"} \xB7 settings: ${existsSync12(s.envFile) ? s.envFile : "missing (run `npx gtm stack up`)"} \xB7 worker image: ${s.workerImage}`);
|
|
1824
|
+
}
|
|
1825
|
+
async function runStack(root2, args2, opts = {}) {
|
|
1826
|
+
const [action] = args2;
|
|
1827
|
+
if (!action || !["up", "down", "status"].includes(action)) {
|
|
1828
|
+
console.error("Usage: gtm stack up | down [--wipe] | status");
|
|
1829
|
+
return false;
|
|
1830
|
+
}
|
|
1831
|
+
if (!existsSync12(join14(root2, "project.yaml"))) {
|
|
1832
|
+
console.error("project.yaml not found: run gtm stack inside a project repo.");
|
|
1833
|
+
return false;
|
|
1834
|
+
}
|
|
1835
|
+
try {
|
|
1836
|
+
const project = ProjectFile.parse(parse8(readFileSync13(join14(root2, "project.yaml"), "utf8")));
|
|
1837
|
+
const s = stackSettings(project.project_id);
|
|
1838
|
+
if (action === "up") await up(s);
|
|
1839
|
+
else if (action === "down") down(s, Boolean(opts.wipe));
|
|
1840
|
+
else await status(s);
|
|
1841
|
+
return true;
|
|
1842
|
+
} catch (e) {
|
|
1843
|
+
console.error(`gtm stack ${action}: ${e.message}`);
|
|
1844
|
+
return false;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// src/commands/status.ts
|
|
1849
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync14 } from "fs";
|
|
1850
|
+
import { join as join15 } from "path";
|
|
1851
|
+
import { parse as parse9 } from "yaml";
|
|
1702
1852
|
function readYaml2(file) {
|
|
1703
1853
|
try {
|
|
1704
|
-
return
|
|
1854
|
+
return parse9(readFileSync14(file, "utf8"));
|
|
1705
1855
|
} catch {
|
|
1706
1856
|
return null;
|
|
1707
1857
|
}
|
|
1708
1858
|
}
|
|
1709
1859
|
function campaignStages(root2) {
|
|
1710
|
-
return subdirs(
|
|
1860
|
+
return subdirs(join15(root2, "campaigns")).map((id) => ({ id, stage: readYaml2(join15(root2, "campaigns", id, "config.yaml"))?.stage ?? "?" }));
|
|
1711
1861
|
}
|
|
1712
1862
|
function runStatus(root2) {
|
|
1713
|
-
const project = readYaml2(
|
|
1714
|
-
const icp = readYaml2(
|
|
1863
|
+
const project = readYaml2(join15(root2, "project.yaml"));
|
|
1864
|
+
const icp = readYaml2(join15(root2, "context", "icp.yaml"));
|
|
1715
1865
|
console.log(`${project?.name ?? "?"} (${project?.project_id ?? "?"}) \xB7 status ${project?.status ?? "?"} \xB7 core v${GTM_VERSION}`);
|
|
1716
1866
|
console.log(`ICP gate: ${icp?.approval?.status ?? "?"}${icp?.approval?.approved_by ? ` by ${icp.approval.approved_by} on ${icp.approval.approved_at}` : ""}
|
|
1717
1867
|
`);
|
|
1718
|
-
const rows = subdirs(
|
|
1719
|
-
const cfg = readYaml2(
|
|
1720
|
-
const runsDir =
|
|
1721
|
-
const last =
|
|
1868
|
+
const rows = subdirs(join15(root2, "campaigns")).map((id) => {
|
|
1869
|
+
const cfg = readYaml2(join15(root2, "campaigns", id, "config.yaml")) ?? {};
|
|
1870
|
+
const runsDir = join15(root2, "campaigns", id, "runs");
|
|
1871
|
+
const last = existsSync13(runsDir) ? readdirSync3(runsDir).filter((f) => f.endsWith(".md")).sort().at(-1) ?? "-" : "-";
|
|
1722
1872
|
return [id, cfg.stage ?? "?", cfg.gates?.copy?.status ?? "?", cfg.gates?.launch?.status ?? "?", cfg.owner ?? "?", last];
|
|
1723
1873
|
});
|
|
1724
1874
|
if (!rows.length) {
|
|
@@ -1735,9 +1885,9 @@ function runStatus(root2) {
|
|
|
1735
1885
|
}
|
|
1736
1886
|
|
|
1737
1887
|
// src/commands/upgrade.ts
|
|
1738
|
-
import { spawnSync as
|
|
1739
|
-
import { existsSync as
|
|
1740
|
-
import { join as
|
|
1888
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
1889
|
+
import { existsSync as existsSync14, readFileSync as readFileSync15 } from "fs";
|
|
1890
|
+
import { join as join16 } from "path";
|
|
1741
1891
|
|
|
1742
1892
|
// src/changelog.ts
|
|
1743
1893
|
function compareVersions(a, b) {
|
|
@@ -1756,17 +1906,17 @@ function changelogBetween(text, from, to) {
|
|
|
1756
1906
|
|
|
1757
1907
|
// src/commands/upgrade.ts
|
|
1758
1908
|
function detectPackageManager(root2) {
|
|
1759
|
-
if (
|
|
1760
|
-
if (
|
|
1909
|
+
if (existsSync14(join16(root2, "pnpm-lock.yaml"))) return "pnpm";
|
|
1910
|
+
if (existsSync14(join16(root2, "yarn.lock"))) return "yarn";
|
|
1761
1911
|
return "npm";
|
|
1762
1912
|
}
|
|
1763
1913
|
function installExact(root2, pm, spec) {
|
|
1764
|
-
const onPath = (bin) =>
|
|
1914
|
+
const onPath = (bin) => spawnSync5(bin, ["--version"], { stdio: "ignore" }).status === 0;
|
|
1765
1915
|
const [cmd, args2] = pm === "npm" ? ["npm", ["install", "--save-dev", "--save-exact", spec]] : pm === "pnpm" ? onPath("pnpm") ? ["pnpm", ["add", "-D", "--save-exact", spec]] : ["npx", ["-y", "pnpm@10", "add", "-D", "--save-exact", spec]] : onPath("yarn") ? ["yarn", ["add", "-D", "--exact", spec]] : ["npx", ["-y", "yarn@1", "add", "-D", "--exact", spec]];
|
|
1766
|
-
return
|
|
1916
|
+
return spawnSync5(cmd, args2, { cwd: root2, stdio: "inherit" }).status ?? 1;
|
|
1767
1917
|
}
|
|
1768
1918
|
function gtm(root2, args2) {
|
|
1769
|
-
return
|
|
1919
|
+
return spawnSync5("npx", ["gtm", ...args2], { cwd: root2, stdio: "inherit" }).status ?? 1;
|
|
1770
1920
|
}
|
|
1771
1921
|
var FROZEN_STAGES = ["qa", "launch"];
|
|
1772
1922
|
function runUpgrade(root2, opts) {
|
|
@@ -1781,7 +1931,7 @@ function runUpgrade(root2, opts) {
|
|
|
1781
1931
|
console.error("Finish the launch first, or re-run with --force if a GTM engineer approved it.");
|
|
1782
1932
|
return false;
|
|
1783
1933
|
}
|
|
1784
|
-
const dirty =
|
|
1934
|
+
const dirty = spawnSync5("git", ["status", "--porcelain"], { cwd: root2, encoding: "utf8" });
|
|
1785
1935
|
if (dirty.status === 0 && dirty.stdout.trim() && !opts.force) {
|
|
1786
1936
|
console.error("Refusing to upgrade with uncommitted changes. Commit or stash first (or --force).");
|
|
1787
1937
|
return false;
|
|
@@ -1794,9 +1944,9 @@ function runUpgrade(root2, opts) {
|
|
|
1794
1944
|
if (gtm(root2, ["migrate"]) !== 0) return false;
|
|
1795
1945
|
const valid = gtm(root2, ["validate"]) === 0;
|
|
1796
1946
|
const after = pinnedVersion(root2) ?? target;
|
|
1797
|
-
const changelog =
|
|
1798
|
-
if (
|
|
1799
|
-
const notes = changelogBetween(
|
|
1947
|
+
const changelog = join16(root2, "node_modules", ...PACKAGE_NAME.split("/"), "CHANGELOG.md");
|
|
1948
|
+
if (existsSync14(changelog)) {
|
|
1949
|
+
const notes = changelogBetween(readFileSync15(changelog, "utf8"), before, after);
|
|
1800
1950
|
console.log(`
|
|
1801
1951
|
=== Changes ${before} \u2192 ${after} ===
|
|
1802
1952
|
${notes || "(no changelog entries)"}
|
|
@@ -1935,11 +2085,24 @@ var COMMANDS = {
|
|
|
1935
2085
|
"tam: builds the TAM for the campaign's segments into the database (needs an approved ICP and",
|
|
1936
2086
|
"config.yaml \u2192 tam.source; mock-accounts for a local proof) and prints the funnel, cost and run_id.",
|
|
1937
2087
|
"Validates first, sends the resolved config to the workers via Hatchet, and waits for the result.",
|
|
1938
|
-
"
|
|
2088
|
+
"Needs this project's stack: `npx gtm stack up` (settings in ~/.gtm/stacks/<project_id>.env).",
|
|
1939
2089
|
"Planned (exit 1, a stop per AGENTS.md): enrich, suppression:import, signals, qualify, qa, push, activate, results."
|
|
1940
2090
|
],
|
|
1941
2091
|
writes: "Rows in Postgres (runs, accounts, costs). No repo files: copy the printed run-log row into the run log.",
|
|
1942
2092
|
examples: ["npx gtm job tam q1-uk-cfo"]
|
|
2093
|
+
},
|
|
2094
|
+
stack: {
|
|
2095
|
+
usage: "gtm stack up\ngtm stack down [--wipe]\ngtm stack status",
|
|
2096
|
+
summary: "run this project's own stack (Postgres + Hatchet + worker) on Docker",
|
|
2097
|
+
details: [
|
|
2098
|
+
"One stack per project (compose project gtm-<project_id>, its own volumes and database). The worker image",
|
|
2099
|
+
"matches the core version this repo pins and applies database migrations when it starts. Needs Docker.",
|
|
2100
|
+
"Ports: GTM_PG_PORT (5432), GTM_HATCHET_UI_PORT (8888), GTM_HATCHET_GRPC_PORT (7077); set them to run two stacks at once.",
|
|
2101
|
+
"Provider API keys for the worker: ~/.gtm/stacks/<project_id>.secrets.env (never in the repo).",
|
|
2102
|
+
"down keeps the data; down --wipe deletes this project's database and Hatchet state (ask the human first)."
|
|
2103
|
+
],
|
|
2104
|
+
writes: "Docker containers and volumes, and ~/.gtm/stacks/<project_id>.env. No repo files.",
|
|
2105
|
+
examples: ["npx gtm stack up", "npx gtm stack status", "GTM_PG_PORT=5433 npx gtm stack up"]
|
|
1943
2106
|
}
|
|
1944
2107
|
};
|
|
1945
2108
|
function generalHelp(version) {
|
|
@@ -1997,6 +2160,7 @@ var { values, positionals } = parseArgs({
|
|
|
1997
2160
|
check: { type: "boolean" },
|
|
1998
2161
|
"dry-run": { type: "boolean" },
|
|
1999
2162
|
force: { type: "boolean" },
|
|
2163
|
+
wipe: { type: "boolean" },
|
|
2000
2164
|
help: { type: "boolean", short: "h" },
|
|
2001
2165
|
version: { type: "boolean", short: "v" }
|
|
2002
2166
|
}
|
|
@@ -2050,6 +2214,8 @@ async function run() {
|
|
|
2050
2214
|
return runUpgrade(root, { version: args[0], force: values.force });
|
|
2051
2215
|
case "job":
|
|
2052
2216
|
return runJob(root, args);
|
|
2217
|
+
case "stack":
|
|
2218
|
+
return runStack(root, args, { wipe: values.wipe });
|
|
2053
2219
|
case void 0:
|
|
2054
2220
|
return help(void 0);
|
|
2055
2221
|
case "help":
|
package/kit/AGENTS.md
CHANGED
|
@@ -26,8 +26,8 @@ Three layers, never mixed:
|
|
|
26
26
|
| Layer | Where | Holds | Never holds |
|
|
27
27
|
|---|---|---|---|
|
|
28
28
|
| **Brain** | git (this repo) | context, configs, copy, run logs, aggregate reports | PII, lead lists, exports, secrets |
|
|
29
|
-
| **Execution** |
|
|
30
|
-
| **Data** |
|
|
29
|
+
| **Execution** | this project's workers (gtm-core code, run on Hatchet) | TAM build, enrichment waterfall, verification, scoring, signals, sender push | business decisions |
|
|
30
|
+
| **Data** | this project's own Postgres (one database per project) | accounts, contacts, enrichments, signals, suppression, campaign leads, events, runs, costs | another project's data; rows without a `run_id` |
|
|
31
31
|
|
|
32
32
|
**The agent orchestrates; code does bulk work.** The agent picks the playbook, edits config, runs a
|
|
33
33
|
`npx gtm job` command, reads the aggregate output, QAs a small sample, and writes the run log. The
|
|
@@ -129,6 +129,7 @@ Rules, no exceptions:
|
|
|
129
129
|
- Bounce rate > 3% or spam complaints in results: recommend pausing; the human decides.
|
|
130
130
|
- Anything that would contact a real person outside a launch that passed all three gates.
|
|
131
131
|
- You are about to edit `AGENTS.md` or anything in `.gtm/`.
|
|
132
|
+
- You are about to run `npx gtm stack down --wipe`: it deletes this project's database. Only on the human's request.
|
|
132
133
|
|
|
133
134
|
## 5. Data rules
|
|
134
135
|
|
|
@@ -140,10 +141,13 @@ Rules, no exceptions:
|
|
|
140
141
|
3. QA sample review happens in the DB console or terminal output. Do not paste sampled rows into files
|
|
141
142
|
or PR descriptions; summarize ("19/20 correct persona").
|
|
142
143
|
4. **Secrets** live only in `.env` (git-ignored) or the secret manager. Never print, echo, commit or
|
|
143
|
-
paste keys. Provider API keys never live in a project repo: they are on
|
|
144
|
-
|
|
144
|
+
paste keys. Provider API keys never live in a project repo: they are on this project's workers (local stack:
|
|
145
|
+
`~/.gtm/stacks/<project_id>.secrets.env`, written by the human).
|
|
146
|
+
5. Every DB row carries `campaign_id`, `run_id` and timestamps (the database itself is the project scope).
|
|
147
|
+
Every job prints its `run_id`.
|
|
145
148
|
6. Every lead field records its source (`field_sources`) so GDPR "where did you get my data?" can be answered.
|
|
146
|
-
7. This project
|
|
149
|
+
7. This project's data is never mixed with another project's: it has its own database, bound to this
|
|
150
|
+
`project_id` on the first job, and jobs for another project are refused.
|
|
147
151
|
|
|
148
152
|
## 6. Team and concurrency rules
|
|
149
153
|
|
|
@@ -178,6 +182,8 @@ Run from the repo root. If you cannot run shell commands, give the exact command
|
|
|
178
182
|
| `npx gtm sync [--check]` | re-copy core kit into `.gtm/` and `AGENTS.md` | available |
|
|
179
183
|
| `npx gtm migrate [--dry-run]` | rewrite YAML files to the installed core's schema versions | available |
|
|
180
184
|
| `npx gtm upgrade [version]` | install a newer core, sync, migrate, validate, show changelog | available |
|
|
185
|
+
| `npx gtm stack up` / `npx gtm stack status` | start / check this project's own stack (Postgres + Hatchet + worker at this core version) on Docker; jobs need it | available |
|
|
186
|
+
| `npx gtm stack down [--wipe]` | stop the stack, data kept; `--wipe` deletes this project's database (only when the human asks) | available |
|
|
181
187
|
| `npx gtm job tam <campaign>` | build TAM into `accounts` (via the workers), print funnel counts, cost, run_id and a run-log row | available |
|
|
182
188
|
| `npx gtm job enrich <campaign>` | find people + email waterfall + verification, cost-capped | planned |
|
|
183
189
|
| `npx gtm job suppression:import <campaign> <file>` | load suppression lists (file stays outside git) | planned |
|
|
@@ -203,7 +209,8 @@ Every job prints a `run_id`, counts and cost. Copy those into the run log.
|
|
|
203
209
|
|
|
204
210
|
- **New project:** create an empty **private** repo (e.g. `<org>/gtm-project-<slug>`), clone it, and in
|
|
205
211
|
that folder run `npx @veleads/gtm init . --name "<Name>"` (or `init <slug>` to create a new folder).
|
|
206
|
-
Commit, push, then stage `00-intake` begins.
|
|
212
|
+
Commit, push, then stage `00-intake` begins. Before the first job (stage 2), start the project's stack
|
|
213
|
+
with `npx gtm stack up` (needs Docker).
|
|
207
214
|
- **New campaign:** from up-to-date `main`: `git switch -c campaign/<slug>`,
|
|
208
215
|
`npx gtm new-campaign <slug>`, fill `brief.md`, set `stage`, follow the routing table.
|
|
209
216
|
- **New SDR:** read this file and `PROJECT.md`, run `npx gtm status`, then section 2 with a GTM engineer watching.
|
|
@@ -214,7 +221,7 @@ Every job prints a `run_id`, counts and cost. Copy those into the run log.
|
|
|
214
221
|
The PR contains the new `.gtm/`, `AGENTS.md`, lockfile, and any YAML migrations. Read the changelog
|
|
215
222
|
in the PR body: entries marked **migration** or **action** need a human look.
|
|
216
223
|
- Upgrades are refused while a campaign is in `qa` or `launch`. Live campaigns (`results`) keep running
|
|
217
|
-
on
|
|
224
|
+
on this project's workers across upgrades; nothing changes for them mid-flight.
|
|
218
225
|
- New opt-in capabilities appear as `features` in `project.yaml`; they are off until you enable them.
|
|
219
226
|
- If `validate` says "run `npx gtm sync`" or "`migrate`", the repo and the installed core disagree:
|
|
220
227
|
run the command, review the diff, commit it on its own branch.
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"type": "object",
|
|
4
4
|
"properties": {
|
|
5
|
-
"project_id": {
|
|
6
|
-
"type": "string",
|
|
7
|
-
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
8
|
-
},
|
|
9
5
|
"tam_id": {
|
|
10
6
|
"type": "string",
|
|
11
7
|
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
@@ -90,7 +86,6 @@
|
|
|
90
86
|
}
|
|
91
87
|
},
|
|
92
88
|
"required": [
|
|
93
|
-
"project_id",
|
|
94
89
|
"tam_id",
|
|
95
90
|
"domain",
|
|
96
91
|
"name",
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"type": "object",
|
|
4
4
|
"properties": {
|
|
5
|
-
"project_id": {
|
|
6
|
-
"type": "string",
|
|
7
|
-
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
8
|
-
},
|
|
9
5
|
"account_domain": {
|
|
10
6
|
"type": "string",
|
|
11
7
|
"pattern": "^(?!-)[a-z0-9-]+(\\.[a-z0-9-]+)+$"
|
|
@@ -91,7 +87,6 @@
|
|
|
91
87
|
}
|
|
92
88
|
},
|
|
93
89
|
"required": [
|
|
94
|
-
"project_id",
|
|
95
90
|
"account_domain",
|
|
96
91
|
"first_name",
|
|
97
92
|
"last_name",
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"type": "object",
|
|
4
4
|
"properties": {
|
|
5
|
-
"project_id": {
|
|
6
|
-
"type": "string",
|
|
7
|
-
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
8
|
-
},
|
|
9
5
|
"campaign_id": {
|
|
10
6
|
"type": "string",
|
|
11
7
|
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
@@ -114,7 +110,6 @@
|
|
|
114
110
|
}
|
|
115
111
|
},
|
|
116
112
|
"required": [
|
|
117
|
-
"project_id",
|
|
118
113
|
"campaign_id",
|
|
119
114
|
"run_id",
|
|
120
115
|
"email",
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"type": "object",
|
|
4
4
|
"properties": {
|
|
5
|
-
"project_id": {
|
|
6
|
-
"type": "string",
|
|
7
|
-
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
8
|
-
},
|
|
9
5
|
"signal_key": {
|
|
10
6
|
"type": "string",
|
|
11
7
|
"pattern": "^[a-z0-9][a-z0-9-]*$"
|
|
@@ -53,7 +49,6 @@
|
|
|
53
49
|
}
|
|
54
50
|
},
|
|
55
51
|
"required": [
|
|
56
|
-
"project_id",
|
|
57
52
|
"signal_key",
|
|
58
53
|
"account_domain",
|
|
59
54
|
"contact_linkedin_url",
|
|
@@ -22,8 +22,9 @@ Know how big the market is, and whether the ICP filters are right, before spendi
|
|
|
22
22
|
4. Trigger the job: `npx gtm job tam <campaign>`. It validates, sends segment filters and exclusions
|
|
23
23
|
from `icp.yaml` to the workers, which page accounts through the adapter, upsert them into
|
|
24
24
|
`accounts` (re-runs update, never duplicate) and print counts per segment plus a run-log row.
|
|
25
|
-
**You do not page through accounts yourself.** If it says there is no Hatchet connection
|
|
26
|
-
|
|
25
|
+
**You do not page through accounts yourself.** If it says there is no Hatchet connection, run
|
|
26
|
+
`npx gtm stack up` once (needs Docker). If that fails, or the job keeps waiting for a worker
|
|
27
|
+
(`npx gtm stack status`), stop and tell the human (AGENTS.md section 4).
|
|
27
28
|
5. Funnel counts per segment (printed by the job):
|
|
28
29
|
- **TAM**: accounts matching filters.
|
|
29
30
|
- **Reachable**: accounts where at least one target persona can be found (from the job's
|
|
@@ -36,7 +37,7 @@ Know how big the market is, and whether the ICP filters are right, before spendi
|
|
|
36
37
|
8. Write `tam/<tam_id>.md` from `.gtm/templates/outputs/tam-summary.md` and the run log.
|
|
37
38
|
|
|
38
39
|
## Outputs
|
|
39
|
-
- Rows in `accounts` (Postgres), tagged `
|
|
40
|
+
- Rows in `accounts` (this project's Postgres), tagged `tam_id`, `segment_id`, `run_id`.
|
|
40
41
|
- `tam/<tam_id>.md` with funnel table and sanity check. Run log.
|
|
41
42
|
|
|
42
43
|
## Checklist
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# One project's stack: Postgres (this project's data + Hatchet's state), hatchet-lite (job engine) and
|
|
2
|
+
# the gtm worker at the core version this project repo pins. Driven by `npx gtm stack up|down|status`,
|
|
3
|
+
# which sets the compose project name (gtm-<project_id>), the ports and the worker image.
|
|
4
|
+
# Data stays in this project's Docker volumes; nothing here is written into the repo.
|
|
5
|
+
services:
|
|
6
|
+
postgres:
|
|
7
|
+
image: postgres:16.10
|
|
8
|
+
command: postgres -c max_connections=200
|
|
9
|
+
environment:
|
|
10
|
+
POSTGRES_USER: gtm
|
|
11
|
+
POSTGRES_PASSWORD: gtm
|
|
12
|
+
POSTGRES_DB: gtm
|
|
13
|
+
# Local only: reachable from this machine (QA samples in a DB console), not from the network.
|
|
14
|
+
ports: ["127.0.0.1:${GTM_PG_PORT:-5432}:5432"]
|
|
15
|
+
volumes:
|
|
16
|
+
- pgdata:/var/lib/postgresql/data
|
|
17
|
+
- ./init:/docker-entrypoint-initdb.d:ro
|
|
18
|
+
healthcheck:
|
|
19
|
+
test: ["CMD-SHELL", "pg_isready -U gtm -d gtm"]
|
|
20
|
+
interval: 2s
|
|
21
|
+
timeout: 5s
|
|
22
|
+
retries: 30
|
|
23
|
+
|
|
24
|
+
hatchet-lite:
|
|
25
|
+
image: ghcr.io/hatchet-dev/hatchet/hatchet-lite:v0.110.5
|
|
26
|
+
depends_on:
|
|
27
|
+
postgres:
|
|
28
|
+
condition: service_healthy
|
|
29
|
+
ports:
|
|
30
|
+
- "127.0.0.1:${GTM_HATCHET_UI_PORT:-8888}:8888"
|
|
31
|
+
- "127.0.0.1:${GTM_HATCHET_GRPC_PORT:-7077}:7077"
|
|
32
|
+
environment:
|
|
33
|
+
DATABASE_URL: postgresql://gtm:gtm@postgres:5432/hatchet?sslmode=disable
|
|
34
|
+
SERVER_AUTH_COOKIE_DOMAIN: localhost
|
|
35
|
+
SERVER_AUTH_COOKIE_INSECURE: "t"
|
|
36
|
+
SERVER_GRPC_BIND_ADDRESS: 0.0.0.0
|
|
37
|
+
SERVER_GRPC_INSECURE: "t"
|
|
38
|
+
SERVER_GRPC_BROADCAST_ADDRESS: localhost:${GTM_HATCHET_GRPC_PORT:-7077}
|
|
39
|
+
SERVER_GRPC_PORT: "7077"
|
|
40
|
+
SERVER_URL: http://localhost:${GTM_HATCHET_UI_PORT:-8888}
|
|
41
|
+
SERVER_AUTH_SET_EMAIL_VERIFIED: "t"
|
|
42
|
+
SERVER_INTERNAL_CLIENT_INTERNAL_GRPC_BROADCAST_ADDRESS: localhost:7077
|
|
43
|
+
volumes:
|
|
44
|
+
- hatchet_config:/config
|
|
45
|
+
|
|
46
|
+
worker:
|
|
47
|
+
image: ${GTM_WORKER_IMAGE:-ghcr.io/veleads/gtm-worker:latest}
|
|
48
|
+
depends_on:
|
|
49
|
+
postgres:
|
|
50
|
+
condition: service_healthy
|
|
51
|
+
hatchet-lite:
|
|
52
|
+
condition: service_started
|
|
53
|
+
restart: unless-stopped
|
|
54
|
+
environment:
|
|
55
|
+
DATABASE_URL: postgresql://gtm:gtm@postgres:5432/gtm
|
|
56
|
+
HATCHET_CLIENT_TOKEN: ${HATCHET_CLIENT_TOKEN:-}
|
|
57
|
+
HATCHET_CLIENT_HOST_PORT: hatchet-lite:7077
|
|
58
|
+
HATCHET_CLIENT_TLS_STRATEGY: none
|
|
59
|
+
# Provider API keys: ~/.gtm/stacks/<project_id>.secrets.env, outside the repo. Optional.
|
|
60
|
+
env_file:
|
|
61
|
+
- path: ${GTM_SECRETS_FILE:-/dev/null}
|
|
62
|
+
required: false
|
|
63
|
+
|
|
64
|
+
volumes:
|
|
65
|
+
pgdata:
|
|
66
|
+
hatchet_config:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# Copy to .env (never commit). Provider API keys do NOT belong here: they
|
|
1
|
+
# Copy to .env (never commit). Provider API keys do NOT belong here: they go to this project's worker
|
|
2
|
+
# (local stack: ~/.gtm/stacks/<project_id>.secrets.env). `npx gtm stack up` writes the connection
|
|
3
|
+
# settings to ~/.gtm/stacks/<project_id>.env; set HATCHET_CLIENT_TOKEN here only for a hosted stack.
|
|
2
4
|
GTM_USER=jdoe
|
|
3
|
-
# Phase 2: trigger jobs on the shared Hatchet, read-only DB access for QA samples
|
|
4
5
|
HATCHET_CLIENT_TOKEN=
|
|
5
|
-
DATABASE_URL_READONLY=
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veleads/gtm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "gtm-brain core: process kit (AGENTS.md, skills, templates, schemas) and the gtm CLI for client repos",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gtm",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsup": "^8.5.0",
|
|
42
|
-
"@gtm/
|
|
43
|
-
"@gtm/
|
|
42
|
+
"@gtm/core": "0.0.0",
|
|
43
|
+
"@gtm/schemas": "0.0.0"
|
|
44
44
|
},
|
|
45
45
|
"homepage": "https://github.com/Veleads/gtm-core#readme",
|
|
46
46
|
"bugs": {
|