@aiguru/google-web-operations 0.6.4

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 (116) hide show
  1. package/.env.example +42 -0
  2. package/README.md +110 -0
  3. package/apps/cli/src/index.js +286 -0
  4. package/apps/mcp-server/src/index.js +360 -0
  5. package/apps/worker/src/index.js +96 -0
  6. package/apps/worker/src/loop.js +46 -0
  7. package/bin/gwo.js +67 -0
  8. package/docs/BOOTSTRAP-NEXT-PROMPT.md +57 -0
  9. package/docs/GETTING-STARTED.he.md +63 -0
  10. package/docs/HOSTED-WORKER.md +36 -0
  11. package/docs/INSTALL.he.md +57 -0
  12. package/docs/PRODUCT-TECHNICAL-SPEC-v0.2.md +153 -0
  13. package/docs/PRODUCT-TECHNICAL-SPEC-v0.3.md +86 -0
  14. package/docs/PRODUCT-TECHNICAL-SPEC-v0.4.md +92 -0
  15. package/docs/PRODUCT-TECHNICAL-SPEC-v0.5.md +94 -0
  16. package/docs/PRODUCT-TECHNICAL-SPEC-v0.6.md +98 -0
  17. package/docs/SECURITY.md +69 -0
  18. package/docs/SITE-ADAPTERS.md +87 -0
  19. package/docs/adr/ADR-001-shared-mcp-core.md +27 -0
  20. package/docs/adr/ADR-002-policy-gated-writes.md +18 -0
  21. package/docs/adr/ADR-003-single-source-playbooks.md +18 -0
  22. package/docs/adr/ADR-004-native-typescript-runtime.md +16 -0
  23. package/docs/adr/ADR-005-two-token-publish.md +22 -0
  24. package/docs/adr/ADR-006-one-package-one-installer.md +20 -0
  25. package/hosts/claude/.claude-plugin/plugin.json +9 -0
  26. package/hosts/claude/.mcp.json +10 -0
  27. package/hosts/claude/README.md +7 -0
  28. package/hosts/claude/commands/gwo-add-site.md +7 -0
  29. package/hosts/claude/commands/gwo-daily.md +7 -0
  30. package/hosts/claude/commands/gwo-measure.md +7 -0
  31. package/hosts/claude/commands/gwo-monthly.md +7 -0
  32. package/hosts/claude/commands/gwo-onboard.md +7 -0
  33. package/hosts/claude/commands/gwo-scan.md +7 -0
  34. package/hosts/claude/commands/gwo-setup.md +7 -0
  35. package/hosts/claude/commands/gwo-triage.md +7 -0
  36. package/hosts/claude/commands/gwo-weekly.md +7 -0
  37. package/hosts/claude/skills/gwo-client-report-template/SKILL.md +36 -0
  38. package/hosts/claude/skills/gwo-content-brief/SKILL.md +60 -0
  39. package/hosts/claude/skills/gwo-daily/SKILL.md +47 -0
  40. package/hosts/claude/skills/gwo-measurement/SKILL.md +92 -0
  41. package/hosts/claude/skills/gwo-monthly/SKILL.md +30 -0
  42. package/hosts/claude/skills/gwo-onboarding/SKILL.md +27 -0
  43. package/hosts/claude/skills/gwo-operating-model/SKILL.md +76 -0
  44. package/hosts/claude/skills/gwo-setup/SKILL.md +42 -0
  45. package/hosts/claude/skills/gwo-triage/SKILL.md +37 -0
  46. package/hosts/claude/skills/gwo-weekly/SKILL.md +48 -0
  47. package/hosts/codex/.codex/config.template.toml +5 -0
  48. package/hosts/codex/.codex-plugin/plugin.json +30 -0
  49. package/hosts/codex/AGENTS.md +470 -0
  50. package/hosts/codex/README.md +13 -0
  51. package/hosts/codex/skills/gwo-client-report-template/SKILL.md +36 -0
  52. package/hosts/codex/skills/gwo-content-brief/SKILL.md +60 -0
  53. package/hosts/codex/skills/gwo-daily/SKILL.md +47 -0
  54. package/hosts/codex/skills/gwo-measurement/SKILL.md +92 -0
  55. package/hosts/codex/skills/gwo-monthly/SKILL.md +30 -0
  56. package/hosts/codex/skills/gwo-onboarding/SKILL.md +27 -0
  57. package/hosts/codex/skills/gwo-operating-model/SKILL.md +76 -0
  58. package/hosts/codex/skills/gwo-setup/SKILL.md +42 -0
  59. package/hosts/codex/skills/gwo-triage/SKILL.md +37 -0
  60. package/hosts/codex/skills/gwo-weekly/SKILL.md +48 -0
  61. package/package.json +24 -0
  62. package/packages/connectors/browser/src/index.js +122 -0
  63. package/packages/connectors/docx/src/index.js +122 -0
  64. package/packages/connectors/github/src/index.js +102 -0
  65. package/packages/connectors/google/src/auth.js +73 -0
  66. package/packages/connectors/google/src/discover.js +135 -0
  67. package/packages/connectors/google/src/ga4.js +42 -0
  68. package/packages/connectors/google/src/gsc.js +49 -0
  69. package/packages/connectors/google/src/gtm.js +261 -0
  70. package/packages/connectors/google/src/index.js +8 -0
  71. package/packages/connectors/google/src/oauth.js +198 -0
  72. package/packages/connectors/google/src/pagespeed.js +48 -0
  73. package/packages/connectors/google/src/token-store.js +89 -0
  74. package/packages/connectors/odoo/src/index.js +206 -0
  75. package/packages/connectors/repo/src/index.js +94 -0
  76. package/packages/connectors/resend/src/index.js +30 -0
  77. package/packages/connectors/web-crawler/src/a11y.js +91 -0
  78. package/packages/connectors/web-crawler/src/fetch.js +165 -0
  79. package/packages/connectors/web-crawler/src/index.js +69 -0
  80. package/packages/connectors/web-crawler/src/parse.js +157 -0
  81. package/packages/core/src/adapters.js +27 -0
  82. package/packages/core/src/admin.js +232 -0
  83. package/packages/core/src/content.js +213 -0
  84. package/packages/core/src/doctor.js +39 -0
  85. package/packages/core/src/escalate.js +36 -0
  86. package/packages/core/src/gtm-ops.js +311 -0
  87. package/packages/core/src/index.js +13 -0
  88. package/packages/core/src/operations.js +239 -0
  89. package/packages/core/src/paths.js +46 -0
  90. package/packages/core/src/registry.js +114 -0
  91. package/packages/core/src/report/monthly.js +272 -0
  92. package/packages/core/src/scan/render.js +40 -0
  93. package/packages/core/src/scan/rules.js +289 -0
  94. package/packages/core/src/scan/suppress.js +37 -0
  95. package/packages/core/src/types.js +10 -0
  96. package/packages/core/src/writes.js +50 -0
  97. package/packages/playbooks/00-operating-model.md +74 -0
  98. package/packages/playbooks/10-daily.md +45 -0
  99. package/packages/playbooks/20-weekly.md +46 -0
  100. package/packages/playbooks/30-monthly.md +28 -0
  101. package/packages/playbooks/40-triage.md +35 -0
  102. package/packages/playbooks/50-onboarding.md +25 -0
  103. package/packages/playbooks/60-content-brief.md +58 -0
  104. package/packages/playbooks/70-measurement.md +90 -0
  105. package/packages/playbooks/80-setup.md +40 -0
  106. package/packages/playbooks/90-client-report-template.md +34 -0
  107. package/packages/policy/src/index.js +115 -0
  108. package/packages/storage/src/index.js +131 -0
  109. package/policies/default.yaml +90 -0
  110. package/sites/schema.yaml +71 -0
  111. package/sites/secrets.example.yaml +6 -0
  112. package/tools/build-hosts.js +118 -0
  113. package/tools/build-package.js +43 -0
  114. package/tools/install.js +145 -0
  115. package/tools/schedule.ps1 +37 -0
  116. package/tools/smoke-mcp.js +61 -0
@@ -0,0 +1,6 @@
1
+ # Copy to sites/<host>.secrets.yaml next to sites/<host>.yaml. Gitignored.
2
+ # Merged into the site record at load time and stripped from every public view.
3
+
4
+ odoo:
5
+ login: gwo-api # dedicated API user, Website > Restricted Editor is enough
6
+ api_key: "paste-the-api-key-here"
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Generate hosts/claude and hosts/codex from packages/playbooks.
4
+ *
5
+ * Never edit hosts/ by hand. CI fails if hosts/ differs from what this
6
+ * script produces, so nothing here may depend on the machine it runs on:
7
+ * absolute paths are placeholders that tools/install.ts resolves.
8
+ *
9
+ * Claude Code: a plugin directory with .claude-plugin/plugin.json, .mcp.json,
10
+ * skills/<name>/SKILL.md and commands/<name>.md.
11
+ * Codex: a plugin directory with .codex-plugin/plugin.json and the same
12
+ * skills/<name>/SKILL.md (Codex reads the same SKILL.md format),
13
+ * plus AGENTS.md (all playbooks, for projects that only take
14
+ * instructions) and a config.toml template for the MCP server.
15
+ */
16
+ import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
17
+ import path from 'node:path';
18
+ import { HOSTS_DIR, PLAYBOOKS_DIR, ROOT } from "../packages/core/src/paths.js";
19
+ const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
20
+ const CLAUDE = path.join(HOSTS_DIR, 'claude');
21
+ const CODEX = path.join(HOSTS_DIR, 'codex');
22
+ const HEADER = '<!-- GENERATED by tools/build-hosts.ts from packages/playbooks. Do not edit here. -->\n\n';
23
+ /** Placeholders resolved per machine by tools/install.ts (gwo install). */
24
+ export const PLACEHOLDER = { server: '__GWO_SERVER__', node: '__NODE__' };
25
+ function readPlaybooks() {
26
+ return readdirSync(PLAYBOOKS_DIR).filter((f) => f.endsWith('.md')).sort().map((file) => {
27
+ const raw = readFileSync(path.join(PLAYBOOKS_DIR, file), 'utf8');
28
+ const m = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
29
+ if (!m)
30
+ throw new Error(`${file}: missing frontmatter`);
31
+ const fm = Object.fromEntries((m[1] ?? '').split('\n').map((l) => {
32
+ const i = l.indexOf(':');
33
+ return [l.slice(0, i).trim(), l.slice(i + 1).trim()];
34
+ }));
35
+ const name = fm['name'], description = fm['description'];
36
+ if (!name || !description)
37
+ throw new Error(`${file}: frontmatter needs name and description`);
38
+ return { file, name, description, body: (m[2] ?? '').trim(), frontmatter: m[1] ?? '' };
39
+ });
40
+ }
41
+ function write(p, s) { mkdirSync(path.dirname(p), { recursive: true }); writeFileSync(p, s); }
42
+ /** One SKILL.md per playbook. Identical for both hosts; the format is shared. */
43
+ function writeSkills(dir, playbooks) {
44
+ for (const p of playbooks) {
45
+ write(path.join(dir, 'skills', p.name, 'SKILL.md'), `---\n${p.frontmatter}\n---\n\n${HEADER}${p.body}\n`);
46
+ }
47
+ }
48
+ const COMMANDS = {
49
+ 'gwo-daily': 'Run the daily health pass. Load skills gwo-operating-model and gwo-daily, then execute for every enabled site, or only the site named in $ARGUMENTS.',
50
+ 'gwo-weekly': 'Run the weekly improvement pass. Load gwo-operating-model and gwo-weekly, then execute for every enabled site, or only $ARGUMENTS.',
51
+ 'gwo-monthly': 'Run the monthly deep scan and produce both reports. Load gwo-operating-model, gwo-monthly and gwo-client-report-template. Site: $ARGUMENTS or all.',
52
+ 'gwo-triage': 'Triage an incident for the site in $ARGUMENTS. Load gwo-operating-model and gwo-triage. Find the cause before proposing any fix.',
53
+ 'gwo-onboard': 'Onboard the site in $ARGUMENTS. Load gwo-operating-model and gwo-onboarding.',
54
+ 'gwo-scan': 'Run web_scan_site for the site in $ARGUMENTS and summarise the findings in the operator tone from gwo-operating-model.',
55
+ 'gwo-setup': 'Set up or administer the system from the chat. Load gwo-setup, call ops_setup_status, and walk the operator through the next step: $ARGUMENTS.',
56
+ 'gwo-add-site': 'Register the site in $ARGUMENTS. Load gwo-setup and gwo-onboarding; ask only for what cannot be inferred, then ops_add_site, baseline scan, and the access checklist for the client.',
57
+ 'gwo-measure': 'Audit measurement for the site in $ARGUMENTS. Load gwo-operating-model and gwo-measurement, run the audit, and propose staged Tag Manager changes with their tokens.',
58
+ };
59
+ // ------------------------------------------------------------------ Claude Code
60
+ function buildClaude(playbooks) {
61
+ rmSync(CLAUDE, { recursive: true, force: true });
62
+ write(path.join(CLAUDE, '.claude-plugin', 'plugin.json'), JSON.stringify({
63
+ name: 'google-web-operations', version: pkg.version, description: pkg.description,
64
+ author: { name: 'AI Guru' }, mcpServers: './.mcp.json',
65
+ }, null, 2) + '\n');
66
+ // Runs the server with node directly: no npm, no tsx, no build step.
67
+ write(path.join(CLAUDE, '.mcp.json'), JSON.stringify({ mcpServers: { 'google-web-operations': {
68
+ command: 'node', args: ['${CLAUDE_PLUGIN_ROOT}/../../apps/mcp-server/src/index.ts'],
69
+ } } }, null, 2) + '\n');
70
+ writeSkills(CLAUDE, playbooks);
71
+ for (const [name, body] of Object.entries(COMMANDS)) {
72
+ write(path.join(CLAUDE, 'commands', `${name}.md`), `---\ndescription: ${body.split('.')[0]}.\n---\n\n${HEADER}${body}\n`);
73
+ }
74
+ write(path.join(CLAUDE, 'README.md'), HEADER + '# Google Web Operations for Claude Code\n\n' +
75
+ 'Install with `gwo install claude` (user-scope MCP server, skills and commands), or load this directory as a plugin.\n\n' +
76
+ `Commands: ${Object.keys(COMMANDS).map((c) => '/' + c).join(', ')}.\n`);
77
+ }
78
+ // ------------------------------------------------------------------ Codex
79
+ function buildCodex(playbooks) {
80
+ rmSync(CODEX, { recursive: true, force: true });
81
+ // A valid Codex plugin directory: manifest + skills. Installable from a local marketplace.
82
+ write(path.join(CODEX, '.codex-plugin', 'plugin.json'), JSON.stringify({
83
+ name: 'google-web-operations', version: pkg.version, description: pkg.description,
84
+ author: { name: 'AI Guru' }, homepage: 'https://github.com/ai-gur/GWeb-Ops', license: 'Proprietary',
85
+ keywords: ['seo', 'geo', 'search console', 'analytics', 'tag manager', 'website operations'],
86
+ skills: './skills/',
87
+ interface: {
88
+ displayName: 'Google Web Operations', shortDescription: 'Operate client websites in search and AI answer engines',
89
+ longDescription: 'Scans sites, reads Search Console, GA4, Tag Manager and PageSpeed, applies policy-gated and verified fixes, and runs daily, weekly and monthly playbooks.',
90
+ developerName: 'AI Guru', category: 'Productivity', capabilities: ['Read', 'Write'],
91
+ },
92
+ }, null, 2) + '\n');
93
+ writeSkills(CODEX, playbooks);
94
+ // Instructions file for projects that take AGENTS.md rather than skills.
95
+ const agents = [HEADER, '# Google Web Operations\n',
96
+ '\nThe `google-web-operations` MCP server is the source of truth for site state. The sections below are the operating playbooks; load the matching one before a run.\n'];
97
+ for (const p of playbooks)
98
+ agents.push(`\n\n<!-- ${p.file} -->\n\n${p.body}\n`);
99
+ write(path.join(CODEX, 'AGENTS.md'), agents.join(''));
100
+ // MCP server entry. Codex config cannot expand variables and the existing
101
+ // entries on operator machines use absolute executables, so both the node
102
+ // binary and the repository root are placeholders resolved at install time.
103
+ write(path.join(CODEX, '.codex', 'config.template.toml'), `# Generated template. "gwo install codex" resolves ${PLACEHOLDER.node} and ${PLACEHOLDER.server} and writes this into ~/.codex/config.toml.\n` +
104
+ '[mcp_servers.google-web-operations]\n' +
105
+ `command = '${PLACEHOLDER.node}'\n` +
106
+ `args = ['${PLACEHOLDER.server}']\n` +
107
+ 'startup_timeout_sec = 60\n');
108
+ write(path.join(CODEX, 'README.md'), HEADER + '# Google Web Operations for Codex\n\n' +
109
+ 'This directory is a Codex plugin (`.codex-plugin/plugin.json` + `skills/`). Install everything with:\n\n' +
110
+ '```\ngwo install codex\n```\n\n' +
111
+ 'That writes the MCP server into `~/.codex/config.toml` (absolute paths resolved for this machine) and copies the ' +
112
+ 'skills into `~/.codex/skills/`. `gwo install codex --print` only shows what it would do.\n\n' +
113
+ 'For a project that takes instructions rather than skills, place `AGENTS.md` where Codex runs.\n');
114
+ }
115
+ const playbooks = readPlaybooks();
116
+ buildClaude(playbooks);
117
+ buildCodex(playbooks);
118
+ console.log(`hosts/claude: ${playbooks.length} skills, ${Object.keys(COMMANDS).length} commands. hosts/codex: plugin manifest, ${playbooks.length} skills, AGENTS.md, config template.`);
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build the distributable package under build/.
4
+ *
5
+ * Development runs TypeScript natively (ADR-004), but Node refuses to strip
6
+ * types for anything under node_modules, so the package people install
7
+ * globally must ship JavaScript. This script:
8
+ * 1. compiles apps/, packages/ and tools/ to build/ with .ts imports
9
+ * rewritten to .js (tsc, rewriteRelativeImportExtensions);
10
+ * 2. copies the non-code parts the code resolves from its root: policies,
11
+ * hosts, playbooks, site templates, .env.example, docs, the scheduler,
12
+ * the bin;
13
+ * 3. writes a package.json without dev-only fields.
14
+ * `npm pack` inside build/ then produces the tarball (npm run pack:release).
15
+ */
16
+ import { execFileSync } from 'node:child_process';
17
+ import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs';
18
+ import path from 'node:path';
19
+ import { ROOT } from "../packages/core/src/paths.js";
20
+ const OUT = path.join(ROOT, 'build');
21
+ rmSync(OUT, { recursive: true, force: true });
22
+ mkdirSync(OUT, { recursive: true });
23
+ // 1. Compile.
24
+ const tsc = path.join(ROOT, 'node_modules', 'typescript', 'bin', 'tsc');
25
+ execFileSync(process.execPath, [tsc, '-p', path.join(ROOT, 'tsconfig.build.json')], { stdio: 'inherit' });
26
+ // 2. Static parts, at the same relative paths the code expects under its root.
27
+ const copy = (rel) => { if (existsSync(path.join(ROOT, rel)))
28
+ cpSync(path.join(ROOT, rel), path.join(OUT, rel), { recursive: true }); };
29
+ for (const rel of ['policies', 'hosts', 'bin', 'docs', 'README.md', '.env.example', path.join('tools', 'schedule.ps1'), path.join('sites', 'schema.yaml'), path.join('sites', 'secrets.example.yaml')])
30
+ copy(rel);
31
+ for (const f of ['00-operating-model.md'])
32
+ void f; // playbooks are copied whole below
33
+ cpSync(path.join(ROOT, 'packages', 'playbooks'), path.join(OUT, 'packages', 'playbooks'), { recursive: true, filter: (src) => !src.endsWith('.ts') });
34
+ // 3. package.json for the tarball: runtime fields only.
35
+ const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
36
+ const out = {
37
+ name: pkg['name'], version: pkg['version'], description: pkg['description'], license: pkg['license'] ?? 'UNLICENSED', private: false,
38
+ type: 'module', engines: pkg['engines'], repository: pkg['repository'],
39
+ bin: { gwo: 'bin/gwo.js' },
40
+ dependencies: pkg['dependencies'],
41
+ };
42
+ writeFileSync(path.join(OUT, 'package.json'), JSON.stringify(out, null, 2) + '\n');
43
+ console.log(`build/: ${pkg['name']}@${pkg['version']} (JavaScript, ready for npm pack)`);
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Install or remove the hosts' side of Google Web Operations on this machine.
4
+ *
5
+ * gwo install [claude|codex|all] [--print]
6
+ * gwo uninstall [claude|codex|all]
7
+ *
8
+ * Claude Code: the MCP server goes into ~/.claude.json (user scope), skills
9
+ * into ~/.claude/skills/gwo-*, slash commands into
10
+ * ~/.claude/commands/gwo-*.md.
11
+ * Codex: the MCP server goes into ~/.codex/config.toml, skills into
12
+ * ~/.codex/skills/gwo-*.
13
+ * Both: the operator state directory (GWO_HOME, default ~/.gwo) is
14
+ * created with .env, sites/schema.yaml and secrets.example.yaml
15
+ * so the first ops_setup_status has something to point at.
16
+ *
17
+ * Only gwo-* entries are ever written or removed; nothing else in those
18
+ * directories or files is touched. Idempotent: run it again after an upgrade.
19
+ */
20
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, copyFileSync } from 'node:fs';
21
+ import { homedir } from 'node:os';
22
+ import path from 'node:path';
23
+ import { HOME_DIR, HOSTS_DIR, IS_CHECKOUT, ROOT } from "../packages/core/src/paths.js";
24
+ const argv = process.argv.slice(2);
25
+ const uninstall = argv.includes('uninstall');
26
+ const targetArg = argv.find((a) => ['claude', 'codex', 'all'].includes(a)) ?? 'all';
27
+ const print = argv.includes('--print') || argv.includes('--dry-run');
28
+ const targets = targetArg === 'all' ? ['claude', 'codex'] : [targetArg];
29
+ const SERVER = 'google-web-operations';
30
+ const toPosix = (p) => p.replace(/\\/g, '/');
31
+ const node = process.execPath;
32
+ // A checkout runs TypeScript; the packaged build is JavaScript.
33
+ const server = ['index.js', 'index.ts'].map((f) => path.join(ROOT, 'apps', 'mcp-server', 'src', f)).find((f) => existsSync(f));
34
+ const CLAUDE_HOME = process.env['CLAUDE_CONFIG_DIR']?.trim() || path.join(homedir(), '.claude');
35
+ const CODEX_HOME = process.env['CODEX_HOME']?.trim() || path.join(homedir(), '.codex');
36
+ const log = (s) => console.log(s);
37
+ const gwoDirs = (dir) => (existsSync(dir) ? readdirSync(dir).filter((n) => n.startsWith('gwo-')) : []);
38
+ // ---------------------------------------------------------------- operator home
39
+ function ensureHome() {
40
+ if (IS_CHECKOUT) {
41
+ log(`state: repository checkout ${HOME_DIR}`);
42
+ return;
43
+ }
44
+ log(`state: ${HOME_DIR}${existsSync(HOME_DIR) ? '' : ' (will be created)'}`);
45
+ if (print)
46
+ return;
47
+ for (const d of ['sites', 'secrets', '.gwo-data'])
48
+ mkdirSync(path.join(HOME_DIR, d), { recursive: true });
49
+ const seed = (from, to) => { if (!existsSync(to) && existsSync(from))
50
+ copyFileSync(from, to); };
51
+ seed(path.join(ROOT, '.env.example'), path.join(HOME_DIR, '.env'));
52
+ seed(path.join(ROOT, 'sites', 'schema.yaml'), path.join(HOME_DIR, 'sites', 'schema.yaml'));
53
+ seed(path.join(ROOT, 'sites', 'secrets.example.yaml'), path.join(HOME_DIR, 'sites', 'secrets.example.yaml'));
54
+ }
55
+ // ---------------------------------------------------------------- Claude Code
56
+ function claude() {
57
+ const cfg = path.join(homedir(), '.claude.json');
58
+ const skillsSrc = path.join(HOSTS_DIR, 'claude', 'skills'), cmdSrc = path.join(HOSTS_DIR, 'claude', 'commands');
59
+ const skillsDst = path.join(CLAUDE_HOME, 'skills'), cmdDst = path.join(CLAUDE_HOME, 'commands');
60
+ const skills = gwoDirs(skillsSrc), commands = readdirSync(cmdSrc).filter((n) => n.startsWith('gwo-'));
61
+ log(`\nClaude Code`);
62
+ log(` MCP server "${SERVER}" ${uninstall ? 'removed from' : 'in'} ${cfg} (user scope)`);
63
+ log(` ${skills.length} skills -> ${skillsDst}, ${commands.length} commands -> ${cmdDst}`);
64
+ if (print)
65
+ return;
66
+ let json = {};
67
+ if (existsSync(cfg))
68
+ json = JSON.parse(readFileSync(cfg, 'utf8'));
69
+ const servers = (json['mcpServers'] ?? {});
70
+ if (uninstall)
71
+ delete servers[SERVER];
72
+ else
73
+ servers[SERVER] = { type: 'stdio', command: node, args: [server], env: {} };
74
+ json['mcpServers'] = servers;
75
+ writeFileSync(cfg, JSON.stringify(json, null, 2));
76
+ for (const n of gwoDirs(skillsDst))
77
+ rmSync(path.join(skillsDst, n), { recursive: true, force: true });
78
+ for (const n of (existsSync(cmdDst) ? readdirSync(cmdDst).filter((x) => x.startsWith('gwo-')) : []))
79
+ rmSync(path.join(cmdDst, n), { force: true });
80
+ if (!uninstall) {
81
+ mkdirSync(skillsDst, { recursive: true });
82
+ mkdirSync(cmdDst, { recursive: true });
83
+ for (const n of skills)
84
+ cpSync(path.join(skillsSrc, n), path.join(skillsDst, n), { recursive: true });
85
+ for (const n of commands)
86
+ copyFileSync(path.join(cmdSrc, n), path.join(cmdDst, n));
87
+ }
88
+ }
89
+ // ---------------------------------------------------------------- Codex
90
+ function codex() {
91
+ const cfg = path.join(CODEX_HOME, 'config.toml');
92
+ const skillsSrc = path.join(HOSTS_DIR, 'codex', 'skills'), skillsDst = path.join(CODEX_HOME, 'skills');
93
+ const skills = gwoDirs(skillsSrc);
94
+ const template = readFileSync(path.join(HOSTS_DIR, 'codex', '.codex', 'config.template.toml'), 'utf8')
95
+ .replace(/^# Generated template.*\n/m, '').replaceAll('__GWO_SERVER__', toPosix(server)).replaceAll('__NODE__', toPosix(node)).trim();
96
+ log(`\nCodex`);
97
+ log(` MCP server "${SERVER}" ${uninstall ? 'removed from' : 'in'} ${cfg}`);
98
+ log(` ${skills.length} skills -> ${skillsDst}`);
99
+ if (print) {
100
+ log(template.split('\n').map((l) => ' ' + l).join('\n'));
101
+ return;
102
+ }
103
+ mkdirSync(CODEX_HOME, { recursive: true });
104
+ let toml = existsSync(cfg) ? readFileSync(cfg, 'utf8') : '';
105
+ // Replace our block wholesale (from its header to the next table header or EOF), so upgrades change paths in place.
106
+ const block = new RegExp(`\\n?\\[mcp_servers\\.${SERVER.replace(/-/g, '\\-')}\\][\\s\\S]*?(?=\\n\\[|$)`);
107
+ toml = toml.replace(block, '');
108
+ if (!uninstall)
109
+ toml = toml.replace(/\s*$/, '\n') + '\n' + template + '\n';
110
+ writeFileSync(cfg, toml);
111
+ for (const n of gwoDirs(skillsDst))
112
+ rmSync(path.join(skillsDst, n), { recursive: true, force: true });
113
+ if (!uninstall) {
114
+ mkdirSync(skillsDst, { recursive: true });
115
+ for (const n of skills)
116
+ cpSync(path.join(skillsSrc, n), path.join(skillsDst, n), { recursive: true });
117
+ }
118
+ }
119
+ log(`Google Web Operations ${uninstall ? 'uninstall' : 'install'}${print ? ' (print only)' : ''}`);
120
+ log(`package: ${ROOT}`);
121
+ log(`node: ${node}`);
122
+ if (!uninstall)
123
+ ensureHome();
124
+ if (targets.includes('claude'))
125
+ claude();
126
+ if (targets.includes('codex'))
127
+ codex();
128
+ if (print)
129
+ log('\nNothing changed. Run without --print to apply.');
130
+ else if (uninstall)
131
+ log('\nDone. Restart Claude Code and Codex. Your sites, data and secrets were left in place.');
132
+ else
133
+ log([
134
+ '',
135
+ 'Done.',
136
+ '',
137
+ 'Next',
138
+ ' 1. Restart Claude Code and Codex so they pick up the server and skills.',
139
+ ' 2. Claude Code: type /gwo-setup. Codex: say "set up Google Web Operations".',
140
+ ' The assistant connects Google, registers the first site and tells you what the client must grant.',
141
+ ' 3. Any time: gwo doctor (what is configured, what is missing, the next step)',
142
+ '',
143
+ `State lives in ${HOME_DIR}: sites/, .gwo-data/, secrets/, .env. Upgrades never touch it.`,
144
+ 'Docs: docs/INSTALL.he.md, docs/GETTING-STARTED.he.md (עברית), README.md.',
145
+ ].join('\n'));
@@ -0,0 +1,37 @@
1
+ # Register (or remove) Windows Task Scheduler jobs that run the worker.
2
+ # powershell -ExecutionPolicy Bypass -File tools\schedule.ps1 install
3
+ # powershell -ExecutionPolicy Bypass -File tools\schedule.ps1 remove
4
+ # powershell -ExecutionPolicy Bypass -File tools\schedule.ps1 status
5
+ #
6
+ # Runs as the current user (the OAuth token is DPAPI-bound to this user), only
7
+ # when the machine is on, and catches up a missed run at next start.
8
+ param([ValidateSet('install','remove','status')][string]$Action = 'status')
9
+
10
+ $root = Split-Path -Parent $PSScriptRoot
11
+ $node = (Get-Command node).Source
12
+ $worker = Join-Path $root 'apps\worker\src\index.js' # packaged build
13
+ if (-not (Test-Path $worker)) { $worker = Join-Path $root 'apps\worker\src\index.ts' } # repository checkout
14
+ $jobs = @(
15
+ @{ Name = 'GWebOps Daily'; Args = "daily --deliver"; Trigger = (New-ScheduledTaskTrigger -Daily -At 07:00) },
16
+ @{ Name = 'GWebOps Weekly'; Args = "weekly --deliver"; Trigger = (New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 07:30) }
17
+ )
18
+
19
+ switch ($Action) {
20
+ 'install' {
21
+ foreach ($j in $jobs) {
22
+ $taskAction = New-ScheduledTaskAction -Execute $node -Argument "`"$worker`" $($j.Args)" -WorkingDirectory $root
23
+ $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 1) -MultipleInstances IgnoreNew
24
+ Register-ScheduledTask -TaskName $j.Name -Action $taskAction -Trigger $j.Trigger -Settings $settings -Description 'Google Web Operations scheduled sensing run' -Force | Out-Null
25
+ Write-Output "registered: $($j.Name)"
26
+ }
27
+ }
28
+ 'remove' {
29
+ foreach ($j in $jobs) { Unregister-ScheduledTask -TaskName $j.Name -Confirm:$false -ErrorAction SilentlyContinue; Write-Output "removed: $($j.Name)" }
30
+ }
31
+ 'status' {
32
+ Get-ScheduledTask -TaskName 'GWebOps *' -ErrorAction SilentlyContinue | ForEach-Object {
33
+ $i = $_ | Get-ScheduledTaskInfo
34
+ "{0,-16} state={1,-8} last={2} result={3} next={4}" -f $_.TaskName, $_.State, $i.LastRunTime, $i.LastTaskResult, $i.NextRunTime
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Smoke test: start the MCP server over stdio, initialise, list tools,
4
+ * call a registry tool and two policy checks. Exits non-zero on failure.
5
+ */
6
+ import { spawn } from 'node:child_process';
7
+ import path from 'node:path';
8
+ import { ROOT } from "../packages/core/src/paths.js";
9
+ const server = path.join(ROOT, 'apps', 'mcp-server', 'src', 'index.ts');
10
+ const child = spawn(process.execPath, [server], { stdio: ['pipe', 'pipe', 'inherit'] });
11
+ const pending = new Map();
12
+ let buf = '';
13
+ child.stdout.on('data', (d) => {
14
+ buf += d.toString();
15
+ let i;
16
+ while ((i = buf.indexOf('\n')) >= 0) {
17
+ const line = buf.slice(0, i).trim();
18
+ buf = buf.slice(i + 1);
19
+ if (!line)
20
+ continue;
21
+ try {
22
+ const m = JSON.parse(line);
23
+ if (m.id !== undefined && pending.has(m.id)) {
24
+ pending.get(m.id)(m);
25
+ pending.delete(m.id);
26
+ }
27
+ }
28
+ catch { /* not json */ }
29
+ }
30
+ });
31
+ let nextId = 1;
32
+ const call = (method, params = {}) => new Promise((res) => {
33
+ const id = nextId++;
34
+ pending.set(id, res);
35
+ child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
36
+ });
37
+ const notify = (method) => child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params: {} }) + '\n');
38
+ const first = (m) => (m.result?.content?.[0]?.text ?? '').replace(/\s+/g, ' ').slice(0, 220);
39
+ const timer = setTimeout(() => { console.error('smoke: timeout'); child.kill(); process.exit(1); }, 20_000);
40
+ try {
41
+ const init = await call('initialize', { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'smoke', version: '0' } });
42
+ console.log('server:', JSON.stringify(init.result?.serverInfo));
43
+ notify('notifications/initialized');
44
+ const tools = await call('tools/list');
45
+ const names = (tools.result?.tools ?? []).map((t) => t.name);
46
+ console.log(`tools (${names.length}):`, names.join(', '));
47
+ if (names.length < 10)
48
+ throw new Error('expected at least 10 tools');
49
+ console.log('ops_list_sites ->', first(await call('tools/call', { name: 'ops_list_sites', arguments: {} })));
50
+ console.log('classify gtm.publish ->', first(await call('tools/call', { name: 'ops_classify_action', arguments: { action: 'gtm.publish', site: 'aiguru' } })));
51
+ console.log('classify google.property.delete ->', first(await call('tools/call', { name: 'ops_classify_action', arguments: { action: 'google.property.delete' } })));
52
+ console.log('smoke: OK');
53
+ }
54
+ catch (e) {
55
+ console.error('smoke: FAIL', e.message);
56
+ process.exitCode = 1;
57
+ }
58
+ finally {
59
+ clearTimeout(timer);
60
+ child.kill();
61
+ }