anymorph 0.2.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +46 -2
  2. package/dist/index.js +598 -26
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -27,6 +27,7 @@ anymorph check yourdomain.com
27
27
  anymorph workspaces
28
28
 
29
29
  # Prepare and validate local GEO strategy artifacts
30
+ anymorph geo init --repo .
30
31
  anymorph geo prepare yourdomain.com
31
32
  anymorph geo validate 20260517T074200Z --repo .
32
33
  ```
@@ -122,12 +123,55 @@ repos under `~/.anymorph/repos/{domain}`. Use `--repo` to target an explicit
122
123
  local checkout.
123
124
 
124
125
  ```bash
126
+ anymorph geo init --repo ~/works/yourdomain.com
125
127
  anymorph geo prepare yourdomain.com
126
128
  anymorph geo prepare yourdomain.com --repo ~/works/yourdomain.com
127
129
  ```
128
130
 
129
- This initializes `agent/runs/{runId}/manifest.json`, `context.json`, and
130
- `status.json`. The agent fills the content artifacts separately.
131
+ `prepare` syncs the GEO scaffold first unless `--skip-scaffold` is passed. It
132
+ then initializes `agent/runs/{runId}/manifest.json`, `context.json`,
133
+ `intents.json`, `crawl_logs.json`, and `status.json`. The agent fills the
134
+ content artifacts separately.
135
+
136
+ ### `anymorph geo init`
137
+
138
+ Initialize a local tenant repo for GEO work.
139
+
140
+ ```bash
141
+ anymorph geo init --repo .
142
+ anymorph geo init --repo . --skills-source /path/to/anymorph-geo-skills/plugins/anymorph-geo/skills
143
+ ```
144
+
145
+ This installs managed skills under `.claude/skills` and `.agents/skills`, writes
146
+ `agent/contracts`, creates `agent/runs` and `agent/archive`, and creates missing
147
+ memory files without overwriting existing `agent/BRAND.md`, `agent/STRATEGY.md`,
148
+ or `agent/LEARNINGS.md`.
149
+
150
+ ### `anymorph geo sync`
151
+
152
+ Sync managed GEO files without touching memory or run artifacts.
153
+
154
+ ```bash
155
+ anymorph geo sync --repo .
156
+ ```
157
+
158
+ ### `anymorph geo doctor`
159
+
160
+ Check whether the local scaffold still matches the installed skillpack.
161
+
162
+ ```bash
163
+ anymorph geo doctor --repo .
164
+ anymorph geo doctor --repo . --json
165
+ ```
166
+
167
+ ### `anymorph geo intents <runId>`
168
+
169
+ Show the pre-fetched intent list for a local GEO strategy run.
170
+
171
+ ```bash
172
+ anymorph geo intents 20260517T074200Z --repo .
173
+ anymorph geo intents 20260517T074200Z --repo . --json
174
+ ```
131
175
 
132
176
  ### `anymorph geo validate <runId>`
133
177
 
package/dist/index.js CHANGED
@@ -5430,14 +5430,14 @@ var baseOpen = async (options) => {
5430
5430
  }
5431
5431
  const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
5432
5432
  if (options.wait) {
5433
- return new Promise((resolve2, reject) => {
5433
+ return new Promise((resolve3, reject) => {
5434
5434
  subprocess.once("error", reject);
5435
5435
  subprocess.once("close", (exitCode) => {
5436
5436
  if (!options.allowNonzeroExitCode && exitCode > 0) {
5437
5437
  reject(new Error(`Exited with code ${exitCode}`));
5438
5438
  return;
5439
5439
  }
5440
- resolve2(subprocess);
5440
+ resolve3(subprocess);
5441
5441
  });
5442
5442
  });
5443
5443
  }
@@ -7051,7 +7051,7 @@ async function loginCommand() {
7051
7051
  process.exit(1);
7052
7052
  }
7053
7053
  function sleep(ms) {
7054
- return new Promise((resolve2) => setTimeout(resolve2, ms));
7054
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
7055
7055
  }
7056
7056
 
7057
7057
  // src/lib/display.ts
@@ -7181,7 +7181,7 @@ async function checkCommand(domain, opts) {
7181
7181
  process.exit(1);
7182
7182
  }
7183
7183
  function sleep2(ms) {
7184
- return new Promise((resolve2) => setTimeout(resolve2, ms));
7184
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
7185
7185
  }
7186
7186
 
7187
7187
  // src/commands/status.ts
@@ -7246,6 +7246,10 @@ async function workspacesCommand(opts) {
7246
7246
  console.log();
7247
7247
  }
7248
7248
 
7249
+ // src/commands/geo.ts
7250
+ import { readFile as readFile3 } from "node:fs/promises";
7251
+ import { join as join6 } from "node:path";
7252
+
7249
7253
  // src/geo/package-writer.ts
7250
7254
  import { mkdir, writeFile } from "node:fs/promises";
7251
7255
  import { join as join2 } from "node:path";
@@ -7261,16 +7265,57 @@ async function writeGeoRunPackage(input) {
7261
7265
  branch: input.package.branch,
7262
7266
  execution: input.package.execution,
7263
7267
  mode: input.package.mode,
7264
- signals: input.package.signals,
7268
+ signals: buildContextSignals(input.package.signals),
7265
7269
  systemContext: input.package.systemContext,
7266
- schemaCatalog: input.package.schemaCatalog
7270
+ schemaCatalog: input.package.schemaCatalog,
7271
+ intentsPath: `agent/runs/${input.package.runId}/intents.json`
7267
7272
  });
7273
+ await writeJson(join2(runDir, "intents.json"), buildIntentList(input.package));
7274
+ await writeJson(
7275
+ join2(runDir, "crawl_logs.json"),
7276
+ buildCrawlLogAggregate(input.package)
7277
+ );
7268
7278
  await writeJson(join2(runDir, "status.json"), {
7269
7279
  status: "pending",
7270
7280
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
7271
7281
  });
7272
7282
  return { runDir };
7273
7283
  }
7284
+ function buildIntentList(pkg) {
7285
+ return {
7286
+ runId: pkg.runId,
7287
+ workspaceId: pkg.workspaceId,
7288
+ workspaceDomain: pkg.workspaceDomain,
7289
+ source: "signals.intents",
7290
+ intents: extractIntents(pkg.signals)
7291
+ };
7292
+ }
7293
+ function buildCrawlLogAggregate(pkg) {
7294
+ return {
7295
+ runId: pkg.runId,
7296
+ workspaceId: pkg.workspaceId,
7297
+ workspaceDomain: pkg.workspaceDomain,
7298
+ source: "signals.crawlLogs",
7299
+ crawlLogs: extractCrawlLogs(pkg.signals)
7300
+ };
7301
+ }
7302
+ function extractIntents(signals2) {
7303
+ if (!signals2 || typeof signals2 !== "object") return [];
7304
+ const intents = signals2.intents;
7305
+ return Array.isArray(intents) ? intents : [];
7306
+ }
7307
+ function extractCrawlLogs(signals2) {
7308
+ if (!signals2 || typeof signals2 !== "object") return null;
7309
+ return signals2.crawlLogs ?? null;
7310
+ }
7311
+ function buildContextSignals(signals2) {
7312
+ if (!signals2 || typeof signals2 !== "object" || Array.isArray(signals2))
7313
+ return signals2;
7314
+ const record = signals2;
7315
+ const { crawlLogs: _crawlLogs, ...rest } = record;
7316
+ const { crawlLogsSummary: _crawlLogsSummary, ...contextSignals } = rest;
7317
+ return contextSignals;
7318
+ }
7274
7319
  async function writeJson(path2, value) {
7275
7320
  await writeFile(path2, `${JSON.stringify(value, null, 2)}
7276
7321
  `);
@@ -7361,22 +7406,317 @@ async function gitOutput(repoPath, args) {
7361
7406
  }
7362
7407
  }
7363
7408
 
7409
+ // src/geo/scaffold.ts
7410
+ import { execFile as execFile7 } from "node:child_process";
7411
+ import {
7412
+ cp,
7413
+ mkdir as mkdir3,
7414
+ readdir as readdir2,
7415
+ readFile,
7416
+ rm,
7417
+ stat as stat2,
7418
+ writeFile as writeFile2
7419
+ } from "node:fs/promises";
7420
+ import { createHash } from "node:crypto";
7421
+ import { dirname, join as join4, relative, resolve as resolve2 } from "node:path";
7422
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
7423
+ import { promisify as promisify7 } from "node:util";
7424
+ var execFileAsync6 = promisify7(execFile7);
7425
+ var DEFAULT_SKILLPACK_RELATIVE_PATH = join4(
7426
+ "apps",
7427
+ "anymorph-geo-skills",
7428
+ "plugins",
7429
+ "anymorph-geo",
7430
+ "skills"
7431
+ );
7432
+ var MANAGED_PATHS = [".claude/skills", ".agents/skills", "agent/contracts"];
7433
+ async function initGeoScaffold(input) {
7434
+ return applyGeoScaffold(input, { createMemory: true });
7435
+ }
7436
+ async function syncGeoScaffold(input) {
7437
+ return applyGeoScaffold(input, { createMemory: false });
7438
+ }
7439
+ async function doctorGeoScaffold(input) {
7440
+ const repoPath = resolve2(input.repoPath);
7441
+ const skillsSourceDir = await resolveSkillSourceDir(input.skillsSourceDir);
7442
+ const manifestHash = await hashDirectory(skillsSourceDir);
7443
+ const problems = [];
7444
+ for (const target of [".claude/skills", ".agents/skills"]) {
7445
+ await compareDirectories(skillsSourceDir, join4(repoPath, target), target, problems);
7446
+ }
7447
+ for (const path2 of [
7448
+ "agent/BRAND.md",
7449
+ "agent/STRATEGY.md",
7450
+ "agent/LEARNINGS.md",
7451
+ "agent/runs/.gitkeep",
7452
+ "agent/archive/.gitkeep",
7453
+ "agent/skillpack.json"
7454
+ ]) {
7455
+ if (!await exists(join4(repoPath, path2))) problems.push(`Missing ${path2}`);
7456
+ }
7457
+ const lock = await readJson(join4(repoPath, "agent", "skillpack.json"));
7458
+ if (lock && typeof lock === "object") {
7459
+ const actual = lock.manifestHash;
7460
+ if (actual !== manifestHash) {
7461
+ problems.push("agent/skillpack.json manifestHash does not match current skillpack");
7462
+ }
7463
+ }
7464
+ return {
7465
+ ok: problems.length === 0,
7466
+ repoPath,
7467
+ skillsSourceDir,
7468
+ changed: [],
7469
+ problems,
7470
+ manifestHash
7471
+ };
7472
+ }
7473
+ async function resolveSkillSourceDir(explicit) {
7474
+ if (explicit) {
7475
+ const resolved = resolve2(explicit);
7476
+ await ensureDirectory2(resolved);
7477
+ return resolved;
7478
+ }
7479
+ for (const root of searchRoots()) {
7480
+ const candidate = join4(root, DEFAULT_SKILLPACK_RELATIVE_PATH);
7481
+ if (await isDirectory(candidate)) return candidate;
7482
+ }
7483
+ throw new Error(
7484
+ `Could not find GEO skillpack. Pass --skills-source <path> pointing to ${DEFAULT_SKILLPACK_RELATIVE_PATH}.`
7485
+ );
7486
+ }
7487
+ async function applyGeoScaffold(input, options) {
7488
+ const repoPath = resolve2(input.repoPath);
7489
+ const skillsSourceDir = await resolveSkillSourceDir(input.skillsSourceDir);
7490
+ await ensureDirectory2(repoPath);
7491
+ const changed = [];
7492
+ const problems = [];
7493
+ const manifestHash = await hashDirectory(skillsSourceDir);
7494
+ await syncManagedDirectory(skillsSourceDir, join4(repoPath, ".claude", "skills"), changed);
7495
+ await syncManagedDirectory(skillsSourceDir, join4(repoPath, ".agents", "skills"), changed);
7496
+ await syncContracts(repoPath, changed);
7497
+ await ensureRunDirs(repoPath, changed);
7498
+ if (options.createMemory) await ensureMemoryFiles(repoPath, changed);
7499
+ await writeSkillpackLock(
7500
+ {
7501
+ repoPath,
7502
+ skillsSourceDir,
7503
+ ref: input.ref ?? "local",
7504
+ manifestHash
7505
+ },
7506
+ changed
7507
+ );
7508
+ return {
7509
+ ok: problems.length === 0,
7510
+ repoPath,
7511
+ skillsSourceDir,
7512
+ changed,
7513
+ problems,
7514
+ manifestHash
7515
+ };
7516
+ }
7517
+ async function syncManagedDirectory(sourceDir, targetDir, changed) {
7518
+ await rm(targetDir, { recursive: true, force: true });
7519
+ await mkdir3(dirname(targetDir), { recursive: true });
7520
+ await cp(sourceDir, targetDir, { recursive: true });
7521
+ changed.push(relative(process.cwd(), targetDir) || targetDir);
7522
+ }
7523
+ async function syncContracts(repoPath, changed) {
7524
+ const contractsDir = join4(repoPath, "agent", "contracts");
7525
+ await mkdir3(contractsDir, { recursive: true });
7526
+ await writeIfChanged(
7527
+ join4(contractsDir, "actions.schema.json"),
7528
+ `${JSON.stringify(ACTIONS_SCHEMA, null, 2)}
7529
+ `,
7530
+ changed
7531
+ );
7532
+ await writeIfChanged(
7533
+ join4(contractsDir, "memory-item.schema.json"),
7534
+ `${JSON.stringify(MEMORY_ITEM_SCHEMA, null, 2)}
7535
+ `,
7536
+ changed
7537
+ );
7538
+ }
7539
+ async function ensureRunDirs(repoPath, changed) {
7540
+ for (const path2 of [join4(repoPath, "agent", "runs"), join4(repoPath, "agent", "archive")]) {
7541
+ await mkdir3(path2, { recursive: true });
7542
+ await writeIfMissing(join4(path2, ".gitkeep"), "", changed);
7543
+ }
7544
+ }
7545
+ async function ensureMemoryFiles(repoPath, changed) {
7546
+ await mkdir3(join4(repoPath, "agent"), { recursive: true });
7547
+ await writeIfMissing(
7548
+ join4(repoPath, "agent", "BRAND.md"),
7549
+ "# Brand\n\nInitial local scaffold. Replace with workspace brand context before serious runs.\n",
7550
+ changed
7551
+ );
7552
+ await writeIfMissing(
7553
+ join4(repoPath, "agent", "STRATEGY.md"),
7554
+ "# GEO Strategy\n\n## Current Priorities\n\n- Initial local scaffold.\n",
7555
+ changed
7556
+ );
7557
+ await writeIfMissing(
7558
+ join4(repoPath, "agent", "LEARNINGS.md"),
7559
+ "# Learnings\n\n## Stable Learnings\n\n- Initial local scaffold.\n",
7560
+ changed
7561
+ );
7562
+ }
7563
+ async function writeSkillpackLock(input, changed) {
7564
+ const sourceCommit = await gitOutput2(input.skillsSourceDir, ["rev-parse", "HEAD"]);
7565
+ const lock = {
7566
+ name: "anymorph-geo",
7567
+ source: input.skillsSourceDir,
7568
+ sourceRepo: "opactor-dev/anymorph-geo-skills",
7569
+ requestedRef: input.ref,
7570
+ resolvedCommit: sourceCommit,
7571
+ manifestHash: input.manifestHash,
7572
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
7573
+ managedPaths: MANAGED_PATHS
7574
+ };
7575
+ await writeIfChanged(
7576
+ join4(input.repoPath, "agent", "skillpack.json"),
7577
+ `${JSON.stringify(lock, null, 2)}
7578
+ `,
7579
+ changed
7580
+ );
7581
+ }
7582
+ async function compareDirectories(sourceDir, targetDir, displayPrefix, problems) {
7583
+ const sourceFiles = await listFiles(sourceDir);
7584
+ const targetFiles = await listFiles(targetDir);
7585
+ const targetSet = new Set(targetFiles);
7586
+ for (const file of sourceFiles) {
7587
+ const source = await readFile(join4(sourceDir, file), "utf8");
7588
+ const targetPath = join4(targetDir, file);
7589
+ if (!await exists(targetPath)) {
7590
+ problems.push(`Missing ${displayPrefix}/${file}`);
7591
+ continue;
7592
+ }
7593
+ const target = await readFile(targetPath, "utf8");
7594
+ if (source !== target) problems.push(`Drifted ${displayPrefix}/${file}`);
7595
+ targetSet.delete(file);
7596
+ }
7597
+ for (const extra of targetSet) problems.push(`Extra managed file ${displayPrefix}/${extra}`);
7598
+ }
7599
+ async function hashDirectory(dir) {
7600
+ const files = await listFiles(dir);
7601
+ const hash = createHash("sha256");
7602
+ for (const file of files) {
7603
+ hash.update(file);
7604
+ hash.update("\0");
7605
+ hash.update(await readFile(join4(dir, file)));
7606
+ hash.update("\0");
7607
+ }
7608
+ return `sha256:${hash.digest("hex")}`;
7609
+ }
7610
+ async function listFiles(dir, base = "") {
7611
+ let entries;
7612
+ try {
7613
+ entries = await readdir2(join4(dir, base), { withFileTypes: true });
7614
+ } catch {
7615
+ return [];
7616
+ }
7617
+ const files = [];
7618
+ for (const entry of entries) {
7619
+ const name = String(entry.name);
7620
+ const path2 = base ? `${base}/${name}` : name;
7621
+ if (entry.isDirectory()) files.push(...await listFiles(dir, path2));
7622
+ else if (entry.isFile()) files.push(path2);
7623
+ }
7624
+ return files.sort();
7625
+ }
7626
+ async function writeIfMissing(path2, content, changed) {
7627
+ if (await exists(path2)) return;
7628
+ await mkdir3(dirname(path2), { recursive: true });
7629
+ await writeFile2(path2, content);
7630
+ changed.push(relative(process.cwd(), path2) || path2);
7631
+ }
7632
+ async function writeIfChanged(path2, content, changed) {
7633
+ const current = await readFile(path2, "utf8").catch(() => null);
7634
+ if (current === content) return;
7635
+ await mkdir3(dirname(path2), { recursive: true });
7636
+ await writeFile2(path2, content);
7637
+ changed.push(relative(process.cwd(), path2) || path2);
7638
+ }
7639
+ async function readJson(path2) {
7640
+ const raw = await readFile(path2, "utf8").catch(() => null);
7641
+ if (!raw) return null;
7642
+ try {
7643
+ return JSON.parse(raw);
7644
+ } catch {
7645
+ return null;
7646
+ }
7647
+ }
7648
+ async function gitOutput2(cwd, args) {
7649
+ try {
7650
+ const { stdout } = await execFileAsync6("git", args, { cwd });
7651
+ return stdout.trim() || null;
7652
+ } catch {
7653
+ return null;
7654
+ }
7655
+ }
7656
+ function searchRoots() {
7657
+ const roots = /* @__PURE__ */ new Set();
7658
+ let cwd = resolve2(process.cwd());
7659
+ for (; ; ) {
7660
+ roots.add(cwd);
7661
+ const parent = dirname(cwd);
7662
+ if (parent === cwd) break;
7663
+ cwd = parent;
7664
+ }
7665
+ let here = dirname(fileURLToPath2(import.meta.url));
7666
+ for (; ; ) {
7667
+ roots.add(here);
7668
+ const parent = dirname(here);
7669
+ if (parent === here) break;
7670
+ here = parent;
7671
+ }
7672
+ return [...roots];
7673
+ }
7674
+ async function ensureDirectory2(path2) {
7675
+ const s = await stat2(path2);
7676
+ if (!s.isDirectory()) throw new Error(`${path2} is not a directory`);
7677
+ }
7678
+ async function isDirectory(path2) {
7679
+ return stat2(path2).then((s) => s.isDirectory()).catch(() => false);
7680
+ }
7681
+ async function exists(path2) {
7682
+ return stat2(path2).then(() => true).catch(() => false);
7683
+ }
7684
+ var ACTIONS_SCHEMA = {
7685
+ $schema: "https://json-schema.org/draft/2020-12/schema",
7686
+ title: "Anymorph GEO actions artifact",
7687
+ type: "object",
7688
+ required: ["runId", "actions"],
7689
+ properties: {
7690
+ runId: { type: "string" },
7691
+ actions: { type: "array", items: { type: "object" } },
7692
+ artifactPaths: { type: "array", items: { type: "string" } }
7693
+ }
7694
+ };
7695
+ var MEMORY_ITEM_SCHEMA = {
7696
+ $schema: "https://json-schema.org/draft/2020-12/schema",
7697
+ title: "Anymorph GEO memory item",
7698
+ type: "object",
7699
+ required: ["summary"],
7700
+ properties: {
7701
+ summary: { type: "string" },
7702
+ evidence: { type: "array", items: { type: "string" } }
7703
+ }
7704
+ };
7705
+
7364
7706
  // src/geo/validate.ts
7365
- import { access, readFile } from "node:fs/promises";
7366
- import { join as join4 } from "node:path";
7707
+ import { access, readFile as readFile2 } from "node:fs/promises";
7708
+ import { join as join5 } from "node:path";
7367
7709
  async function validateGeoRunArtifacts(input) {
7368
- const runDir = join4(input.repoPath, "agent", "runs", input.runId);
7710
+ const runDir = join5(input.repoPath, "agent", "runs", input.runId);
7369
7711
  const errors = [];
7370
7712
  const warnings = [];
7371
- const manifest = await readJson(join4(runDir, "manifest.json"), errors);
7372
- const actions = await readJson(join4(runDir, "actions.json"), errors);
7373
- const status = await readJson(join4(runDir, "status.json"), errors);
7374
- const skillUsage = await readJson(join4(runDir, "skill-usage.json"), errors);
7375
- await requireFile(join4(runDir, "rationale.md"), errors);
7713
+ const manifest = await readJson2(join5(runDir, "manifest.json"), errors);
7714
+ const actions = await readJson2(join5(runDir, "actions.json"), errors);
7715
+ const status = await readJson2(join5(runDir, "status.json"), errors);
7716
+ await requireFile(join5(runDir, "rationale.md"), errors);
7376
7717
  if (manifest) validateManifest(manifest, input.runId, errors);
7377
7718
  if (actions) validateActions(actions, input.runId, errors);
7378
7719
  if (status) validateStatus(status, errors);
7379
- if (skillUsage) validateSkillUsage(skillUsage, errors);
7380
7720
  return {
7381
7721
  ok: errors.length === 0,
7382
7722
  errors,
@@ -7391,9 +7731,9 @@ async function requireFile(path2, errors) {
7391
7731
  errors.push(`Missing ${shortPath(path2)}`);
7392
7732
  }
7393
7733
  }
7394
- async function readJson(path2, errors) {
7734
+ async function readJson2(path2, errors) {
7395
7735
  try {
7396
- const raw = await readFile(path2, "utf8");
7736
+ const raw = await readFile2(path2, "utf8");
7397
7737
  return JSON.parse(raw);
7398
7738
  } catch (err) {
7399
7739
  const message = err instanceof SyntaxError ? "Invalid JSON" : "Missing";
@@ -7436,6 +7776,7 @@ function validateActions(value, runId, errors) {
7436
7776
  }
7437
7777
  if (value.runId !== runId) errors.push(`actions.json runId must be ${runId}`);
7438
7778
  if (!Array.isArray(value.actions)) errors.push("actions.json actions must be an array");
7779
+ if (Array.isArray(value.actions)) validateActionItems(value.actions, errors);
7439
7780
  if (!Array.isArray(value.artifactPaths)) {
7440
7781
  errors.push("actions.json artifactPaths must be an array");
7441
7782
  return;
@@ -7450,19 +7791,100 @@ function validateActions(value, runId, errors) {
7450
7791
  }
7451
7792
  }
7452
7793
  }
7453
- function validateStatus(value, errors) {
7454
- if (!isRecord(value)) {
7455
- errors.push("status.json must be an object");
7794
+ function validateActionItems(actions, errors) {
7795
+ if (actions.length > 20) errors.push("actions.json actions must contain at most 20 items");
7796
+ actions.forEach((action, index) => validateActionItem(action, index, errors));
7797
+ }
7798
+ function validateActionItem(action, index, errors) {
7799
+ const prefix = `actions.json actions[${index}]`;
7800
+ if (!isRecord(action)) {
7801
+ errors.push(`${prefix} must be an object`);
7456
7802
  return;
7457
7803
  }
7458
- if (value.status !== "proposed") errors.push('status.json status must be "proposed"');
7804
+ requireString(action, "id", prefix, errors);
7805
+ requireEnum(action, "operation", ["create", "update"], prefix, errors);
7806
+ requireEnum(action, "surface", ["on_page", "off_page"], prefix, errors);
7807
+ requireEnum(action, "assetType", ["brand_owned", "geo_page", "third_party"], prefix, errors);
7808
+ validateIntentRef(action, prefix, errors);
7809
+ validateTarget(action, prefix, errors);
7810
+ requireString(action, "objective", prefix, errors);
7811
+ requireString(action, "changeBrief", prefix, errors);
7812
+ requireString(action, "reason", prefix, errors);
7813
+ requireString(action, "expectedOutcome", prefix, errors);
7814
+ validateEvidence(action.evidence, prefix, errors);
7815
+ requireEnum(action, "priority", ["high", "medium", "low"], prefix, errors);
7816
+ requireEnum(action, "confidence", ["high", "medium", "low"], prefix, errors);
7817
+ }
7818
+ function validateIntentRef(action, prefix, errors) {
7819
+ if (action.intentId !== null && typeof action.intentId !== "string") {
7820
+ errors.push(`${prefix}.intentId must be a string or null`);
7821
+ }
7822
+ if (action.intentId === null) {
7823
+ if (!isRecord(action.proposedIntent)) {
7824
+ errors.push(`${prefix}.proposedIntent is required when intentId is null`);
7825
+ return;
7826
+ }
7827
+ requireString(action.proposedIntent, "name", `${prefix}.proposedIntent`, errors);
7828
+ requireString(action.proposedIntent, "reason", `${prefix}.proposedIntent`, errors);
7829
+ }
7830
+ }
7831
+ function validateTarget(action, prefix, errors) {
7832
+ const target = action.target;
7833
+ if (!isRecord(target)) {
7834
+ errors.push(`${prefix}.target must be an object`);
7835
+ return;
7836
+ }
7837
+ if (action.operation === "create") {
7838
+ if (typeof target.url === "string" && target.url.length > 0) {
7839
+ errors.push(`${prefix}.target.url must be omitted for create actions`);
7840
+ }
7841
+ if (typeof target.pageId === "string" && target.pageId.length > 0) {
7842
+ errors.push(`${prefix}.target.pageId must be omitted for create actions`);
7843
+ }
7844
+ if (!Array.isArray(target.queryCluster) && typeof target.description !== "string") {
7845
+ errors.push(`${prefix}.target needs queryCluster or description for create actions`);
7846
+ }
7847
+ }
7848
+ if (action.operation === "update") {
7849
+ const hasUrl = typeof target.url === "string" && target.url.length > 0;
7850
+ const hasPageId = typeof target.pageId === "string" && target.pageId.length > 0;
7851
+ if (!hasUrl && !hasPageId) {
7852
+ errors.push(`${prefix}.target needs pageId or url for update actions`);
7853
+ }
7854
+ }
7855
+ }
7856
+ function validateEvidence(value, prefix, errors) {
7857
+ if (!Array.isArray(value)) {
7858
+ errors.push(`${prefix}.evidence must be an array`);
7859
+ return;
7860
+ }
7861
+ value.forEach((item, index) => {
7862
+ const evidencePrefix = `${prefix}.evidence[${index}]`;
7863
+ if (!isRecord(item)) {
7864
+ errors.push(`${evidencePrefix} must be an object`);
7865
+ return;
7866
+ }
7867
+ requireEnum(item, "type", ["signal", "workspace", "source", "research"], evidencePrefix, errors);
7868
+ requireString(item, "ref", evidencePrefix, errors);
7869
+ requireString(item, "summary", evidencePrefix, errors);
7870
+ });
7871
+ }
7872
+ function requireString(record, key, prefix, errors) {
7873
+ if (typeof record[key] !== "string" || record[key].length === 0) {
7874
+ errors.push(`${prefix}.${key} must be a non-empty string`);
7875
+ }
7459
7876
  }
7460
- function validateSkillUsage(value, errors) {
7877
+ function requireEnum(record, key, allowed, prefix, errors) {
7878
+ if (typeof record[key] !== "string" || !allowed.includes(record[key])) {
7879
+ errors.push(`${prefix}.${key} must be one of ${allowed.join(", ")}`);
7880
+ }
7881
+ }
7882
+ function validateStatus(value, errors) {
7461
7883
  if (!isRecord(value)) {
7462
- errors.push("skill-usage.json must be an object");
7884
+ errors.push("status.json must be an object");
7463
7885
  return;
7464
7886
  }
7465
- if (!Array.isArray(value.skills)) errors.push("skill-usage.json skills must be an array");
7887
+ if (value.status !== "proposed") errors.push('status.json status must be "proposed"');
7466
7888
  }
7467
7889
  function isRecord(value) {
7468
7890
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -7475,12 +7897,62 @@ function shortPath(path2) {
7475
7897
 
7476
7898
  // src/commands/geo.ts
7477
7899
  function buildGeoCommand() {
7478
- const geo = new Command("geo").description("Prepare and validate local GEO strategy runs");
7479
- geo.command("prepare <workspace>").description("Prepare a local GEO strategy run package").option("--repo <path>", "Use an explicit tenant repo path").option("--days <days>", "Signal lookback window in days", parsePositiveInt).option("--json", "Output as JSON").action(geoPrepareCommand);
7900
+ const geo = new Command("geo").description("Manage local GEO repo setup and strategy runs");
7901
+ geo.command("init").description("Initialize GEO scaffold and skills in a tenant repo").option("--repo <path>", "Tenant repo path", ".").option("--skills-source <path>", "Use an explicit GEO skillpack source directory").option("--ref <ref>", "Skillpack ref label to write into agent/skillpack.json", "local").option("--json", "Output as JSON").action(geoInitCommand);
7902
+ geo.command("sync").description("Sync managed GEO scaffold files without touching memory or run artifacts").option("--repo <path>", "Tenant repo path", ".").option("--skills-source <path>", "Use an explicit GEO skillpack source directory").option("--ref <ref>", "Skillpack ref label to write into agent/skillpack.json", "local").option("--json", "Output as JSON").action(geoSyncCommand);
7903
+ geo.command("doctor").description("Check whether a tenant repo GEO scaffold matches the installed skillpack").option("--repo <path>", "Tenant repo path", ".").option("--skills-source <path>", "Use an explicit GEO skillpack source directory").option("--json", "Output as JSON").action(geoDoctorCommand);
7904
+ geo.command("prepare <workspace>").description("Prepare a local GEO strategy run package").option("--repo <path>", "Use an explicit tenant repo path").option("--days <days>", "Signal lookback window in days", parsePositiveInt).option("--skills-source <path>", "Use an explicit GEO skillpack source directory").option("--skip-scaffold", "Skip GEO scaffold sync before writing the run package").option("--json", "Output as JSON").action(geoPrepareCommand);
7480
7905
  geo.command("validate <runId>").description("Validate local GEO strategy run artifacts before pushing").option("--repo <path>", "Use an explicit tenant repo path").option("--json", "Output as JSON").action(geoValidateCommand);
7906
+ geo.command("intents <runId>").description("Show the pre-fetched intents for a local GEO strategy run").option("--repo <path>", "Use an explicit tenant repo path").option("--json", "Output as JSON").action(geoIntentsCommand);
7481
7907
  geo.command("status <runId>").description("Show backend sync status for a GEO strategy run").option("--json", "Output as JSON").action(geoStatusCommand);
7482
7908
  return geo;
7483
7909
  }
7910
+ async function geoInitCommand(opts) {
7911
+ try {
7912
+ const result = await initGeoScaffold({
7913
+ repoPath: opts.repo,
7914
+ skillsSourceDir: opts.skillsSource,
7915
+ ref: opts.ref
7916
+ });
7917
+ printScaffoldResult("Initialized GEO scaffold", result, opts.json);
7918
+ } catch (err) {
7919
+ printScaffoldError("Couldn't initialize GEO scaffold", err);
7920
+ }
7921
+ }
7922
+ async function geoSyncCommand(opts) {
7923
+ try {
7924
+ const result = await syncGeoScaffold({
7925
+ repoPath: opts.repo,
7926
+ skillsSourceDir: opts.skillsSource,
7927
+ ref: opts.ref
7928
+ });
7929
+ printScaffoldResult("Synced GEO scaffold", result, opts.json);
7930
+ } catch (err) {
7931
+ printScaffoldError("Couldn't sync GEO scaffold", err);
7932
+ }
7933
+ }
7934
+ async function geoDoctorCommand(opts) {
7935
+ try {
7936
+ const result = await doctorGeoScaffold({
7937
+ repoPath: opts.repo,
7938
+ skillsSourceDir: opts.skillsSource
7939
+ });
7940
+ if (opts.json) {
7941
+ console.log(JSON.stringify(result, null, 2));
7942
+ process.exit(result.ok ? 0 : 1);
7943
+ }
7944
+ if (result.ok) {
7945
+ console.log(source_default.green("\n GEO scaffold is healthy.\n"));
7946
+ return;
7947
+ }
7948
+ console.error(source_default.red("\n GEO scaffold has drift\n"));
7949
+ for (const problem of result.problems) console.error(` - ${problem}`);
7950
+ console.error();
7951
+ process.exit(1);
7952
+ } catch (err) {
7953
+ printScaffoldError("Couldn't check GEO scaffold", err);
7954
+ }
7955
+ }
7484
7956
  async function geoPrepareCommand(workspace, opts) {
7485
7957
  const res = await apiRequest("POST", "/api/cli/geo-strategy/prepare", {
7486
7958
  workspace,
@@ -7495,6 +7967,13 @@ async function geoPrepareCommand(workspace, opts) {
7495
7967
  let runDir;
7496
7968
  try {
7497
7969
  repoPath = await prepareTenantRepo({ package: pkg, repo: opts.repo });
7970
+ if (!opts.skipScaffold) {
7971
+ await initGeoScaffold({
7972
+ repoPath,
7973
+ skillsSourceDir: opts.skillsSource,
7974
+ ref: pkg.branch
7975
+ });
7976
+ }
7498
7977
  ({ runDir } = await writeGeoRunPackage({ repoPath, package: pkg }));
7499
7978
  } catch (err) {
7500
7979
  const message = err instanceof Error ? err.message : String(err);
@@ -7554,6 +8033,45 @@ async function geoValidateCommand(runId, opts) {
7554
8033
  console.error();
7555
8034
  process.exit(1);
7556
8035
  }
8036
+ async function geoIntentsCommand(runId, opts) {
8037
+ const repoPath = opts.repo ?? await findRepoForRun(runId);
8038
+ if (!repoPath) {
8039
+ console.error(source_default.red(`Could not find local repo for run ${runId}. Pass --repo <path>.`));
8040
+ process.exit(1);
8041
+ }
8042
+ let payload;
8043
+ try {
8044
+ payload = await readGeoIntentList(join6(repoPath, "agent", "runs", runId));
8045
+ } catch (err) {
8046
+ const message = err instanceof Error ? err.message : String(err);
8047
+ console.error(source_default.red(`Couldn't read intents for run ${runId}: ${message}`));
8048
+ process.exit(1);
8049
+ }
8050
+ if (opts.json) {
8051
+ console.log(JSON.stringify(payload, null, 2));
8052
+ return;
8053
+ }
8054
+ console.log();
8055
+ console.log(source_default.bold(` GEO intents for ${runId}`));
8056
+ console.log(` ${source_default.bold("Workspace:")} ${payload.workspaceDomain ?? payload.workspaceId ?? "unknown"}`);
8057
+ console.log(` ${source_default.bold("Count:")} ${payload.intents.length}`);
8058
+ console.log();
8059
+ for (const intent of payload.intents) {
8060
+ const row = intent && typeof intent === "object" ? intent : {};
8061
+ const id = stringValue(row.id ?? row.intentId ?? row.intent_id) ?? "unknown";
8062
+ const name = stringValue(row.name ?? row.intentName ?? row.intent_name) ?? "(unnamed)";
8063
+ const type = stringValue(row.type ?? row.intentType ?? row.intent_type);
8064
+ const visibility = numberValue(row.currentVisibility ?? row.current_visibility);
8065
+ const competitor = numberValue(row.competitorVisibility ?? row.competitor_visibility);
8066
+ const details = [
8067
+ type ? `type=${type}` : null,
8068
+ visibility !== null ? `visibility=${formatPct(visibility)}` : null,
8069
+ competitor !== null ? `competitor=${formatPct(competitor)}` : null
8070
+ ].filter(Boolean).join(", ");
8071
+ console.log(` - ${name} ${source_default.dim(`(${id})`)}${details ? source_default.dim(` - ${details}`) : ""}`);
8072
+ }
8073
+ console.log();
8074
+ }
7557
8075
  async function geoStatusCommand(runId, opts) {
7558
8076
  const res = await apiRequest("GET", `/api/cli/geo-strategy/runs/${encodeURIComponent(runId)}`);
7559
8077
  if (!res.ok) {
@@ -7579,10 +8097,64 @@ function parsePositiveInt(value) {
7579
8097
  }
7580
8098
  return parsed;
7581
8099
  }
8100
+ async function readGeoIntentList(runDir) {
8101
+ try {
8102
+ const raw = await readFile3(join6(runDir, "intents.json"), "utf8");
8103
+ const parsed = JSON.parse(raw);
8104
+ if (parsed && typeof parsed === "object" && Array.isArray(parsed.intents)) {
8105
+ return parsed;
8106
+ }
8107
+ throw new Error("intents.json does not contain an intents array");
8108
+ } catch (err) {
8109
+ const contextRaw = await readFile3(join6(runDir, "context.json"), "utf8").catch(() => null);
8110
+ if (!contextRaw) throw err;
8111
+ const context = JSON.parse(contextRaw);
8112
+ return {
8113
+ runId: context.runId,
8114
+ workspaceId: context.workspaceId,
8115
+ workspaceDomain: context.workspaceDomain,
8116
+ intents: Array.isArray(context.signals?.intents) ? context.signals.intents : []
8117
+ };
8118
+ }
8119
+ }
8120
+ function stringValue(value) {
8121
+ return typeof value === "string" && value.length > 0 ? value : null;
8122
+ }
8123
+ function numberValue(value) {
8124
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
8125
+ }
8126
+ function formatPct(value) {
8127
+ return `${(value * 100).toFixed(1)}%`;
8128
+ }
7582
8129
  async function printApiFailure(res, fallback2) {
7583
8130
  const body = await res.json().catch(() => null);
7584
8131
  console.error(source_default.red(body?.message ?? body?.error ?? fallback2));
7585
8132
  }
8133
+ function printScaffoldResult(title, result, json) {
8134
+ if (json) {
8135
+ console.log(JSON.stringify(result, null, 2));
8136
+ process.exit(result.ok ? 0 : 1);
8137
+ }
8138
+ console.log();
8139
+ console.log(source_default.green.bold(` ${title}`));
8140
+ console.log(` ${source_default.bold("Repo:")} ${result.repoPath}`);
8141
+ console.log(` ${source_default.bold("Skillpack:")} ${result.skillsSourceDir}`);
8142
+ console.log(` ${source_default.bold("Hash:")} ${result.manifestHash}`);
8143
+ if (result.changed.length > 0) {
8144
+ console.log();
8145
+ console.log(source_default.bold(" Changed:"));
8146
+ for (const path2 of result.changed) console.log(` - ${path2}`);
8147
+ } else {
8148
+ console.log();
8149
+ console.log(source_default.dim(" No file changes."));
8150
+ }
8151
+ console.log();
8152
+ }
8153
+ function printScaffoldError(prefix, err) {
8154
+ const message = err instanceof Error ? err.message : String(err);
8155
+ console.error(source_default.red(`${prefix}: ${message}`));
8156
+ process.exit(1);
8157
+ }
7586
8158
 
7587
8159
  // src/index.ts
7588
8160
  var program2 = new Command();
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "anymorph",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Check your brand's AI visibility across ChatGPT, Perplexity, Gemini, and more",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "bin": {
8
- "anymorph": "./dist/index.js"
8
+ "anymorph": "dist/index.js"
9
9
  },
10
10
  "files": [
11
11
  "dist",