@adgine/geo-cli 1.2.8

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 (115) hide show
  1. package/README.md +93 -0
  2. package/bin/adgine-geo.js +224 -0
  3. package/package.json +38 -0
  4. package/skills/.env.example +11 -0
  5. package/skills/README.md +247 -0
  6. package/skills/SKILL.md +117 -0
  7. package/skills/VERSION +1 -0
  8. package/skills/adgine-geo-aiagent/SKILL.md +200 -0
  9. package/skills/adgine-geo-aiagent/scripts/_client.py +341 -0
  10. package/skills/adgine-geo-aiagent/scripts/bot_traffic.py +278 -0
  11. package/skills/adgine-geo-aiagent/scripts/human_traffic.py +357 -0
  12. package/skills/adgine-geo-aiagent/scripts/page_analytics.py +317 -0
  13. package/skills/adgine-geo-aiagent/scripts/page_detail.py +273 -0
  14. package/skills/adgine-geo-analytics/API.md +89 -0
  15. package/skills/adgine-geo-analytics/SKILL.md +191 -0
  16. package/skills/adgine-geo-analytics/scripts/_client.py +341 -0
  17. package/skills/adgine-geo-analytics/scripts/get_overview.py +129 -0
  18. package/skills/adgine-geo-billing/SKILL.md +118 -0
  19. package/skills/adgine-geo-billing/scripts/_client.py +341 -0
  20. package/skills/adgine-geo-billing/scripts/get_credits.py +51 -0
  21. package/skills/adgine-geo-billing/scripts/get_credits_pricing.py +52 -0
  22. package/skills/adgine-geo-billing/scripts/get_subscription.py +48 -0
  23. package/skills/adgine-geo-billing/scripts/list_plans.py +66 -0
  24. package/skills/adgine-geo-brand/SKILL.md +193 -0
  25. package/skills/adgine-geo-brand/WORKFLOW.md +66 -0
  26. package/skills/adgine-geo-brand/scripts/_client.py +341 -0
  27. package/skills/adgine-geo-brand/scripts/generate_brand.py +95 -0
  28. package/skills/adgine-geo-brand/scripts/get_brand.py +67 -0
  29. package/skills/adgine-geo-brand/scripts/list_jobs.py +153 -0
  30. package/skills/adgine-geo-brand/scripts/update_brand.py +61 -0
  31. package/skills/adgine-geo-citation/SKILL.md +220 -0
  32. package/skills/adgine-geo-citation/scripts/_client.py +341 -0
  33. package/skills/adgine-geo-citation/scripts/create_tests.py +62 -0
  34. package/skills/adgine-geo-citation/scripts/get_aggregate.py +108 -0
  35. package/skills/adgine-geo-citation/scripts/get_results.py +179 -0
  36. package/skills/adgine-geo-content/SKILL.md +224 -0
  37. package/skills/adgine-geo-content/WORKFLOW.md +100 -0
  38. package/skills/adgine-geo-content/scripts/_client.py +341 -0
  39. package/skills/adgine-geo-content/scripts/generate_article.py +108 -0
  40. package/skills/adgine-geo-content/scripts/generate_outline.py +113 -0
  41. package/skills/adgine-geo-content/scripts/generate_titles.py +51 -0
  42. package/skills/adgine-geo-content/scripts/list_content.py +65 -0
  43. package/skills/adgine-geo-content/scripts/manage_content.py +90 -0
  44. package/skills/adgine-geo-content/scripts/manage_jobs.py +184 -0
  45. package/skills/adgine-geo-dashboard/SKILL.md +181 -0
  46. package/skills/adgine-geo-dashboard/scripts/_client.py +341 -0
  47. package/skills/adgine-geo-dashboard/scripts/check_integrations.py +139 -0
  48. package/skills/adgine-geo-dashboard/scripts/get_overview.py +155 -0
  49. package/skills/adgine-geo-dashboard/scripts/get_visibility_overview.py +93 -0
  50. package/skills/adgine-geo-docs/SKILL.md +60 -0
  51. package/skills/adgine-geo-domains/SKILL.md +223 -0
  52. package/skills/adgine-geo-domains/scripts/_client.py +341 -0
  53. package/skills/adgine-geo-domains/scripts/get_domain.py +75 -0
  54. package/skills/adgine-geo-domains/scripts/list_domains.py +66 -0
  55. package/skills/adgine-geo-domains/scripts/search_domains.py +85 -0
  56. package/skills/adgine-geo-integrations/SKILL.md +188 -0
  57. package/skills/adgine-geo-integrations/scripts/_client.py +341 -0
  58. package/skills/adgine-geo-integrations/scripts/cloudflare_connect.py +155 -0
  59. package/skills/adgine-geo-integrations/scripts/cloudflare_worker.py +212 -0
  60. package/skills/adgine-geo-integrations/scripts/ga4_data.py +192 -0
  61. package/skills/adgine-geo-integrations/scripts/ga4_setup.py +120 -0
  62. package/skills/adgine-geo-opportunities/SKILL.md +162 -0
  63. package/skills/adgine-geo-opportunities/scripts/_client.py +341 -0
  64. package/skills/adgine-geo-opportunities/scripts/get_opportunity.py +90 -0
  65. package/skills/adgine-geo-opportunities/scripts/list_opportunities.py +92 -0
  66. package/skills/adgine-geo-performance/SKILL.md +177 -0
  67. package/skills/adgine-geo-performance/scripts/_client.py +341 -0
  68. package/skills/adgine-geo-performance/scripts/analyze_page.py +100 -0
  69. package/skills/adgine-geo-projects/SKILL.md +279 -0
  70. package/skills/adgine-geo-projects/scripts/_client.py +341 -0
  71. package/skills/adgine-geo-projects/scripts/check_auth.py +45 -0
  72. package/skills/adgine-geo-projects/scripts/list_projects.py +47 -0
  73. package/skills/adgine-geo-projects/scripts/manage_competitors.py +122 -0
  74. package/skills/adgine-geo-projects/scripts/manage_project.py +88 -0
  75. package/skills/adgine-geo-saas/SKILL.md +144 -0
  76. package/skills/adgine-geo-saas/scripts/_client.py +341 -0
  77. package/skills/adgine-geo-saas/scripts/check_domain.py +43 -0
  78. package/skills/adgine-geo-saas/scripts/create_website.py +56 -0
  79. package/skills/adgine-geo-saas/scripts/get_task.py +94 -0
  80. package/skills/adgine-geo-site-audit/README.md +281 -0
  81. package/skills/adgine-geo-site-audit/SKILL.md +449 -0
  82. package/skills/adgine-geo-site-audit/requirements.txt +5 -0
  83. package/skills/adgine-geo-site-audit/scripts/_client.py +341 -0
  84. package/skills/adgine-geo-site-audit/scripts/_geo_notice.py +55 -0
  85. package/skills/adgine-geo-site-audit/scripts/export_skill_package.py +197 -0
  86. package/skills/adgine-geo-site-audit/scripts/geo_collect.py +2443 -0
  87. package/skills/adgine-geo-site-audit/scripts/geo_score.py +490 -0
  88. package/skills/adgine-geo-site-audit/scripts/geo_timing.py +399 -0
  89. package/skills/adgine-geo-site-audit/scripts/geo_visibility.py +718 -0
  90. package/skills/adgine-geo-site-audit/scripts/render_report_pdf.py +1502 -0
  91. package/skills/adgine-geo-topics/SKILL.md +232 -0
  92. package/skills/adgine-geo-topics/scripts/_client.py +341 -0
  93. package/skills/adgine-geo-topics/scripts/generate_prompts.py +140 -0
  94. package/skills/adgine-geo-topics/scripts/manage_prompts.py +153 -0
  95. package/skills/adgine-geo-topics/scripts/manage_topics.py +145 -0
  96. package/skills/adgine-geo-visibility/SKILL.md +168 -0
  97. package/skills/adgine-geo-visibility/scripts/_client.py +341 -0
  98. package/skills/adgine-geo-visibility/scripts/get_execution.py +151 -0
  99. package/skills/adgine-geo-visibility/scripts/get_matrix.py +111 -0
  100. package/skills/adgine-geo-visibility/scripts/get_prompt_metrics.py +104 -0
  101. package/skills/adgine-geo-visibility/scripts/get_topic_metrics.py +201 -0
  102. package/skills/adgine-geo-visibility/scripts/get_visibility.py +166 -0
  103. package/skills/adgine-geo-wordpress/SKILL.md +151 -0
  104. package/skills/adgine-geo-wordpress/scripts/_client.py +341 -0
  105. package/skills/adgine-geo-wordpress/scripts/list_publishable.py +62 -0
  106. package/skills/adgine-geo-wordpress/scripts/manage_credentials.py +139 -0
  107. package/skills/adgine-geo-wordpress/scripts/manage_publishes.py +108 -0
  108. package/skills/adgine-geo-wordpress/scripts/publish.py +79 -0
  109. package/skills/scripts/check_version.py +290 -0
  110. package/skills/setup.py +248 -0
  111. package/src/config.js +126 -0
  112. package/src/doctor.js +137 -0
  113. package/src/install.js +90 -0
  114. package/src/manifest.js +99 -0
  115. package/src/runner.js +159 -0
package/src/config.js ADDED
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Config manager for adgine-geo CLI.
5
+ *
6
+ * The GEO API key and optional base URL live in ~/.adgine/config.json
7
+ * (0600). They are NOT written into the package (node_modules is often
8
+ * read-only for global installs) and NOT written to shell rc files.
9
+ *
10
+ * Resolution precedence (highest first):
11
+ * 1. process.env.GEO_API_KEY / GEO_API_BASE_URL
12
+ * 2. ~/.adgine/config.json
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const os = require('os');
17
+ const path = require('path');
18
+
19
+ const CONFIG_DIR = path.join(os.homedir(), '.adgine');
20
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
21
+
22
+ const DEFAULT_BASE_URL = 'https://platform.adgine.ai';
23
+
24
+ function configPath() {
25
+ return CONFIG_PATH;
26
+ }
27
+
28
+ function readConfig() {
29
+ try {
30
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
31
+ const parsed = JSON.parse(raw);
32
+ return parsed && typeof parsed === 'object' ? parsed : {};
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ function writeConfig(config) {
39
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
40
+ const json = JSON.stringify(config, null, 2) + '\n';
41
+ // Write then chmod so the key file is never briefly world-readable.
42
+ fs.writeFileSync(CONFIG_PATH, json, { mode: 0o600 });
43
+ try {
44
+ fs.chmodSync(CONFIG_PATH, 0o600);
45
+ } catch {
46
+ /* best effort on platforms without chmod */
47
+ }
48
+ }
49
+
50
+ function setKey(key) {
51
+ const trimmed = (key || '').trim();
52
+ if (!trimmed) {
53
+ throw new Error('API key is empty.');
54
+ }
55
+ const config = readConfig();
56
+ config.apiKey = trimmed;
57
+ writeConfig(config);
58
+ return trimmed;
59
+ }
60
+
61
+ function setBaseUrl(url) {
62
+ const config = readConfig();
63
+ const trimmed = (url || '').trim().replace(/\/+$/, '');
64
+ if (trimmed) {
65
+ config.baseUrl = trimmed;
66
+ } else {
67
+ delete config.baseUrl;
68
+ }
69
+ writeConfig(config);
70
+ return trimmed;
71
+ }
72
+
73
+ /**
74
+ * Resolve the effective key. Returns undefined if none configured.
75
+ */
76
+ function getKey() {
77
+ if (process.env.GEO_API_KEY && process.env.GEO_API_KEY.trim()) {
78
+ return process.env.GEO_API_KEY.trim();
79
+ }
80
+ const config = readConfig();
81
+ return config.apiKey || undefined;
82
+ }
83
+
84
+ /**
85
+ * Resolve the effective base URL. Env > config > default.
86
+ */
87
+ function getBaseUrl() {
88
+ if (process.env.GEO_API_BASE_URL && process.env.GEO_API_BASE_URL.trim()) {
89
+ return process.env.GEO_API_BASE_URL.trim().replace(/\/+$/, '');
90
+ }
91
+ const config = readConfig();
92
+ return (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
93
+ }
94
+
95
+ /**
96
+ * Where the effective key came from, for `config get` / doctor messaging.
97
+ */
98
+ function keySource() {
99
+ if (process.env.GEO_API_KEY && process.env.GEO_API_KEY.trim()) {
100
+ return 'env (GEO_API_KEY)';
101
+ }
102
+ if (readConfig().apiKey) {
103
+ return CONFIG_PATH;
104
+ }
105
+ return null;
106
+ }
107
+
108
+ function maskKey(key) {
109
+ if (!key) return '(not set)';
110
+ if (key.length <= 12) return key.slice(0, 3) + '…';
111
+ return key.slice(0, 10) + '…' + key.slice(-4);
112
+ }
113
+
114
+ module.exports = {
115
+ CONFIG_PATH,
116
+ DEFAULT_BASE_URL,
117
+ configPath,
118
+ readConfig,
119
+ writeConfig,
120
+ setKey,
121
+ setBaseUrl,
122
+ getKey,
123
+ getBaseUrl,
124
+ keySource,
125
+ maskKey,
126
+ };
package/src/doctor.js ADDED
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Environment health check for adgine-geo.
5
+ *
6
+ * Verifies: Python 3 present, API key configured + valid, and (optionally
7
+ * installs) the extra pip dependencies the site-audit skill needs.
8
+ */
9
+
10
+ const { spawnSync } = require('child_process');
11
+ const path = require('path');
12
+ const config = require('./config');
13
+ const runner = require('./runner');
14
+
15
+ const OK = '✔';
16
+ const BAD = '✖';
17
+ const WARN = '!';
18
+
19
+ const AUDIT_REQS = path.join(
20
+ runner.SKILLS_DIR,
21
+ 'adgine-geo-site-audit',
22
+ 'requirements.txt'
23
+ );
24
+ const AUDIT_MODULES = ['curl_cffi', 'bs4', 'lxml', 'markdown', 'reportlab'];
25
+
26
+ function checkPython() {
27
+ const py = runner.findPython();
28
+ if (!py) return { ok: false, line: `${BAD} Python 3: not found on PATH` };
29
+ const res = spawnSync(py, ['--version'], { encoding: 'utf8' });
30
+ const ver = ((res.stdout || '') + (res.stderr || '')).trim();
31
+ return { ok: true, py, line: `${OK} Python 3: ${ver} (${py})` };
32
+ }
33
+
34
+ async function checkKey() {
35
+ const key = config.getKey();
36
+ if (!key) {
37
+ return {
38
+ ok: false,
39
+ line:
40
+ `${BAD} GEO_API_KEY: not set\n` +
41
+ ` Fix: adgine-geo config set-key geo_sk_live_xxx\n` +
42
+ ` Get a key at https://platform.adgine.ai`,
43
+ };
44
+ }
45
+ const source = config.keySource();
46
+ const base = config.getBaseUrl();
47
+ // Verify against the same endpoint setup.py uses.
48
+ try {
49
+ const res = await fetch(`${base}/api/projects?limit=1`, {
50
+ method: 'GET',
51
+ headers: {
52
+ Authorization: `Bearer ${key}`,
53
+ Accept: 'application/json',
54
+ 'User-Agent': 'adgine-geo-cli-doctor/1.0',
55
+ },
56
+ signal: AbortSignal.timeout(10000),
57
+ });
58
+ if (res.status === 401) {
59
+ return {
60
+ ok: false,
61
+ line:
62
+ `${BAD} GEO_API_KEY: ${config.maskKey(key)} — 401 Unauthorized (invalid or revoked)\n` +
63
+ ` Generate a new key at https://platform.adgine.ai`,
64
+ };
65
+ }
66
+ if (res.ok) {
67
+ return {
68
+ ok: true,
69
+ line: `${OK} GEO_API_KEY: ${config.maskKey(key)} — valid (from ${source})`,
70
+ };
71
+ }
72
+ return {
73
+ ok: true,
74
+ line: `${WARN} GEO_API_KEY: ${config.maskKey(key)} — HTTP ${res.status} (non-auth; treating as ok)`,
75
+ };
76
+ } catch (err) {
77
+ return {
78
+ ok: true,
79
+ line: `${WARN} GEO_API_KEY: ${config.maskKey(key)} — verification skipped (${err.message})`,
80
+ };
81
+ }
82
+ }
83
+
84
+ function checkAuditDeps() {
85
+ const py = runner.findPython();
86
+ if (!py) return { ok: false, line: `${WARN} site-audit deps: skipped (no Python)` };
87
+ const res = spawnSync(py, ['-c', `import ${AUDIT_MODULES.join(', ')}`], {
88
+ encoding: 'utf8',
89
+ });
90
+ if (res.status === 0) {
91
+ return { ok: true, line: `${OK} site-audit deps: installed` };
92
+ }
93
+ return {
94
+ ok: true, // optional — not a hard failure
95
+ line:
96
+ `${WARN} site-audit deps: missing (${AUDIT_MODULES.join(', ')})\n` +
97
+ ` Only needed for the site-audit skill. Install with:\n` +
98
+ ` adgine-geo doctor --install-audit-deps`,
99
+ };
100
+ }
101
+
102
+ function installAuditDeps() {
103
+ const py = runner.findPython();
104
+ if (!py) {
105
+ process.stderr.write('ERROR: Python 3 not found; cannot install deps.\n');
106
+ return 69;
107
+ }
108
+ process.stdout.write(`Installing site-audit dependencies from ${AUDIT_REQS} ...\n`);
109
+ const res = spawnSync(py, ['-m', 'pip', 'install', '-r', AUDIT_REQS], {
110
+ stdio: 'inherit',
111
+ });
112
+ return res.status == null ? 1 : res.status;
113
+ }
114
+
115
+ async function run({ installAudit = false } = {}) {
116
+ if (installAudit) {
117
+ return installAuditDeps();
118
+ }
119
+ process.stdout.write('adgine-geo doctor\n\n');
120
+ const py = checkPython();
121
+ process.stdout.write(py.line + '\n');
122
+ const key = await checkKey();
123
+ process.stdout.write(key.line + '\n');
124
+ const audit = checkAuditDeps();
125
+ process.stdout.write(audit.line + '\n');
126
+ process.stdout.write('\n');
127
+
128
+ const hardFail = !py.ok || !key.ok;
129
+ if (hardFail) {
130
+ process.stdout.write('Some checks failed — see fixes above.\n');
131
+ return 1;
132
+ }
133
+ process.stdout.write('All required checks passed.\n');
134
+ return 0;
135
+ }
136
+
137
+ module.exports = { run, installAuditDeps };
package/src/install.js ADDED
@@ -0,0 +1,90 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Install the bundled skill folders into an AI agent's skills directory, so
5
+ * agents (Claude Code, Cursor, etc.) can load them as native skills and run the
6
+ * Python scripts themselves — the same layout they'd get from a git clone.
7
+ *
8
+ * We copy the skill folders plus the supporting files (scripts/, VERSION,
9
+ * SKILL.md, .env.example) preserving the layout _client.py expects (repo root =
10
+ * the install target). The GEO_API_KEY is written to a .env at the target root
11
+ * so the agent-run scripts pick it up, matching the existing clone workflow.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
17
+ const config = require('./config');
18
+ const { SKILLS_DIR } = require('./runner');
19
+
20
+ /** Common agent skill directories, offered as install targets. */
21
+ function defaultTargets() {
22
+ const home = os.homedir();
23
+ return [
24
+ { label: 'Claude Code (user)', dir: path.join(home, '.claude', 'skills', 'adgine-geo') },
25
+ { label: 'Current project (.claude)', dir: path.join(process.cwd(), '.claude', 'skills', 'adgine-geo') },
26
+ ];
27
+ }
28
+
29
+ function copyRecursive(src, dest) {
30
+ const stat = fs.statSync(src);
31
+ if (stat.isDirectory()) {
32
+ if (path.basename(src) === '__pycache__') return;
33
+ fs.mkdirSync(dest, { recursive: true });
34
+ for (const entry of fs.readdirSync(src)) {
35
+ copyRecursive(path.join(src, entry), path.join(dest, entry));
36
+ }
37
+ } else if (stat.isFile()) {
38
+ if (src.endsWith('.pyc')) return;
39
+ fs.copyFileSync(src, dest);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Copy the whole skills payload into `targetDir`. Returns the target path.
45
+ */
46
+ function installTo(targetDir) {
47
+ const target = path.resolve(targetDir);
48
+ fs.mkdirSync(target, { recursive: true });
49
+ for (const entry of fs.readdirSync(SKILLS_DIR)) {
50
+ copyRecursive(path.join(SKILLS_DIR, entry), path.join(target, entry));
51
+ }
52
+ writeEnv(target);
53
+ return target;
54
+ }
55
+
56
+ /**
57
+ * Write GEO_API_KEY into <target>/.env if a key is configured. Preserves any
58
+ * existing lines. No-op (with a note) when no key is available.
59
+ */
60
+ function writeEnv(target) {
61
+ const key = config.getKey();
62
+ const envPath = path.join(target, '.env');
63
+ if (!key) return { written: false, envPath };
64
+
65
+ let lines = [];
66
+ if (fs.existsSync(envPath)) {
67
+ lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/);
68
+ }
69
+ let found = false;
70
+ lines = lines.map((l) => {
71
+ if (l.startsWith('GEO_API_KEY=')) {
72
+ found = true;
73
+ return `GEO_API_KEY=${key}`;
74
+ }
75
+ return l;
76
+ });
77
+ if (!found) {
78
+ if (lines.length && lines[lines.length - 1] === '') lines.pop();
79
+ lines.push(`GEO_API_KEY=${key}`);
80
+ }
81
+ fs.writeFileSync(envPath, lines.join('\n').replace(/\n*$/, '\n'), { mode: 0o600 });
82
+ try {
83
+ fs.chmodSync(envPath, 0o600);
84
+ } catch {
85
+ /* best effort */
86
+ }
87
+ return { written: true, envPath };
88
+ }
89
+
90
+ module.exports = { defaultTargets, installTo, writeEnv };
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Build the command tree by scanning the bundled skills payload.
5
+ *
6
+ * A "skill" is a folder `adgine-geo-<name>`. Its "actions" are the Python
7
+ * scripts under `scripts/` whose filename does not start with `_` (which are
8
+ * shared helpers like `_client.py` / `_geo_notice.py`).
9
+ *
10
+ * A short description per skill is parsed from the SKILL.md YAML frontmatter.
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const { SKILLS_DIR } = require('./runner');
16
+
17
+ /** Extract the frontmatter `description:` (inline or `>`/`|` folded block). */
18
+ function parseDescription(skillMdPath) {
19
+ let text;
20
+ try {
21
+ text = fs.readFileSync(skillMdPath, 'utf8');
22
+ } catch {
23
+ return '';
24
+ }
25
+ const lines = text.split(/\r?\n/);
26
+ if (lines[0].trim() !== '---') return '';
27
+ let i = 1;
28
+ for (; i < lines.length; i++) {
29
+ if (lines[i].trim() === '---') return '';
30
+ const m = lines[i].match(/^description:\s*(.*)$/);
31
+ if (!m) continue;
32
+ let value = m[1].trim();
33
+ if (value === '>' || value === '|' || value === '>-' || value === '|-') {
34
+ // Folded block: collect subsequent indented lines.
35
+ const parts = [];
36
+ for (let j = i + 1; j < lines.length; j++) {
37
+ if (lines[j].trim() === '---') break;
38
+ if (/^\S/.test(lines[j])) break; // next top-level key
39
+ parts.push(lines[j].trim());
40
+ }
41
+ value = parts.join(' ').trim();
42
+ }
43
+ return firstSentence(value);
44
+ }
45
+ return '';
46
+ }
47
+
48
+ function firstSentence(s) {
49
+ if (!s) return '';
50
+ // Cut at first sentence boundary (English period or Chinese 。), else truncate.
51
+ const m = s.match(/^(.*?[.。])(\s|$)/);
52
+ let out = m ? m[1] : s;
53
+ if (out.length > 140) out = out.slice(0, 137) + '…';
54
+ return out.trim();
55
+ }
56
+
57
+ function listActions(scriptsDir) {
58
+ let entries;
59
+ try {
60
+ entries = fs.readdirSync(scriptsDir);
61
+ } catch {
62
+ return [];
63
+ }
64
+ return entries
65
+ .filter((f) => f.endsWith('.py') && !f.startsWith('_'))
66
+ .map((f) => f.slice(0, -3))
67
+ .sort();
68
+ }
69
+
70
+ /**
71
+ * Return the full manifest: array of { skill, folder, description, actions }.
72
+ */
73
+ function buildManifest() {
74
+ let dirs;
75
+ try {
76
+ dirs = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
77
+ } catch {
78
+ return [];
79
+ }
80
+ const skills = [];
81
+ for (const d of dirs) {
82
+ if (!d.isDirectory() || !d.name.startsWith('adgine-geo-')) continue;
83
+ const folder = d.name;
84
+ const skill = folder.replace(/^adgine-geo-/, '');
85
+ const description = parseDescription(path.join(SKILLS_DIR, folder, 'SKILL.md'));
86
+ const actions = listActions(path.join(SKILLS_DIR, folder, 'scripts'));
87
+ skills.push({ skill, folder, description, actions });
88
+ }
89
+ skills.sort((a, b) => a.skill.localeCompare(b.skill));
90
+ return skills;
91
+ }
92
+
93
+ /** Look up one skill by its short name (without the adgine-geo- prefix). */
94
+ function findSkill(name) {
95
+ const normalized = name.replace(/^adgine-geo-/, '');
96
+ return buildManifest().find((s) => s.skill === normalized) || null;
97
+ }
98
+
99
+ module.exports = { buildManifest, findSkill, parseDescription };
package/src/runner.js ADDED
@@ -0,0 +1,159 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Locate a Python interpreter and the bundled skills payload, then spawn a
5
+ * skill script with the GEO_API_KEY injected into the child environment.
6
+ *
7
+ * Key design point: the Python client (_client.py) reads GEO_API_KEY from the
8
+ * environment first, so we never need to write a .env file — we just pass the
9
+ * key through in the child's env. We also set GEO_SKIP_VERSION_CHECK so stdout
10
+ * stays clean and no per-command network round-trip to GitHub happens.
11
+ */
12
+
13
+ const { spawnSync } = require('child_process');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const config = require('./config');
17
+
18
+ const SKILLS_DIR = path.resolve(__dirname, '..', 'skills');
19
+
20
+ // Skills whose scripts run without a GEO_API_KEY (site technical audit is fully
21
+ // local; docs just links the manual). Everything else needs a key.
22
+ const KEYLESS_SKILLS = new Set(['site-audit', 'docs']);
23
+
24
+ const HELP_FLAGS = new Set(['-h', '--help']);
25
+
26
+ let _cachedPython;
27
+
28
+ /**
29
+ * Find a working Python 3 interpreter. Returns the command string or null.
30
+ */
31
+ function findPython() {
32
+ if (_cachedPython !== undefined) return _cachedPython;
33
+ const candidates =
34
+ process.platform === 'win32'
35
+ ? ['python', 'python3', 'py']
36
+ : ['python3', 'python'];
37
+ for (const cmd of candidates) {
38
+ try {
39
+ const res = spawnSync(cmd, ['--version'], { encoding: 'utf8' });
40
+ if (res.status === 0) {
41
+ const out = (res.stdout || '') + (res.stderr || '');
42
+ const m = out.match(/Python (\d+)\.(\d+)/);
43
+ if (m && Number(m[1]) >= 3) {
44
+ _cachedPython = cmd;
45
+ return cmd;
46
+ }
47
+ }
48
+ } catch {
49
+ /* try next */
50
+ }
51
+ }
52
+ _cachedPython = null;
53
+ return null;
54
+ }
55
+
56
+ function skillsDir() {
57
+ return SKILLS_DIR;
58
+ }
59
+
60
+ /**
61
+ * Absolute path to a skill's script, or null if it does not exist.
62
+ * skill: e.g. "projects" (without the adgine-geo- prefix) or the full folder name.
63
+ */
64
+ function resolveScript(skill, action) {
65
+ const folder = skill.startsWith('adgine-geo-') ? skill : `adgine-geo-${skill}`;
66
+ const scriptPath = path.join(
67
+ SKILLS_DIR,
68
+ folder,
69
+ 'scripts',
70
+ `${action}.py`
71
+ );
72
+ return fs.existsSync(scriptPath) ? scriptPath : null;
73
+ }
74
+
75
+ /**
76
+ * Build the child environment: pass through the current env, inject the
77
+ * resolved key + base URL, and suppress the Python version check.
78
+ */
79
+ function buildEnv(extra = {}) {
80
+ const env = { ...process.env, GEO_SKIP_VERSION_CHECK: '1', ...extra };
81
+ const key = config.getKey();
82
+ if (key) env.GEO_API_KEY = key;
83
+ const base = config.getBaseUrl();
84
+ if (base && base !== config.DEFAULT_BASE_URL) {
85
+ env.GEO_API_BASE_URL = base;
86
+ }
87
+ return env;
88
+ }
89
+
90
+ /**
91
+ * Run a bundled Python script, inheriting stdio. Returns the exit code.
92
+ * On missing python, prints guidance and returns 69 (EX_UNAVAILABLE).
93
+ */
94
+ function runScript(scriptPath, args = [], opts = {}) {
95
+ const python = findPython();
96
+ if (!python) {
97
+ process.stderr.write(
98
+ 'ERROR: Python 3 not found on PATH.\n' +
99
+ ' adgine-geo wraps Python skills and needs Python 3.9+.\n' +
100
+ ' Install it from https://www.python.org/downloads/ and re-run.\n' +
101
+ " Then verify with: adgine-geo doctor\n"
102
+ );
103
+ return 69;
104
+ }
105
+ const res = spawnSync(python, [scriptPath, ...args], {
106
+ stdio: 'inherit',
107
+ env: buildEnv(opts.env),
108
+ cwd: opts.cwd,
109
+ });
110
+ if (res.error) {
111
+ process.stderr.write(`ERROR: failed to run python — ${res.error.message}\n`);
112
+ return 1;
113
+ }
114
+ if (res.signal) {
115
+ // e.g. SIGINT (Ctrl-C)
116
+ return 130;
117
+ }
118
+ return res.status == null ? 1 : res.status;
119
+ }
120
+
121
+ /**
122
+ * Run a skill action: adgine-geo <skill> <action> [args].
123
+ * Returns exit code; 127 if the skill/action pair does not exist.
124
+ */
125
+ function runSkillAction(skill, action, args = []) {
126
+ const normalized = skill.replace(/^adgine-geo-/, '');
127
+ const scriptPath = resolveScript(normalized, action);
128
+ if (!scriptPath) {
129
+ process.stderr.write(
130
+ `ERROR: unknown command '${normalized} ${action}'.\n` +
131
+ " Run 'adgine-geo list' to see available skills and actions.\n"
132
+ );
133
+ return 127;
134
+ }
135
+ // Give npm users the right fix when no key is configured, instead of the
136
+ // Python client's message that points inside the package dir. Skip for
137
+ // keyless skills and for --help (argparse handles that before any key check).
138
+ const isHelp = args.some((a) => HELP_FLAGS.has(a));
139
+ if (!isHelp && !KEYLESS_SKILLS.has(normalized) && !config.getKey()) {
140
+ process.stderr.write(
141
+ 'ERROR: GEO_API_KEY is not set.\n' +
142
+ ' Set it once with:\n' +
143
+ ' adgine-geo config set-key geo_sk_live_xxx\n' +
144
+ ' Get your key at https://platform.adgine.ai\n'
145
+ );
146
+ return 78; // EX_CONFIG
147
+ }
148
+ return runScript(scriptPath, args);
149
+ }
150
+
151
+ module.exports = {
152
+ SKILLS_DIR,
153
+ findPython,
154
+ skillsDir,
155
+ resolveScript,
156
+ buildEnv,
157
+ runScript,
158
+ runSkillAction,
159
+ };