social-agent-cli 4.0.0 → 4.2.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/ai/runner.ts CHANGED
@@ -15,16 +15,22 @@ export async function runCommand(platform: string, command: string): Promise<voi
15
15
  const url = BROWSER_PLATFORMS[platform];
16
16
  if (!url) { console.log(`"${platform}" desteklenmiyor.`); process.exit(1); }
17
17
 
18
- // Komuttan CONTENT ve IMAGE çıkar
19
- const imageMatch = command.match(/(?:fotoğraf|foto|image|görsel)[:\s]*([^\s,]+)/i);
20
- const image = imageMatch?.[1]?.trim() || "";
18
+ // --content "..." ve --image ... parse et
19
+ let content = "";
20
+ let image = "";
21
21
 
22
- // Image referansını ve prefix'leri temizle
23
- let content = command
24
- .replace(/,?\s*(?:fotoğraf|foto|image|görsel)[:\s]*[^\s,]+/i, "")
25
- .replace(/^şu mesajı (?:fotoğraf ile )?(?:tweetle|paylaş|gönder)[:\s]*/i, "")
26
- .replace(/^(?:tweetle|paylaş|post at|mesaj)[:\s]*/i, "")
27
- .trim();
22
+ const contentMatch = command.match(/--content\s+([\s\S]+?)(?=\s+--|\s*$)/);
23
+ const imageMatch = command.match(/--image\s+(\S+)/);
24
+
25
+ if (contentMatch) {
26
+ content = contentMatch[1].replace(/^"|"$/g, "").trim();
27
+ } else {
28
+ // Flag yoksa tüm komutu content olarak al
29
+ content = command.replace(/--image\s+\S+/g, "").trim();
30
+ }
31
+ if (imageMatch) {
32
+ image = imageMatch[1];
33
+ }
28
34
 
29
35
  // Map seç
30
36
  const map = loadMap(platform, "post");
@@ -47,11 +53,12 @@ export async function runCommand(platform: string, command: string): Promise<voi
47
53
 
48
54
  let steps = [...map.steps];
49
55
 
50
- // Image varsa: gönder butonundan önce upload step ekle
56
+ // Image varsa: metin yazma step'inden ÖNCE upload step ekle (LinkedIn'de önce fotoğraf yüklenmeli)
51
57
  if (image && !steps.some(s => s.action === "upload")) {
52
- const sendIdx = steps.findLastIndex(s => s.action === "click");
53
- if (sendIdx > -1) {
54
- steps.splice(sendIdx, 0, {
58
+ const typeIdx = steps.findIndex(s => s.action === "type");
59
+ const insertIdx = typeIdx > -1 ? typeIdx : steps.findLastIndex(s => s.action === "click");
60
+ if (insertIdx > -1) {
61
+ steps.splice(insertIdx, 0, {
55
62
  action: "upload" as any,
56
63
  description: "Fotoğraf yükle",
57
64
  selector: "input[type='file']",
@@ -63,7 +70,9 @@ export async function runCommand(platform: string, command: string): Promise<voi
63
70
 
64
71
  // Step'leri çalıştır
65
72
  const errors: string[] = [];
73
+ let aborted = false;
66
74
  for (let idx = 0; idx < steps.length; idx++) {
75
+ if (aborted) break;
67
76
  const s = steps[idx];
68
77
  try {
69
78
  console.log(` ${idx + 1}. ${s.description}`);
@@ -71,6 +80,8 @@ export async function runCommand(platform: string, command: string): Promise<voi
71
80
  } catch (err: any) {
72
81
  errors.push(`Step ${idx + 1} (${s.description}): ${err.message}`);
73
82
  console.log(` ✗ ${err.message}`);
83
+ // Upload hatası → geri kalanı çalıştırma (fotoğrafsız post göndermesin)
84
+ if (s.action === "upload") { aborted = true; }
74
85
  }
75
86
  }
76
87
 
package/cli.ts CHANGED
@@ -189,13 +189,11 @@ switch (command) {
189
189
  }
190
190
 
191
191
  case "run": {
192
- const filteredArgs = args.filter(a => !a.startsWith("--"));
193
- const runPlatform = BROWSER_PLATFORMS[filteredArgs[0]] ? filteredArgs[0] : "x";
194
- const runCmd = BROWSER_PLATFORMS[filteredArgs[0]]
195
- ? filteredArgs.slice(1).join(" ")
196
- : filteredArgs.join(" ");
192
+ const runPlatform = BROWSER_PLATFORMS[args[0]] ? args[0] : "x";
193
+ const runArgs = BROWSER_PLATFORMS[args[0]] ? args.slice(1) : args;
194
+ const runCmd = runArgs.join(" ");
197
195
  if (!runCmd) {
198
- console.log('Kullanım: social-agent run <platform> "komut"');
196
+ console.log('Kullanım: social-agent run <platform> --content "mesaj" [--image /path/to/img]');
199
197
  break;
200
198
  }
201
199
  await runCommand(runPlatform, runCmd);
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Upload helper - CDP fileChooser intercept ile dosya yükler
3
+ * LinkedIn gibi native file dialog kullanan platformlar için
4
+ *
5
+ * Kullanım: npx tsx core/upload-helper.ts <cdp-port> <click-selector> <file-path>
6
+ */
7
+ import { chromium } from "playwright";
8
+
9
+ const [, , port, selector, filePath] = process.argv;
10
+
11
+ if (!port || !selector || !filePath) {
12
+ console.error("Kullanım: npx tsx core/upload-helper.ts <port> <selector> <file-path>");
13
+ process.exit(1);
14
+ }
15
+
16
+ async function upload() {
17
+ const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
18
+ const pages = browser.contexts()[0].pages();
19
+ const page = pages[pages.length - 1];
20
+
21
+ // fileChooser event'ini dinlemeye başla
22
+ let resolved = false;
23
+ const fcPromise = page.waitForEvent("filechooser", { timeout: 10000 });
24
+
25
+ // Butona tıkla - fileChooser tetiklenecek
26
+ // Birden fazla selector dene
27
+ const selectors = selector.split("|");
28
+ for (const sel of selectors) {
29
+ try {
30
+ // Playwright text selector
31
+ if (sel.startsWith("text=")) {
32
+ await page.click(sel);
33
+ } else {
34
+ // CSS selector veya role selector
35
+ const el = page.locator(sel).first();
36
+ if (await el.isVisible({ timeout: 2000 })) {
37
+ await el.click();
38
+ }
39
+ }
40
+ } catch {}
41
+ }
42
+
43
+ try {
44
+ const fc = await fcPromise;
45
+ await fc.setFiles(filePath);
46
+ resolved = true;
47
+ console.log("OK");
48
+ } catch (e: any) {
49
+ console.error("FAIL:", e.message.substring(0, 100));
50
+ process.exit(1);
51
+ }
52
+ }
53
+
54
+ upload().catch((e) => {
55
+ console.error(e.message);
56
+ process.exit(1);
57
+ });
@@ -2,68 +2,35 @@
2
2
  "post": {
3
3
  "platform": "linkedin",
4
4
  "action": "post",
5
- "description": "LinkedIn üzerinden yeni bir metin postu paylaşma",
6
- "version": 1,
7
- "lastUpdated": "2026-03-14T12:00:00.000Z",
5
+ "description": "LinkedIn post paylaşma",
6
+ "version": 3,
8
7
  "parameters": [
9
8
  "{{CONTENT}}"
10
9
  ],
11
10
  "steps": [
12
11
  {
13
12
  "action": "click",
14
- "description": "Gönderi oluştur kutusuna tıkla",
13
+ "description": "\"Gönderi başlatın\" butonuna tıkla",
15
14
  "selector": "button.share-box-feed-entry__trigger",
16
- "fallbackSelectors": [
17
- "[aria-label*='Start a post']",
18
- "[aria-label*='Gönderi başlat']",
19
- "button[aria-label*='post']"
20
- ]
21
- },
22
- {
23
- "action": "wait",
24
- "description": "Post oluşturma modalının açılmasını bekle",
25
- "waitMs": 1500
26
- },
27
- {
28
- "action": "click",
29
- "description": "Metin alanına tıkla",
30
- "selector": "[role='textbox'][aria-label*='Text editor']",
31
- "fallbackSelectors": [
32
- "[role='textbox'][contenteditable='true']",
33
- ".ql-editor[contenteditable='true']",
34
- "[aria-label*='Metin düzenleyici']"
35
- ]
15
+ "fallbackSelectors": [],
16
+ "waitMs": 2000
36
17
  },
37
18
  {
38
19
  "action": "type",
39
20
  "description": "Post metnini yaz",
40
- "selector": "[role='textbox'][aria-label*='Text editor']",
21
+ "selector": "[role=\"textbox\"]",
41
22
  "value": "{{CONTENT}}",
42
23
  "fallbackSelectors": [
43
- "[role='textbox'][contenteditable='true']",
44
- ".ql-editor[contenteditable='true']"
45
- ]
46
- },
47
- {
48
- "action": "wait",
49
- "description": "Metin girişi sonrası bekleme",
24
+ "[contenteditable=\"true\"]"
25
+ ],
50
26
  "waitMs": 1000
51
27
  },
52
28
  {
53
29
  "action": "click",
54
- "description": "Gönder butonuna tıkla",
30
+ "description": "\"Gönderi\" gönder butonuna tıkla",
55
31
  "selector": "button.share-actions__primary-action",
56
- "fallbackSelectors": [
57
- "[aria-label='Post']",
58
- "[aria-label='Gönder']",
59
- "button[aria-label*='Post']",
60
- "button[aria-label*='Paylaş']"
61
- ]
62
- },
63
- {
64
- "action": "wait",
65
- "description": "Post gönderimini bekle",
66
- "waitMs": 2000
32
+ "fallbackSelectors": [],
33
+ "waitMs": 3000
67
34
  }
68
35
  ]
69
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "social-agent-cli",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "AI-powered social media agent - free APIs + browser automation with self-healing selectors",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,7 @@
24
24
  "tsconfig.json"
25
25
  ],
26
26
  "dependencies": {
27
+ "playwright": "^1.58.2",
27
28
  "tsx": "^4.19.0"
28
29
  },
29
30
  "devDependencies": {
@@ -146,16 +146,31 @@ export class BrowserDriver {
146
146
 
147
147
  case "click": {
148
148
  const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
149
+ // 1. CSS selector ile dene
149
150
  for (const sel of sels) {
150
151
  if (sel.includes("{{")) continue;
151
152
  try { this.cmd(`click "${sel}"`); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
152
153
  }
153
- throw new Error(`Click: ${sels[0]}`);
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
+ this.cmd(`eval "document.querySelectorAll('button,a,[role=button]').forEach(b => { if(b.textContent.trim()==='${text}' || b.textContent.trim().startsWith('${text}')) b.click() })"`);
163
+ if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
164
+ return;
165
+ } catch {}
166
+ }
167
+ throw new Error(`Click: ${sels[0] || desc}`);
154
168
  }
155
169
 
156
170
  case "type": {
157
171
  const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
158
- const escaped = val.replace(/"/g, '\\"');
172
+ const escaped = val.replace(/"/g, '\\"').replace(/'/g, "\\'");
173
+ // 1. CSS selector ile dene
159
174
  for (const sel of sels) {
160
175
  if (sel.includes("{{")) continue;
161
176
  try {
@@ -164,6 +179,12 @@ export class BrowserDriver {
164
179
  return;
165
180
  } catch {}
166
181
  }
182
+ // 2. Fallback: sayfadaki ilk textbox/contenteditable'a yaz
183
+ try {
184
+ this.cmd(`eval "var el = document.querySelector('[role=textbox],[contenteditable=true]'); if(el) { el.focus(); document.execCommand('insertText', false, '${escaped}'); }"`);
185
+ if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
186
+ return;
187
+ } catch {}
167
188
  throw new Error(`Type: ${sels[0]}`);
168
189
  }
169
190
 
@@ -175,17 +196,44 @@ export class BrowserDriver {
175
196
  case "upload": {
176
197
  const filePath = val || imagePath || "";
177
198
  if (!filePath || filePath.includes("{{")) break;
178
- // Her zaman input[type=file] ile yükle - en güvenilir yol
199
+
200
+ // 1. agent-browser upload ile dene (X'te çalışır)
179
201
  try {
180
- this.cmd(`upload "input[type='file']" "${filePath}"`, 10000);
202
+ this.cmd(`upload "input[type='file']" "${filePath}"`, 5000);
181
203
  if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
182
204
  return;
183
205
  } catch {}
184
- // Fallback: selector'larla dene
185
- const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
186
- for (const sel of sels) {
187
- try { this.cmd(`upload "${sel}" "${filePath}"`, 10000); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
206
+
207
+ // 2. Playwright fileChooser ile dene (LinkedIn gibi native dialog kullananlar için)
208
+ // Snapshot'tan medya butonunun aria-label'ını bul
209
+ let btnSel = "button[aria-label*='Medya']";
210
+ try {
211
+ const snap = this.cmd("snapshot -i -c", 5000);
212
+ const mediaMatch = snap.match(/button "([^"]*(?:Medya|media|Fotoğraf|photo|Dosya|file|video)[^"]*)"/i);
213
+ if (mediaMatch) btnSel = `button[aria-label='${mediaMatch[1]}']`;
214
+ } catch {}
215
+ try {
216
+ const thisDir = new URL(".", import.meta.url).pathname;
217
+ const pkgDir = process.env.SOCIAL_AGENT_PKG || join(thisDir, "../..");
218
+ console.log(` [upload] helper: ${btnSel}`);
219
+ const helperResult = execSync(
220
+ `npx tsx "${join(pkgDir, 'core/upload-helper.ts')}" ${CDP_PORT} "${btnSel}" "${filePath}"`,
221
+ { encoding: "utf-8", timeout: 60000 }
222
+ ).trim();
223
+ if (helperResult === "OK") {
224
+ // Editör modal kapatma - "İleri" butonuna tıkla (3 saniye bekle, modal yüklensin)
225
+ setTimeout(() => {}, 3000);
226
+ try {
227
+ execSync(`agent-browser --cdp ${CDP_PORT} wait 3000`, { stdio: "pipe", timeout: 10000 });
228
+ execSync(`agent-browser --cdp ${CDP_PORT} eval "document.querySelectorAll('button').forEach(b => { var t=b.textContent.trim().toLowerCase(); if(t==='ileri' || t==='bitti' || t==='done' || t==='next') b.click() })"`, { stdio: "pipe", timeout: 5000 });
229
+ execSync(`agent-browser --cdp ${CDP_PORT} wait 2000`, { stdio: "pipe", timeout: 5000 });
230
+ } catch {}
231
+ return;
232
+ }
233
+ } catch (e: any) {
234
+ console.log(` [upload] helper hata: ${e.message?.substring(0, 80)}`);
188
235
  }
236
+
189
237
  throw new Error(`Upload: ${filePath}`);
190
238
  }
191
239
  }