@aiguru/google-web-operations 0.6.5 → 0.6.7

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/.env.example CHANGED
@@ -31,8 +31,8 @@ GWO_DOCX_TEMPLATE=
31
31
 
32
32
  # --- Delivery (worker --deliver) -------------------------------------------
33
33
  RESEND_API_KEY=
34
- GWO_REPORT_FROM=ops@aiguru.co.il
35
- GWO_REPORT_TO=ai@aiguru.co.il
34
+ GWO_REPORT_FROM= # e.g. ops@your-domain.co.il (a domain verified in Resend)
35
+ GWO_REPORT_TO= # e.g. you@your-domain.co.il
36
36
 
37
37
  # --- GitHub (pull requests for repository-backed sites) --------------------
38
38
  # Fine-grained token: Contents write + Pull requests write on the site repos.
package/bin/gwo.js CHANGED
@@ -27,6 +27,12 @@ if (major < 22 || (major === 22 && minor < 18)) {
27
27
 
28
28
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
29
29
  const cmd = process.argv[2];
30
+
31
+ // Installed inside a project rather than globally? Say so once per run (stderr, never on the MCP channel).
32
+ // Global installs live under <prefix>/lib/node_modules or <prefix>/npm/node_modules; a checkout has no node_modules above it.
33
+ if (/[\\/]node_modules[\\/]/.test(root) && !/[\\/](lib|npm)[\\/]node_modules[\\/]/i.test(root) && cmd !== 'mcp') {
34
+ console.error('note: gwo is installed locally in a project. For a stable setup: npm i -g @aiguru/google-web-operations\n');
35
+ }
30
36
  // A checkout runs the TypeScript sources; the packaged build ships JavaScript.
31
37
  const ext = existsSync(path.join(root, 'apps', 'cli', 'src', 'index.js')) ? 'index.js' : 'index.ts';
32
38
  // Dynamic imports need file URLs on Windows; a bare drive path is read as a URL scheme.
@@ -6,7 +6,7 @@
6
6
 
7
7
  דרישות: Node 22.18 ומעלה.
8
8
 
9
- 1. בטרמינל:
9
+ 1. בטרמינל, מכל תיקייה, עם `-g` (בלעדיו npm מתקין לתיקייה הנוכחית ו‑`gwo` לא יהיה זמין):
10
10
  ```
11
11
  npm i -g @aiguru/google-web-operations
12
12
  gwo install
@@ -7,15 +7,29 @@
7
7
 
8
8
  ## 1. התקנה
9
9
 
10
+ מכל תיקייה שהיא (לא צריך תיקיית פרויקט), עם `-g`:
11
+
10
12
  ```
11
13
  npm i -g @aiguru/google-web-operations
12
14
  gwo install
13
15
  ```
14
16
 
17
+ ה‑`-g` חשוב: בלעדיו npm יוצר `package.json`, `package-lock.json` ו‑`node_modules` בתיקייה הנוכחית, ו‑`gwo` לא נכנס ל‑PATH. הכלי מזהה את זה: `gwo install` מסרב לרשום נתיב מתוך `node_modules` של פרויקט ואומר מה לעשות. אם זה קרה: למחוק את שלושת אלה ולהריץ שוב עם `-g`.
18
+
15
19
  `gwo install` רושם את שרת ה‑MCP, את הסקילים ואת הפקודות בקלוד ובקודקס, ויוצר את `~/.gwo`. רק להוסט אחד: `gwo install claude` או `gwo install codex`. לראות בלי לשנות: `gwo install --print`.
16
20
 
17
21
  חלופה בלי npm: להוריד את ה‑tgz מעמוד ה‑Releases בריפו ולהריץ `npm i -g ./aiguru-google-web-operations-<version>.tgz` (ה‑`./` חובה).
18
22
 
23
+ **איפה להריץ.** בטרמינל של המחשב שבו רצים Claude Code וקודקס (ב‑Windows: PowerShell או CMD). לא מתוך סוכן שרץ בסביבה מבודדת (Cowork, קונטיינר, VM): שם ההתקנה עובדת אבל נמחקת בסוף הסשן ולא נראית להוסטים שלך.
24
+
25
+ **Linux/macOS, שגיאת EACCES** על `/usr/lib/node_modules`: להתקין ל‑prefix של המשתמש במקום `sudo`:
26
+ ```
27
+ npm config set prefix ~/.npm-global
28
+ export PATH=~/.npm-global/bin:$PATH # וגם ב-~/.bashrc או ~/.zshrc
29
+ ```
30
+
31
+ **המצב הוא לכל משתמש ולכל מחשב.** `~/.gwo` (אתרים, הסכמה לגוגל, `.env`) לא משותף בין מחשבים או בין Windows ל‑VM. במחשב נוסף שאמור לראות את אותם אתרים: להעתיק `sites/*.yaml`, ולעשות את ההסכמה לגוגל מחדש.
32
+
19
33
  ## 2. הפעלה מחדש ובדיקה
20
34
 
21
35
  הפעלה מחדש של Claude Code ו‑Codex, ואז:
@@ -30,6 +44,9 @@ gwo doctor
30
44
 
31
45
  - Claude Code: `/gwo-setup`
32
46
  - Codex: "הגדר את Google Web Operations"
47
+ - Claude for Windows / Mac (הדסקטופ, Cowork): "הגדר את Google Web Operations"
48
+
49
+ **Claude Desktop זה הוסט אחר.** הוא לא קורא את הסקילים מהדיסק. `gwo install desktop` (או `gwo install` כשהאפליקציה מותקנת) רושם את ה‑MCP ב‑`claude_desktop_config.json`, ואורז את הסקילים כקובצי zip ב‑`~/.gwo/skills-for-claude-ai/`. אותם מעלים פעם אחת ב‑claude.ai: Settings > Capabilities > Skills > Upload, ומאז הם מסתנכרנים לכל המחשבים של אותו חשבון. בלי ההעלאה, ה‑MCP עובד אבל אין סקילים, והסוכן יעבוד לפי תיאורי הכלים בלבד.
33
50
 
34
51
  הסוכן מוביל: חיבור לגוגל (לתת לו את הנתיב לקובץ ה‑JSON של לקוח ה‑OAuth, לא את התוכן), הסכמה בדפדפן עם חשבון התפעול, רישום אתר ראשון, ורשימת ההרשאות שהלקוח צריך לתת.
35
52
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-web-operations",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Google Web Operations: agent-run SEO and GEO operations for client websites. One core, one MCP server, two hosts (Claude Code, Codex).",
5
5
  "author": {
6
6
  "name": "AI Guru"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-web-operations",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Google Web Operations: agent-run SEO and GEO operations for client websites. One core, one MCP server, two hosts (Claude Code, Codex).",
5
5
  "author": {
6
6
  "name": "AI Guru"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiguru/google-web-operations",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Google Web Operations: agent-run SEO and GEO operations for client websites. One core, one MCP server, two hosts (Claude Code, Codex).",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -24,9 +24,9 @@ function siteSetup(s) {
24
24
  const missing = [];
25
25
  const gsc = s.google.search_console?.property ?? null, ga4 = s.google.ga4?.property_id ?? null, gtm = s.google.tag_manager?.public_id ?? null;
26
26
  if (!gsc)
27
- missing.push('Search Console property (grant access, then google_discover_apply)');
27
+ missing.push('Search Console property (the client grants access, then discover the properties)');
28
28
  if (!ga4)
29
- missing.push('GA4 property id (grant access, then google_discover_apply)');
29
+ missing.push('GA4 property id (the client grants access, then discover the properties)');
30
30
  if (!gtm)
31
31
  missing.push('Tag Manager container (none registered; fine if the site has no container)');
32
32
  const adapter = s.write?.adapter ?? 'none';
@@ -62,22 +62,22 @@ export function setupStatus() {
62
62
  const sites = listSites({ includeDisabled: true }).map(siteSetup);
63
63
  const next = [];
64
64
  if (!o.clientFile)
65
- next.push('Create a "Desktop app" OAuth client in Google Cloud (project "Google Web Operations"), download its JSON, then google_setup_oauth_client with the downloaded file path.');
65
+ next.push('Connect Google: create a "Desktop app" OAuth client in Google Cloud (one project for all sites), download its JSON, then in the chat "/gwo-setup" and give it the file path (CLI: save it as secrets/oauth-client.json under the state directory).');
66
66
  else if (!o.authorised)
67
- next.push('google_auth_login: the browser opens for consent with the operations account.');
67
+ next.push('Authorise the operations Google account: in the chat "/gwo-setup" (CLI: gwo auth login). A browser opens for consent.');
68
68
  else {
69
69
  if (!writeScopes.ga4_write)
70
- next.push('Optional: google_auth_login with scopes ["ga4_write"] before creating key events.');
70
+ next.push('Optional, before creating key events: add the ga4_write scope (chat: ask for it in /gwo-setup; CLI: gwo auth login --scope ga4_write).');
71
71
  if (!writeScopes.gtm_edit || !writeScopes.gtm_publish)
72
- next.push('Optional: google_auth_login with scopes ["gtm_edit", "gtm_publish"] before staging Tag Manager changes.');
72
+ next.push('Optional, before Tag Manager changes: add the gtm_edit and gtm_publish scopes (CLI: gwo auth login --scope gtm_edit --scope gtm_publish).');
73
73
  }
74
74
  if (!sites.length)
75
- next.push('ops_add_site for the first site.');
75
+ next.push('Register the first site: in the chat "/gwo-add-site https://example.co.il" (CLI: gwo add-site <url> --name "...").');
76
76
  if (o.authorised && sites.some((s) => !s.searchConsole || !s.ga4PropertyId))
77
- next.push('google_discover_apply once the client has added the operations account to Search Console and GA4.');
77
+ next.push('Once the client has added the operations account to Search Console and GA4: discover the properties (chat: "discover the Google properties"; CLI: gwo discover --apply).');
78
78
  for (const s of sites)
79
79
  for (const m of s.missing)
80
- if (!/discover_apply|none registered/.test(m))
80
+ if (!/discover the properties|none registered/.test(m))
81
81
  next.push(`${s.id}: ${m}`);
82
82
  if (!env.RESEND_API_KEY)
83
83
  next.push('Optional: RESEND_API_KEY, GWO_REPORT_FROM, GWO_REPORT_TO in .env for emailed worker reports.');
@@ -4,12 +4,18 @@
4
4
  * by the chat when the operator asks "is everything set up?".
5
5
  */
6
6
  import { setupStatus } from "./admin.js";
7
+ import { hostname, userInfo } from 'node:os';
7
8
  import { HOME_DIR, IS_CHECKOUT, ROOT } from "./paths.js";
8
9
  const OK = ' [ok] ', MISSING = ' [--] ', OPT = ' [..] ';
9
10
  export function renderDoctor(s = setupStatus()) {
10
11
  const out = [];
11
12
  const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
12
- out.push('Google Web Operations: doctor', '');
13
+ let who = '';
14
+ try {
15
+ who = `${userInfo().username}@${hostname()}`;
16
+ }
17
+ catch { /* cosmetic */ }
18
+ out.push(`Google Web Operations: doctor${who ? ` (${who})` : ''}`, '');
13
19
  out.push(`${major > 22 || (major === 22 && minor >= 18) ? OK : MISSING}Node ${process.versions.node} (needs 22.18+)`);
14
20
  out.push(`${OK}package ${ROOT}`);
15
21
  out.push(`${OK}state ${IS_CHECKOUT ? 'next to the code (repository checkout)' : HOME_DIR}`);
package/tools/install.js CHANGED
@@ -2,12 +2,12 @@
2
2
  /**
3
3
  * Install or remove the hosts' side of Google Web Operations on this machine.
4
4
  *
5
- * gwo install [claude|codex|all] [--print]
6
- * gwo uninstall [claude|codex|all]
5
+ * gwo install [claude|codex|desktop|all] [--print]
6
+ * gwo uninstall [claude|codex|desktop|all]
7
7
  *
8
8
  * Claude Code: the MCP server goes into ~/.claude.json (user scope), skills
9
9
  * into ~/.claude/skills/gwo-*, slash commands into
10
- * ~/.claude/commands/gwo-*.md.
10
+ * ~/.claude/commands/gwo-*.md (both show under /gwo-...).
11
11
  * Codex: the MCP server goes into ~/.codex/config.toml, skills into
12
12
  * ~/.codex/skills/gwo-*.
13
13
  * Both: the operator state directory (GWO_HOME, default ~/.gwo) is
@@ -17,15 +17,20 @@
17
17
  * Only gwo-* entries are ever written or removed; nothing else in those
18
18
  * directories or files is touched. Idempotent: run it again after an upgrade.
19
19
  */
20
+ import { execSync } from 'node:child_process';
20
21
  import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, copyFileSync } from 'node:fs';
21
22
  import { homedir } from 'node:os';
22
23
  import path from 'node:path';
23
24
  import { HOME_DIR, HOSTS_DIR, IS_CHECKOUT, ROOT } from "../packages/core/src/paths.js";
24
25
  const argv = process.argv.slice(2);
25
26
  const uninstall = argv.includes('uninstall');
26
- const targetArg = argv.find((a) => ['claude', 'codex', 'all'].includes(a)) ?? 'all';
27
+ const targetArg = argv.find((a) => ['claude', 'codex', 'desktop', 'all'].includes(a)) ?? 'all';
27
28
  const print = argv.includes('--print') || argv.includes('--dry-run');
28
- const targets = targetArg === 'all' ? ['claude', 'codex'] : [targetArg];
29
+ /** Claude Desktop (Claude for Windows/Mac, Cowork) keeps its MCP servers in its own config file. */
30
+ const DESKTOP_DIR = process.platform === 'win32' ? path.join(process.env['APPDATA'] ?? path.join(homedir(), 'AppData', 'Roaming'), 'Claude')
31
+ : process.platform === 'darwin' ? path.join(homedir(), 'Library', 'Application Support', 'Claude') : path.join(homedir(), '.config', 'Claude');
32
+ // "all" covers the desktop app only where it is installed, so a plain `gwo install` never invents its config.
33
+ const targets = targetArg === 'all' ? ['claude', 'codex', ...(existsSync(DESKTOP_DIR) ? ['desktop'] : [])] : [targetArg];
29
34
  const SERVER = 'google-web-operations';
30
35
  const toPosix = (p) => p.replace(/\\/g, '/');
31
36
  const node = process.execPath;
@@ -86,6 +91,45 @@ function claude() {
86
91
  copyFileSync(path.join(cmdSrc, n), path.join(cmdDst, n));
87
92
  }
88
93
  }
94
+ // ---------------------------------------------------------------- Claude Desktop (Claude for Windows / Mac, Cowork)
95
+ /**
96
+ * The desktop app reads MCP servers from claude_desktop_config.json. Skills
97
+ * cannot be installed from a local file: they are uploaded once in claude.ai
98
+ * (Settings > Capabilities > Skills) and sync down. So the skills are packed
99
+ * as one zip per skill under <state>/skills-for-claude-ai/ for that upload.
100
+ */
101
+ async function desktop() {
102
+ const cfg = path.join(DESKTOP_DIR, 'claude_desktop_config.json');
103
+ const skillsSrc = path.join(HOSTS_DIR, 'claude', 'skills');
104
+ const skills = gwoDirs(skillsSrc);
105
+ const zipDir = path.join(HOME_DIR, 'skills-for-claude-ai');
106
+ log(`\nClaude Desktop (Claude for Windows / Mac, Cowork)`);
107
+ log(` MCP server "${SERVER}" ${uninstall ? 'removed from' : 'in'} ${cfg}`);
108
+ log(` ${skills.length} skills packed as zips -> ${zipDir} (upload once in claude.ai: Settings > Capabilities > Skills)`);
109
+ if (print)
110
+ return;
111
+ mkdirSync(DESKTOP_DIR, { recursive: true });
112
+ let json = {};
113
+ if (existsSync(cfg))
114
+ json = JSON.parse(readFileSync(cfg, 'utf8'));
115
+ const servers = (json['mcpServers'] ?? {});
116
+ if (uninstall)
117
+ delete servers[SERVER];
118
+ else
119
+ servers[SERVER] = { command: node, args: [server] };
120
+ json['mcpServers'] = servers;
121
+ writeFileSync(cfg, JSON.stringify(json, null, 2));
122
+ rmSync(zipDir, { recursive: true, force: true });
123
+ if (!uninstall) {
124
+ const { default: JSZip } = await import('jszip');
125
+ mkdirSync(zipDir, { recursive: true });
126
+ for (const n of skills) {
127
+ const zip = new JSZip();
128
+ zip.file(`${n}/SKILL.md`, readFileSync(path.join(skillsSrc, n, 'SKILL.md')));
129
+ writeFileSync(path.join(zipDir, `${n}.zip`), await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' }));
130
+ }
131
+ }
132
+ }
89
133
  // ---------------------------------------------------------------- Codex
90
134
  function codex() {
91
135
  const cfg = path.join(CODEX_HOME, 'config.toml');
@@ -119,12 +163,37 @@ function codex() {
119
163
  log(`Google Web Operations ${uninstall ? 'uninstall' : 'install'}${print ? ' (print only)' : ''}`);
120
164
  log(`package: ${ROOT}`);
121
165
  log(`node: ${node}`);
166
+ // A package under some project's node_modules is a fragile place to point the
167
+ // hosts at: it disappears with the folder or the next npm install there.
168
+ if (!uninstall && !IS_CHECKOUT && /[\\/]node_modules[\\/]/.test(ROOT) && !argv.includes('--allow-local')) {
169
+ let globalRoot = '';
170
+ try {
171
+ globalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
172
+ }
173
+ catch { /* npm not on PATH */ }
174
+ const isGlobal = globalRoot && path.resolve(ROOT).toLowerCase().startsWith(path.resolve(globalRoot).toLowerCase());
175
+ if (!isGlobal) {
176
+ console.error([
177
+ '',
178
+ 'This copy of the package is installed locally (inside a project\'s node_modules), so the hosts would be',
179
+ 'pointed at a path that vanishes with the folder or the next npm install there. Install it globally instead:',
180
+ '',
181
+ ' npm i -g @aiguru/google-web-operations',
182
+ ' gwo install',
183
+ '',
184
+ 'To register this local copy anyway (a deliberate, pinned setup): gwo install --allow-local',
185
+ ].join('\n'));
186
+ process.exit(2);
187
+ }
188
+ }
122
189
  if (!uninstall)
123
190
  ensureHome();
124
191
  if (targets.includes('claude'))
125
192
  claude();
126
193
  if (targets.includes('codex'))
127
194
  codex();
195
+ if (targets.includes('desktop'))
196
+ await desktop();
128
197
  if (print)
129
198
  log('\nNothing changed. Run without --print to apply.');
130
199
  else if (uninstall)
@@ -135,8 +204,9 @@ else
135
204
  'Done.',
136
205
  '',
137
206
  '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".',
207
+ ' 1. Restart Claude Code, Codex or the Claude desktop app so they pick up the server and skills.',
208
+ ' 2. Claude Code: type /gwo-setup. Codex or Claude desktop: say "set up Google Web Operations".',
209
+ ...(targets.includes('desktop') ? [' Claude desktop: first upload the skill zips once at claude.ai > Settings > Capabilities > Skills.'] : []),
140
210
  ' The assistant connects Google, registers the first site and tells you what the client must grant.',
141
211
  ' 3. Any time: gwo doctor (what is configured, what is missing, the next step)',
142
212
  '',