social-agent-cli 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-agent-cli",
3
- "version": "4.3.0",
3
+ "version": "4.4.0",
4
4
  "description": "AI-powered social media agent - free APIs + browser automation with self-healing selectors",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,58 +1,128 @@
1
- import { execSync, spawn } from "node:child_process";
1
+ import { execSync } from "node:child_process";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  import { existsSync, mkdirSync, copyFileSync } from "node:fs";
5
5
  import { PATHS } from "../../core/config.js";
6
- import { loadMap } from "../../ai/mapper.js";
6
+ import { loadMap, saveMap } from "../../ai/mapper.js";
7
7
  import { pickChromeProfile, getSavedProfile } from "../../core/profiles.js";
8
- import type { SelectorMap } from "../../core/types.js";
8
+ import type { SelectorMap, SelectorStep } from "../../core/types.js";
9
9
 
10
10
  const CHROME_PATH = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
11
11
  const CDP_PORT = 9222;
12
12
 
13
- /**
14
- * Browser Driver - agent-browser + gerçek Chrome profili (CDP)
15
- */
16
13
  export class BrowserDriver {
17
14
  private chromeStarted = false;
18
- private profileDirName = "";
19
15
 
20
16
  constructor(private platform: string, private homeUrl: string) {}
21
17
 
22
- /**
23
- * agent-browser komutu çalıştır (CDP ile)
24
- */
18
+ // ── agent-browser komutu ──────────────────────────────────
25
19
  private cmd(command: string, timeout = 15000): string {
26
20
  try {
27
21
  return execSync(`agent-browser --cdp ${CDP_PORT} ${command}`, {
28
- encoding: "utf-8",
29
- timeout,
30
- stdio: ["pipe", "pipe", "pipe"],
22
+ encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"],
31
23
  }).trim();
32
24
  } catch (err: any) {
33
- throw new Error(err.stderr?.toString()?.trim() || err.stdout?.toString()?.trim() || err.message);
25
+ throw new Error(err.stderr?.toString()?.trim() || err.message);
34
26
  }
35
27
  }
36
28
 
37
- /**
38
- * Chrome'u gerçek profil ile CDP modunda başlat
39
- */
29
+ // ── Claude'a snapshot gönder, doğru ref'i bulsun ────────
30
+ private askClaudeForRef(snapshot: string, description: string, action: string): string | null {
31
+ try {
32
+ // Screenshot al - Claude hem görsel hem text ile analiz etsin
33
+ const ssPath = join(PATHS.screenshots, `${this.platform}_ai_ref.png`);
34
+ try { this.cmd(`screenshot "${ssPath}"`); } catch {}
35
+
36
+ const prompt = `Screenshot: ${ssPath}
37
+
38
+ Accessibility tree:
39
+ ${snapshot}
40
+
41
+ Görev: "${description}" (${action})
42
+
43
+ Bu snapshot'ta ve screenshot'ta yukarıdaki görevi yerine getirebilecek elementin ref değerini bul. Sadece ref değerini yaz, başka bir şey yazma. Örnek: e19`;
44
+
45
+ const result = execSync(
46
+ `echo '${prompt.replace(/'/g, "'\\''")}' | claude -p --model opus --effort high --max-turns 1 --tools ""`,
47
+ { encoding: "utf-8", timeout: 30000, stdio: ["pipe", "pipe", "pipe"] }
48
+ ).trim();
49
+
50
+ // ref değerini çıkar (e19, e7 gibi)
51
+ const refMatch = result.match(/\b(e\d+)\b/);
52
+ if (refMatch) {
53
+ console.log(` [ai] ${description} → @${refMatch[1]}`);
54
+ return refMatch[1];
55
+ }
56
+ } catch {}
57
+ return null;
58
+ }
59
+
60
+ // ── Snapshot al ve parse et ───────────────────────────────
61
+ private getSnapshot(): { raw: string; refs: Map<string, { ref: string; text: string; role: string }> } {
62
+ const raw = this.cmd("snapshot -i -c", 10000);
63
+ const refs = new Map<string, { ref: string; text: string; role: string }>();
64
+
65
+ // "button "Gönderi başlatın" [ref=e19]" → {ref: "e19", text: "Gönderi başlatın", role: "button"}
66
+ for (const match of raw.matchAll(/(\w+)\s+"([^"]*)"[^[]*\[ref=(\w+)\]/g)) {
67
+ refs.set(match[3], { ref: match[3], text: match[2], role: match[1] });
68
+ }
69
+ return { raw, refs };
70
+ }
71
+
72
+ // ── Snapshot'tan element bul (text match) ─────────────────
73
+ private findRef(snapshot: ReturnType<typeof this.getSnapshot>, description: string, role?: string): string | null {
74
+ // Description'dan tırnak içi text çıkar
75
+ const textMatch = description.match(/["'""\u201C\u201D]([^"'""\u201C\u201D]+)["'""\u201C\u201D]/);
76
+ const searchText = textMatch?.[1] || "";
77
+ if (!searchText) return null;
78
+
79
+ for (const [ref, info] of snapshot.refs) {
80
+ // Tam eşleşme
81
+ if (info.text === searchText) {
82
+ if (!role || info.role === role) return ref;
83
+ }
84
+ }
85
+ // Kısmi eşleşme
86
+ for (const [ref, info] of snapshot.refs) {
87
+ if (info.text.includes(searchText)) {
88
+ if (!role || info.role === role) return ref;
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+
94
+ // ── Element'in CSS selector'ını al ────────────────────────
95
+ private getSelector(ref: string): string {
96
+ try {
97
+ const result = this.cmd(`eval "
98
+ (function() {
99
+ var el = document.querySelector('[data-ref=\\"${ref}\\"]');
100
+ if (!el) {
101
+ // ref ile bulamadıysa snapshot'taki sırayla dene
102
+ return '';
103
+ }
104
+ // Selector üret
105
+ if (el.id) return '#' + el.id;
106
+ if (el.getAttribute('data-testid')) return '[data-testid=\\\"' + el.getAttribute('data-testid') + '\\\"]';
107
+ if (el.getAttribute('aria-label')) return el.tagName.toLowerCase() + '[aria-label=\\\"' + el.getAttribute('aria-label') + '\\\"]';
108
+ return '';
109
+ })()
110
+ "`, 5000);
111
+ return result.replace(/^"|"$/g, "");
112
+ } catch {
113
+ return "";
114
+ }
115
+ }
116
+
117
+ // ── Chrome başlat ─────────────────────────────────────────
40
118
  async launch(): Promise<void> {
41
119
  const profile = getSavedProfile(this.platform);
42
120
  if (!profile) throw new Error(`Profil yok. Önce: social-agent login ${this.platform}`);
43
- this.profileDirName = profile.dirName;
44
121
 
45
- // CDP'ye bağlanmayı dene
46
- try {
47
- execSync(`curl -sf http://127.0.0.1:${CDP_PORT}/json/version`, { stdio: "pipe", timeout: 2000 });
48
- return; // zaten çalışıyor
49
- } catch {}
122
+ try { execSync(`curl -sf http://127.0.0.1:${CDP_PORT}/json/version`, { stdio: "pipe", timeout: 2000 }); return; } catch {}
50
123
 
51
- // Chrome'u CDP ile başlat
52
124
  const workDir = join(PATHS.profiles, "chrome-cdp");
53
125
  mkdirSync(join(workDir, profile.dirName), { recursive: true });
54
-
55
- // Cookie'leri kopyala
56
126
  for (const f of ["Cookies", "Cookies-journal", "Login Data", "Login Data-journal", "Preferences", "Secure Preferences"]) {
57
127
  const src = join(profile.fullPath, f);
58
128
  if (existsSync(src)) { try { copyFileSync(src, join(workDir, profile.dirName, f)); } catch {} }
@@ -60,69 +130,45 @@ export class BrowserDriver {
60
130
  const ls = join(homedir(), "Library", "Application Support", "Google", "Chrome", "Local State");
61
131
  if (existsSync(ls)) { try { copyFileSync(ls, join(workDir, "Local State")); } catch {} }
62
132
 
63
- // agent-browser daemon'u kapat
64
- execSync("agent-browser close", { stdio: "pipe" }).toString();
65
-
133
+ const { spawn } = await import("node:child_process");
134
+ execSync("agent-browser close", { stdio: "pipe" }).toString().catch?.(() => {});
66
135
  const child = spawn(CHROME_PATH, [
67
- `--remote-debugging-port=${CDP_PORT}`,
68
- `--user-data-dir=${workDir}`,
69
- `--profile-directory=${profile.dirName}`,
70
- "--no-first-run", "--no-default-browser-check",
136
+ `--remote-debugging-port=${CDP_PORT}`, `--user-data-dir=${workDir}`,
137
+ `--profile-directory=${profile.dirName}`, "--no-first-run", "--no-default-browser-check",
71
138
  ], { detached: true, stdio: "ignore" });
72
139
  child.unref();
73
- this.chromeStarted = true;
74
140
 
75
- // Bağlantıyı bekle
76
141
  for (let i = 0; i < 30; i++) {
77
142
  await new Promise(r => setTimeout(r, 1000));
78
- try {
79
- execSync(`curl -sf http://127.0.0.1:${CDP_PORT}/json/version`, { stdio: "pipe", timeout: 3000 });
80
- console.log(`[${this.platform}] Chrome bağlandı`);
81
- return;
82
- } catch {}
143
+ try { execSync(`curl -sf http://127.0.0.1:${CDP_PORT}/json/version`, { stdio: "pipe", timeout: 2000 }); console.log(`[${this.platform}] Chrome bağlandı`); return; } catch {}
83
144
  }
84
- throw new Error("Chrome başlatılamadı - Chrome'u kapat ve tekrar dene");
145
+ throw new Error("Chrome başlatılamadı");
85
146
  }
86
147
 
87
148
  async close(): Promise<void> {}
88
149
 
89
- /**
90
- * Login
91
- */
92
150
  async login(): Promise<void> {
93
151
  await pickChromeProfile(this.platform);
94
152
  await this.launch();
95
153
  this.cmd(`open "${this.homeUrl}"`, 30000);
96
-
97
154
  console.log(`\n[${this.platform}] Tarayıcı açıldı: ${this.homeUrl}`);
98
155
  const { createInterface } = await import("node:readline");
99
156
  const rl = createInterface({ input: process.stdin, output: process.stdout });
100
- await new Promise<void>(resolve => {
101
- rl.question("Hesabın açıksa Enter'a bas: ", () => { rl.close(); resolve(); });
102
- });
103
- console.log(`[${this.platform}] Kayıt tamamlandı.`);
157
+ await new Promise<void>(resolve => { rl.question("Enter'a bas: ", () => { rl.close(); resolve(); }); });
104
158
  process.exit(0);
105
159
  }
106
160
 
107
- /**
108
- * Learn - ARIA snapshot al ve dosyaya yaz (Claude Code okuyup map oluşturur)
109
- */
110
- async learn(taskDescription = "yeni bir metin postu at", actionName = "post"): Promise<SelectorMap> {
161
+ async learn(taskDescription = "post at", actionName = "post"): Promise<SelectorMap> {
111
162
  await this.launch();
112
163
  this.cmd(`open "${this.homeUrl}"`, 30000);
113
164
  this.cmd("wait 3000");
114
-
115
- const screenshotPath = join(PATHS.screenshots, `${this.platform}_learn.png`);
116
- this.cmd(`screenshot "${screenshotPath}"`);
117
-
165
+ const ssPath = join(PATHS.screenshots, `${this.platform}_learn.png`);
166
+ this.cmd(`screenshot "${ssPath}"`);
118
167
  const snapshot = this.cmd("snapshot -i -c", 10000);
119
- const snapshotPath = join(PATHS.screenshots, `${this.platform}_snapshot.txt`);
120
- const { writeFileSync: writeFile } = await import("node:fs");
121
- writeFile(snapshotPath, snapshot);
122
-
123
- console.log(`Screenshot: ${screenshotPath}`);
124
- console.log(`Snapshot: ${snapshotPath}`);
125
- console.log(`\nBu dosyaları Claude Code'a ver, map oluştursun.`);
168
+ const snapPath = join(PATHS.screenshots, `${this.platform}_snapshot.txt`);
169
+ const { writeFileSync } = await import("node:fs");
170
+ writeFileSync(snapPath, snapshot);
171
+ console.log(`Screenshot: ${ssPath}\nSnapshot: ${snapPath}`);
126
172
  process.exit(0);
127
173
  return {} as SelectorMap;
128
174
  }
@@ -132,7 +178,8 @@ export class BrowserDriver {
132
178
  this.cmd("wait 2000");
133
179
  }
134
180
 
135
- executeStep(step: any, content: string, imagePath?: string): void {
181
+ // ── Step çalıştır: önce cached selector, bozuksa snapshot+ref ──
182
+ executeStep(step: SelectorStep, content: string, imagePath?: string): void {
136
183
  const val = (step.value || "").replace("{{CONTENT}}", content).replace("{{IMAGE}}", imagePath || "");
137
184
 
138
185
  switch (step.action) {
@@ -145,48 +192,69 @@ export class BrowserDriver {
145
192
  break;
146
193
 
147
194
  case "click": {
148
- const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
149
- // 1. CSS selector ile dene
150
- for (const sel of sels) {
151
- if (sel.includes("{{")) continue;
195
+ // 1. Cached selector dene
196
+ if (step.selector) {
197
+ try { this.cmd(`click "${step.selector}"`); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
198
+ }
199
+ for (const sel of step.fallbackSelectors || []) {
152
200
  try { this.cmd(`click "${sel}"`); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
153
201
  }
154
- // 2. JS eval fallback - description'dan buton text'i çıkar
155
- const desc = step.description || "";
156
- // Tırnak içindeki text'leri bul: "Gönderi", 'Post', vb.
157
- const textMatches = desc.match(/["'""\u201C\u201D]([^"'""\u201C\u201D]+)["'""\u201C\u201D]/g) || [];
158
- const texts = textMatches.map(m => m.replace(/["'""\u201C\u201D]/g, "").trim()).filter(Boolean);
159
-
160
- for (const text of texts) {
161
- try {
162
- // Sadece BUTTON elementlerinde tam eşleşme ara - link'lere tıklama
163
- this.cmd(`eval "document.querySelectorAll('button').forEach(b => { if(b.textContent.trim()==='${text}') b.click() })"`);
164
- if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
165
- return;
166
- } catch {}
202
+
203
+ // 2. Selector bozuk screenshot + snapshot al, ref ile bul
204
+ try { this.cmd(`screenshot "${join(PATHS.screenshots, `${this.platform}_refmatch.png`)}"`); } catch {}
205
+ const snapshot = this.getSnapshot();
206
+ const ref = this.findRef(snapshot, step.description || "", "button");
207
+ if (ref) {
208
+ this.cmd(`click @${ref}`);
209
+ // Yeni selector'ı cache'le
210
+ const newSel = this.getSelector(ref);
211
+ if (newSel && newSel !== step.selector) {
212
+ console.log(` [cache] ${step.selector || "?"} → ${newSel}`);
213
+ step.selector = newSel;
214
+ }
215
+ if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
216
+ return;
167
217
  }
168
- throw new Error(`Click: ${sels[0] || desc}`);
218
+
219
+ // 3. Son çare: Claude'a sor - snapshot'tan doğru elementi bulsun
220
+ const aiRef = this.askClaudeForRef(snapshot.raw, step.description || "", "click");
221
+ if (aiRef) {
222
+ this.cmd(`click @${aiRef}`);
223
+ if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
224
+ return;
225
+ }
226
+ throw new Error(`Click: ${step.description}`);
169
227
  }
170
228
 
171
229
  case "type": {
172
- const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
173
- const escaped = val.replace(/"/g, '\\"').replace(/'/g, "\\'");
174
- // 1. CSS selector ile dene
175
- for (const sel of sels) {
176
- if (sel.includes("{{")) continue;
177
- try {
178
- this.cmd(`type "${sel}" "${escaped}"`, 30000);
179
- if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
180
- return;
181
- } catch {}
230
+ const escaped = val.replace(/"/g, '\\"');
231
+
232
+ // 1. Cached selector dene
233
+ if (step.selector) {
234
+ try { this.cmd(`type "${step.selector}" "${escaped}"`, 30000); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
182
235
  }
183
- // 2. Fallback: sayfadaki ilk textbox/contenteditable'a yaz
184
- try {
185
- this.cmd(`eval "var el = document.querySelector('[role=textbox],[contenteditable=true]'); if(el) { el.focus(); document.execCommand('insertText', false, '${escaped}'); }"`);
236
+ for (const sel of step.fallbackSelectors || []) {
237
+ try { this.cmd(`type "${sel}" "${escaped}"`, 30000); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
238
+ }
239
+
240
+ // 2. Screenshot + snapshot → ref ile textbox bul
241
+ try { this.cmd(`screenshot "${join(PATHS.screenshots, `${this.platform}_refmatch.png`)}"`); } catch {}
242
+ const snapshot = this.getSnapshot();
243
+ const ref = this.findRef(snapshot, step.description || "", "textbox");
244
+ if (ref) {
245
+ this.cmd(`type @${ref} "${escaped}"`, 30000);
186
246
  if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
187
247
  return;
188
- } catch {}
189
- throw new Error(`Type: ${sels[0]}`);
248
+ }
249
+
250
+ // 3. Claude'a sor
251
+ const aiRef = this.askClaudeForRef(snapshot.raw, step.description || "", "type");
252
+ if (aiRef) {
253
+ this.cmd(`type @${aiRef} "${escaped}"`, 30000);
254
+ if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
255
+ return;
256
+ }
257
+ throw new Error(`Type: ${step.description}`);
190
258
  }
191
259
 
192
260
  case "keypress":
@@ -198,46 +266,35 @@ export class BrowserDriver {
198
266
  const filePath = val || imagePath || "";
199
267
  if (!filePath || filePath.includes("{{")) break;
200
268
 
201
- // 1. agent-browser upload ile dene (X'te çalışır)
202
- try {
203
- this.cmd(`upload "input[type='file']" "${filePath}"`, 5000);
204
- if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
205
- return;
206
- } catch {}
269
+ // 1. input[type=file] direkt (X'te çalışır)
270
+ try { this.cmd(`upload "input[type='file']" "${filePath}"`, 5000); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
207
271
 
208
- // 2. Playwright fileChooser ile dene (LinkedIn gibi native dialog kullananlar için)
209
- // Snapshot'tan medya butonunun aria-label'ını bul
210
- let btnSel = "button[aria-label*='Medya']";
272
+ // 2. Playwright helper (LinkedIn gibi native dialog için)
273
+ // Snapshot'tan medya butonunu bul
274
+ let btnSel = "button[aria-label='Medya ekle']";
211
275
  try {
212
- const snap = this.cmd("snapshot -i -c", 5000);
213
- const mediaMatch = snap.match(/button "([^"]*(?:Medya|media|Fotoğraf|photo|Dosya|file|video)[^"]*)"/i);
214
- if (mediaMatch) btnSel = `button[aria-label='${mediaMatch[1]}']`;
276
+ const snap = this.getSnapshot();
277
+ for (const [ref, info] of snap.refs) {
278
+ if (info.text.match(/medya|fotoğraf|photo|media|video/i) && info.role === "button") {
279
+ btnSel = `button[aria-label='${info.text}']`;
280
+ break;
281
+ }
282
+ }
215
283
  } catch {}
284
+
216
285
  try {
217
286
  const thisDir = new URL(".", import.meta.url).pathname;
218
287
  const pkgDir = process.env.SOCIAL_AGENT_PKG || join(thisDir, "../..");
219
- console.log(` [upload] helper: ${btnSel}`);
220
- const helperResult = execSync(
288
+ const result = execSync(
221
289
  `npx tsx "${join(pkgDir, 'core/upload-helper.ts')}" ${CDP_PORT} "${btnSel}" "${filePath}"`,
222
290
  { encoding: "utf-8", timeout: 60000 }
223
291
  ).trim();
224
- if (helperResult === "OK") {
225
- return;
226
- }
227
- } catch (e: any) {
228
- console.log(` [upload] helper hata: ${e.message?.substring(0, 80)}`);
229
- }
230
-
292
+ if (result === "OK") return;
293
+ } catch {}
231
294
  throw new Error(`Upload: ${filePath}`);
232
295
  }
233
296
  }
234
297
  }
235
298
 
236
- getSnapshot(): string {
237
- try { return this.cmd("snapshot -i -c", 10000); } catch { return ""; }
238
- }
239
-
240
- takeScreenshot(path: string): void {
241
- try { this.cmd(`screenshot "${path}"`); } catch {}
242
- }
299
+ takeScreenshot(path: string): void { try { this.cmd(`screenshot "${path}"`); } catch {} }
243
300
  }