social-agent-cli 3.2.0 → 4.1.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/mapper.ts CHANGED
@@ -1,173 +1,8 @@
1
- import { spawn } from "node:child_process";
2
- import { readFileSync, writeFileSync, existsSync, mkdtempSync, rmSync, readdirSync } from "node:fs";
1
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs";
3
2
  import { join } from "node:path";
4
- import { tmpdir } from "node:os";
5
3
  import { PATHS } from "../core/config.js";
6
4
  import type { SelectorMap } from "../core/types.js";
7
5
 
8
- // ── JSON Schemas ─────────────────────────────────────────────
9
-
10
- const SELECTOR_MAP_SCHEMA = JSON.stringify({
11
- type: "object",
12
- properties: {
13
- platform: { type: "string" },
14
- action: { type: "string" },
15
- description: { type: "string" },
16
- version: { type: "number" },
17
- lastUpdated: { type: "string" },
18
- parameters: { type: "array", items: { type: "string" } },
19
- steps: {
20
- type: "array",
21
- items: {
22
- type: "object",
23
- properties: {
24
- action: { type: "string", enum: ["click", "type", "wait", "keypress", "goto", "upload"] },
25
- description: { type: "string" },
26
- selector: { type: "string" },
27
- value: { type: "string" },
28
- key: { type: "string" },
29
- url: { type: "string" },
30
- waitMs: { type: "number" },
31
- fallbackSelectors: { type: "array", items: { type: "string" } },
32
- },
33
- required: ["action", "description"],
34
- },
35
- },
36
- },
37
- required: ["platform", "version", "steps"],
38
- });
39
-
40
- const HEAL_SCHEMA = JSON.stringify({
41
- type: "object",
42
- properties: {
43
- selector: { type: "string" },
44
- fallbackSelectors: { type: "array", items: { type: "string" } },
45
- explanation: { type: "string" },
46
- },
47
- required: ["selector", "fallbackSelectors", "explanation"],
48
- });
49
-
50
- // ── Claude -p Wrapper ────────────────────────────────────────
51
-
52
- interface ClaudeOptions {
53
- prompt: string;
54
- jsonSchema?: string;
55
- imagePath?: string;
56
- model?: string;
57
- effort?: string;
58
- stream?: boolean;
59
- }
60
-
61
- function claude(opts: ClaudeOptions): Promise<any> {
62
- const { prompt, jsonSchema, imagePath, model = "opus", effort = "low", stream = true } = opts;
63
-
64
- const tmpDir = mkdtempSync(join(tmpdir(), "social-agent-"));
65
- const promptFile = join(tmpDir, "prompt.txt");
66
- writeFileSync(promptFile, prompt);
67
-
68
- return new Promise((resolve, reject) => {
69
- let cmd: string;
70
- const base = `claude -p --model ${model} --effort ${effort} --max-turns 3 --tools ""`;
71
-
72
- // Screenshot varsa prompt'a dosya yolunu ekle, Claude Code dosyayı okuyabilir
73
- if (imagePath && existsSync(imagePath)) {
74
- writeFileSync(promptFile, `Screenshot: ${imagePath}\n\n${prompt}`);
75
- }
76
- cmd = `cat '${promptFile}' | ${base}`;
77
- if (jsonSchema) cmd += ` --output-format json --json-schema '${jsonSchema}'`;
78
-
79
- const proc = spawn("bash", ["-c", cmd], {
80
- stdio: ["pipe", "pipe", "pipe"],
81
- });
82
-
83
- let stdout = "";
84
- let stderr = "";
85
-
86
- proc.stdout.on("data", (chunk: Buffer) => {
87
- const text = chunk.toString();
88
- stdout += text;
89
- });
90
-
91
- proc.stderr.on("data", (chunk: Buffer) => {
92
- stderr += chunk.toString();
93
- });
94
-
95
- proc.on("close", (code) => {
96
- rmSync(tmpDir, { recursive: true, force: true });
97
- if (stream) process.stdout.write("\n");
98
-
99
- const output = stdout.trim();
100
-
101
- if (jsonSchema) {
102
- try {
103
- const parsed = JSON.parse(output);
104
-
105
- // structured_output alanı varsa ve dolu ise
106
- if (parsed.structured_output && parsed.structured_output.steps) {
107
- resolve(parsed.structured_output);
108
- return;
109
- }
110
-
111
- // result alanında JSON string olabilir
112
- if (parsed.result && typeof parsed.result === "string" && parsed.result.length > 2) {
113
- try { resolve(JSON.parse(parsed.result)); return; } catch {}
114
- }
115
-
116
- // error_max_turns veya boş result durumunda - result'tan text çıkar
117
- if (parsed.subtype === "error_max_turns" || !parsed.structured_output) {
118
- // Claude tool kullanıp bitirmeden durmuş, sonucu elle çıkarmayı dene
119
- reject(new Error(`Claude tamamlayamadı (${parsed.subtype || "unknown"}). max-turns artırılabilir.`));
120
- return;
121
- }
122
-
123
- // Mesaj dizisi formatı
124
- if (Array.isArray(parsed)) {
125
- const last = parsed[parsed.length - 1];
126
- const text = typeof last.content === "string"
127
- ? last.content
128
- : last.content?.map((c: any) => c.text || "").join("") || "";
129
- resolve(JSON.parse(text));
130
- return;
131
- }
132
-
133
- // Direkt obje
134
- if (parsed.steps || parsed.selector) {
135
- resolve(parsed);
136
- return;
137
- }
138
-
139
- resolve(parsed);
140
- } catch (parseErr: any) {
141
- // Regex fallback
142
- const match = output.match(/\{[\s\S]*\}/);
143
- if (match) {
144
- try { resolve(JSON.parse(match[0])); return; } catch {}
145
- }
146
- reject(new Error("Claude'dan geçerli JSON alınamadı"));
147
- }
148
- return;
149
- }
150
-
151
- if (code !== 0 && !output) {
152
- reject(new Error(`Claude hata (code ${code}): ${stderr}`));
153
- return;
154
- }
155
-
156
- resolve(output);
157
- });
158
-
159
- proc.on("error", (err) => {
160
- rmSync(tmpDir, { recursive: true, force: true });
161
- reject(err);
162
- });
163
-
164
- setTimeout(() => {
165
- proc.kill();
166
- reject(new Error("Claude timeout (180s)"));
167
- }, 180000);
168
- });
169
- }
170
-
171
6
  // ── Map CRUD (platform başına tek dosya) ─────────────────────
172
7
 
173
8
  interface PlatformMaps {
@@ -207,182 +42,15 @@ export function listMaps(platform?: string): SelectorMap[] {
207
42
  for (const file of readdirSync(PATHS.maps)) {
208
43
  if (!file.endsWith(".json")) continue;
209
44
  if (platform && file !== `${platform}.json`) continue;
210
- const maps = JSON.parse(readFileSync(join(PATHS.maps, file), "utf-8"));
211
- for (const map of Object.values(maps) as SelectorMap[]) {
212
- results.push(map);
45
+ const content = JSON.parse(readFileSync(join(PATHS.maps, file), "utf-8"));
46
+ if (content.steps) {
47
+ results.push(content);
48
+ } else {
49
+ for (const map of Object.values(content) as SelectorMap[]) {
50
+ if (map.steps) results.push(map);
51
+ }
213
52
  }
214
53
  }
215
54
  } catch {}
216
55
  return results;
217
56
  }
218
-
219
- // ── Map Oluştur ──────────────────────────────────────────────
220
-
221
- export async function generateMap(
222
- platform: string,
223
- screenshotPath: string,
224
- domSnapshot: string,
225
- taskDescription = "yeni bir metin postu at",
226
- actionName = "post"
227
- ): Promise<SelectorMap> {
228
- const prompt = `Sen bir browser otomasyon uzmanısın. ${platform} platformunda şu görevi yerine getirmek için adımları belirle:
229
-
230
- GÖREV: "${taskDescription}"
231
-
232
- Sayfanın Accessibility Tree'si (ARIA Snapshot):
233
- ${domSnapshot}
234
-
235
- Kurallar:
236
- - ARIA snapshot'taki role ve name bilgilerini kullanarak selector oluştur
237
- - aria-label, role, data-testid, id tercih et
238
- - Dosya/görsel/video yükleme için "upload" action kullan. Upload action şöyle çalışır:
239
- - selector'da medya/fotoğraf ekleme BUTONUNU ver (ör: button[aria-label*='Fotoğraf'], button[aria-label*='Add media'])
240
- - Sistem butona tıklayıp fileChooser'ı yakalayacak ve dosyayı programatik olarak set edecek, file picker açılmayacak
241
- - Eğer selector verilmezse sayfadaki input[type=file]'a direkt set eder
242
- - value alanına {{IMAGE}} yaz
243
- - Her adım için 2+ fallback selector
244
- - Parametreler için placeholder kullan: {{CONTENT}} = metin, {{USERNAME}} = kullanıcı adı, {{URL}} = link, vs.
245
- - Hangi parametreleri kullandığını "parameters" alanında listele
246
- - action: "${actionName}"
247
- - platform: "${platform}", version: 1, lastUpdated: "${new Date().toISOString()}"
248
- - Gerekiyorsa "goto" action'ı ile belirli bir URL'e git (url alanını kullan)`;
249
-
250
- console.log(`[claude] "${taskDescription}" analiz ediliyor...\n`);
251
-
252
- let map: SelectorMap;
253
- try {
254
- map = await claude({
255
- prompt,
256
- jsonSchema: SELECTOR_MAP_SCHEMA,
257
- imagePath: screenshotPath,
258
- stream: false,
259
- effort: "low",
260
- });
261
- } catch (err: any) {
262
- console.error(`\n[claude] Hata: ${err.message}`);
263
- throw err;
264
- }
265
-
266
- if (!map || !map.steps) {
267
- throw new Error("Claude geçerli bir map döndürmedi");
268
- }
269
-
270
- map.platform = platform;
271
- map.action = actionName;
272
- map.description = taskDescription;
273
- map.lastUpdated = new Date().toISOString();
274
- saveMap(map);
275
- return map;
276
- }
277
-
278
- // ── Self-Heal: Kaldığın yerden devam et ──────────────────────
279
-
280
- const REMAINING_STEPS_SCHEMA = JSON.stringify({
281
- type: "object",
282
- properties: {
283
- analysis: { type: "string" },
284
- remainingSteps: {
285
- type: "array",
286
- items: {
287
- type: "object",
288
- properties: {
289
- action: { type: "string", enum: ["click", "type", "wait", "keypress", "goto", "upload"] },
290
- description: { type: "string" },
291
- selector: { type: "string" },
292
- value: { type: "string" },
293
- key: { type: "string" },
294
- url: { type: "string" },
295
- waitMs: { type: "number" },
296
- fallbackSelectors: { type: "array", items: { type: "string" } },
297
- },
298
- required: ["action", "description"],
299
- },
300
- },
301
- },
302
- required: ["analysis", "remainingSteps"],
303
- });
304
-
305
- /**
306
- * Bir adım başarısız olduğunda AI ekranı görür,
307
- * durumu analiz eder ve kalan adımları yeniden planlar.
308
- * Sadece selector düzeltmez - tamamen yeni akış çizebilir.
309
- */
310
- export async function healAndContinue(
311
- platform: string,
312
- failedStepIndex: number,
313
- originalSteps: SelectorStep[],
314
- screenshotPath: string,
315
- domSnapshot: string,
316
- errorMessage: string,
317
- postContent: string,
318
- effort = "low"
319
- ): Promise<SelectorStep[]> {
320
- const completedSteps = originalSteps.slice(0, failedStepIndex);
321
- const failedStep = originalSteps[failedStepIndex];
322
- const remainingOriginal = originalSteps.slice(failedStepIndex);
323
-
324
- const prompt = `Bir browser otomasyon script'i çalışırken hata oluştu. Ekranın screenshot'ını ve DOM'unu görüyorsun.
325
-
326
- Platform: ${platform}
327
- Amaç: "${postContent}" metnini post olarak paylaşmak
328
-
329
- Tamamlanan adımlar:
330
- ${completedSteps.map((s, i) => ` ${i + 1}. [${s.action}] ${s.description} ✓`).join("\n")}
331
-
332
- Başarısız olan adım (#${failedStepIndex + 1}):
333
- [${failedStep.action}] ${failedStep.description}
334
- Selector: ${failedStep.selector}
335
- Hata: ${errorMessage}
336
-
337
- Kalan orijinal adımlar:
338
- ${remainingOriginal.map((s, i) => ` ${failedStepIndex + i + 1}. [${s.action}] ${s.description} → ${s.selector}`).join("\n")}
339
-
340
- Sayfanın Accessibility Tree'si:
341
- ${domSnapshot}
342
-
343
- GÖREV: Ekranın mevcut durumunu analiz et. Amaca ulaşmak için kalan adımları yeniden planla.
344
- - Belki selector değişmiş → yeni selector bul
345
- - Belki ekstra bir adım gerekiyor (popup kapatma, scroll, vb.)
346
- - Belki farklı bir yol izlenmeli
347
- - {{CONTENT}} = post metni placeholder'ı
348
- - data-testid, aria-label, role tercih et, her adıma 2+ fallback selector ver
349
- - Dosya yükleme için "upload" action kullan. selector'a medya ekleme butonunu ver, value: {{IMAGE}}`;
350
-
351
- console.log(`\n[heal] AI ekranı analiz ediyor (effort: ${effort})...\n`);
352
-
353
- let result: any;
354
- try {
355
- result = await claude({
356
- prompt,
357
- jsonSchema: REMAINING_STEPS_SCHEMA,
358
- imagePath: screenshotPath,
359
- stream: false,
360
- effort,
361
- });
362
- } catch (err: any) {
363
- console.log(`\n[heal] AI hatası: ${err.message}`);
364
- return originalSteps.slice(failedStepIndex + 1); // başarısız step'i atla
365
- }
366
-
367
- // result çeşitli formatlarda gelebilir
368
- if (!result) {
369
- return originalSteps.slice(failedStepIndex + 1);
370
- }
371
-
372
- // structured_output içinde olabilir
373
- if (result.structured_output?.remainingSteps) {
374
- result = result.structured_output;
375
- }
376
-
377
- // remainingSteps yoksa steps dene
378
- const steps = result.remainingSteps || result.steps || result;
379
-
380
- if (!Array.isArray(steps)) {
381
- console.log(`\n[heal] Plan parse edilemedi, başarısız adım atlanıyor.`);
382
- return originalSteps.slice(failedStepIndex + 1);
383
- }
384
-
385
- console.log(`\n[heal] ${steps.length} yeni adım`);
386
-
387
- return steps;
388
- }
package/ai/planner.ts CHANGED
@@ -151,7 +151,7 @@ GÖREV: Bu komutu yerine getirmek için bir çalıştırma planı oluştur.
151
151
  Kurallar:
152
152
  - Mevcut map varsa "use_map" kullan, mapAction'a action adını yaz
153
153
  - Mevcut map yoksa "learn_new" kullan, AI otomatik öğrenecek
154
- - Mevcut map'in parametrelerini kontrol et. Komutta dosya/görsel/fotoğraf varsa ama map'te {{IMAGE}} parametresi yoksa, bu map yetersizdir - "learn_new" ile görsel destekli versiyonunu öğret
154
+ - Komutta fotoğraf/görsel varsa mevcut "post" map'ini kullan, IMAGE parametresini ekle. Sistem upload'ı otomatik handle eder - ayrı map öğretmeye gerek YOK
155
155
  - Parametreleri doldur ({{TWEET_URL}}, {{USERNAME}}, {{CONTENT}}, {{IMAGE}} vb.)
156
156
  - Belirli kişi/URL verilmemişse platformun keşif/öneri sayfalarını kullan
157
157
  - Tekrar eden işlemler için "repeat" kullan
package/ai/runner.ts CHANGED
@@ -1,101 +1,99 @@
1
1
  import { BrowserDriver } from "../platforms/browser/driver.js";
2
- import { loadMap, healAndContinue } from "./mapper.js";
3
- import { createPlan } from "./planner.js";
2
+ import { loadMap } from "./mapper.js";
4
3
  import { saveToHistory } from "../core/history.js";
5
4
  import type { Post } from "../core/types.js";
6
- import { join } from "node:path";
7
- import { PATHS } from "../core/config.js";
8
5
 
9
6
  const BROWSER_PLATFORMS: Record<string, string> = {
10
7
  x: "https://x.com",
11
8
  linkedin: "https://www.linkedin.com",
12
9
  };
13
10
 
11
+ /**
12
+ * Post at - map'i direkt çalıştır, AI çağırmadan
13
+ */
14
14
  export async function runCommand(platform: string, command: string): Promise<void> {
15
15
  const url = BROWSER_PLATFORMS[platform];
16
- if (!url) { console.log(`"${platform}" desteklenmiyor.`); return; }
16
+ if (!url) { console.log(`"${platform}" desteklenmiyor.`); process.exit(1); }
17
17
 
18
- const plan = await createPlan(platform, command);
19
- console.log("\n[runner] Çalıştırılıyor...\n");
18
+ // --content "..." ve --image ... parse et
19
+ let content = "";
20
+ let image = "";
20
21
 
21
- for (const [i, step] of plan.steps.entries()) {
22
- console.log(`━━━ ${i + 1}/${plan.steps.length}: ${step.description} ━━━`);
22
+ const contentMatch = command.match(/--content\s+([\s\S]+?)(?=\s+--|\s*$)/);
23
+ const imageMatch = command.match(/--image\s+(\S+)/);
23
24
 
24
- if (step.type === "learn_new") {
25
- const driver = new BrowserDriver(platform, url);
26
- await driver.learn(step.learnDescription || step.description, step.learnActionName || "custom");
27
- console.log(`[runner] Öğrenildi!\n`);
28
- }
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
+ }
29
34
 
30
- const mapAction = step.type === "use_map" ? step.mapAction! : step.learnActionName || "custom";
31
- const map = loadMap(platform, mapAction);
32
- if (!map) { console.log(`"${mapAction}" bulunamadı, atlanıyor.`); continue; }
35
+ // Map seç
36
+ const map = loadMap(platform, "post");
37
+ if (!map) {
38
+ console.log(`[${platform}] post map'i bulunamadı. "social-agent learn ${platform}" çalıştır.`);
39
+ process.exit(1);
40
+ }
33
41
 
34
- const repeatCount = step.repeat || 1;
42
+ console.log(`[${platform}] Post: "${content.substring(0, 50)}${content.length > 50 ? "..." : ""}"`);
43
+ if (image) console.log(`[${platform}] Görsel: ${image}`);
35
44
 
36
- for (let r = 0; r < repeatCount; r++) {
37
- if (repeatCount > 1) console.log(` [${r + 1}/${repeatCount}]`);
45
+ const driver = new BrowserDriver(platform, url);
46
+ await driver.launch();
38
47
 
39
- const params = { ...step.parameters };
40
- const content = params["{{CONTENT}}"] || params["CONTENT"] || "";
41
- const image = params["{{IMAGE}}"] || params["IMAGE"] || "";
48
+ try {
49
+ // Ana sayfaya git
50
+ if (map.steps[0]?.action !== "goto") {
51
+ driver.navigate(url);
52
+ }
42
53
 
43
- const driver = new BrowserDriver(platform, url);
44
- await driver.launch();
54
+ let steps = [...map.steps];
55
+
56
+ // Image varsa: gönder butonundan önce upload step ekle
57
+ if (image && !steps.some(s => s.action === "upload")) {
58
+ const sendIdx = steps.findLastIndex(s => s.action === "click");
59
+ if (sendIdx > -1) {
60
+ steps.splice(sendIdx, 0, {
61
+ action: "upload" as any,
62
+ description: "Fotoğraf yükle",
63
+ selector: "input[type='file']",
64
+ value: "{{IMAGE}}",
65
+ waitMs: 3000,
66
+ });
67
+ }
68
+ }
45
69
 
70
+ // Step'leri çalıştır
71
+ const errors: string[] = [];
72
+ for (let idx = 0; idx < steps.length; idx++) {
73
+ const s = steps[idx];
46
74
  try {
47
- // Ana sayfaya git (ilk step goto değilse)
48
- if (map.steps[0]?.action !== "goto") {
49
- driver.navigate(url);
50
- }
51
-
52
- let steps = [...map.steps];
53
- let healCount = 0;
54
-
55
- for (let idx = 0; idx < steps.length; idx++) {
56
- const s = steps[idx];
57
-
58
- try {
59
- console.log(` ${idx + 1}. ${s.description}`);
60
- driver.executeStep(s, content, image);
61
- } catch (err: any) {
62
- if (healCount >= 2) {
63
- console.log(` ✗ ${err.message} (atlanıyor)`);
64
- continue;
65
- }
66
- healCount++;
67
- console.log(` ✗ ${err.message}`);
68
- console.log(` → Düzeltiliyor...`);
69
-
70
- const ss = join(PATHS.screenshots, `${platform}_heal.png`);
71
- driver.takeScreenshot(ss);
72
- const snap = driver.getSnapshot();
73
-
74
- const fixed = await healAndContinue(
75
- platform, idx, steps, ss, snap,
76
- err.message, content || step.description, "medium"
77
- );
78
- steps = [...steps.slice(0, idx), ...fixed];
79
- idx--;
80
- }
81
- }
82
-
83
- console.log(` ✓ Tamamlandı`);
84
- saveToHistory({ platform, content: content || step.description, action: mapAction.includes("post") ? "post" : mapAction, success: true });
75
+ console.log(` ${idx + 1}. ${s.description}`);
76
+ driver.executeStep(s, content, image);
85
77
  } catch (err: any) {
78
+ errors.push(`Step ${idx + 1} (${s.description}): ${err.message}`);
86
79
  console.log(` ✗ ${err.message}`);
87
- saveToHistory({ platform, content: content || step.description, action: mapAction, success: false, error: err.message });
88
- } finally {
89
- await driver.close();
90
80
  }
81
+ }
91
82
 
92
- if (r < repeatCount - 1) {
93
- await new Promise((res) => setTimeout(res, 2000 + Math.random() * 3000));
94
- }
83
+ if (errors.length === 0) {
84
+ console.log(`\n✓ ${platform} post gönderildi`);
85
+ saveToHistory({ platform, content, action: "post", success: true, image: image || undefined });
86
+ } else {
87
+ console.log(`\n⚠ ${errors.length} hata oluştu:`);
88
+ errors.forEach(e => console.log(` - ${e}`));
89
+ saveToHistory({ platform, content, action: "post", success: false, error: errors.join("; ") });
95
90
  }
96
- console.log("");
91
+ } catch (err: any) {
92
+ console.log(`\n✗ ${err.message}`);
93
+ saveToHistory({ platform, content, action: "post", success: false, error: err.message });
94
+ } finally {
95
+ await driver.close();
97
96
  }
98
97
 
99
- console.log("[runner] Tamamlandı.");
100
98
  process.exit(0);
101
99
  }
package/cli.ts CHANGED
@@ -189,17 +189,14 @@ switch (command) {
189
189
  }
190
190
 
191
191
  case "run": {
192
- const hasYes = args.includes("--yes") || args.includes("-y");
193
- const filteredArgs = args.filter(a => a !== "--yes" && a !== "-y");
194
- const runPlatform = BROWSER_PLATFORMS[filteredArgs[0]] ? filteredArgs[0] : "x";
195
- const runCmd = BROWSER_PLATFORMS[filteredArgs[0]]
196
- ? filteredArgs.slice(1).join(" ")
197
- : 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(" ");
198
195
  if (!runCmd) {
199
- console.log('Kullanım: social-agent run <platform> "doğal dil komutu" [--yes]');
196
+ console.log('Kullanım: social-agent run <platform> --content "mesaj" [--image /path/to/img]');
200
197
  break;
201
198
  }
202
- await runCommand(runPlatform, runCmd, hasYes);
199
+ await runCommand(runPlatform, runCmd);
203
200
  break;
204
201
  }
205
202
 
@@ -0,0 +1,34 @@
1
+ /**
2
+ * LinkedIn upload helper - Playwright fileChooser ile dosya yükler
3
+ * agent-browser CDP ile çalışmıyor çünkü LinkedIn native file dialog kullanıyor
4
+ *
5
+ * Kullanım: npx tsx core/upload-helper.ts <cdp-port> <button-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 context = browser.contexts()[0];
19
+ const pages = context.pages();
20
+ const page = pages[pages.length - 1]; // son aktif sayfa
21
+
22
+ const [fileChooser] = await Promise.all([
23
+ page.waitForEvent("filechooser", { timeout: 10000 }),
24
+ page.click(selector),
25
+ ]);
26
+
27
+ await fileChooser.setFiles(filePath);
28
+ console.log("OK");
29
+ }
30
+
31
+ upload().catch((e) => {
32
+ console.error(e.message);
33
+ process.exit(1);
34
+ });
@@ -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": "3.2.0",
3
+ "version": "4.1.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": {
@@ -3,7 +3,7 @@ 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, generateMap } from "../../ai/mapper.js";
6
+ import { loadMap } from "../../ai/mapper.js";
7
7
  import { pickChromeProfile, getSavedProfile } from "../../core/profiles.js";
8
8
  import type { SelectorMap } from "../../core/types.js";
9
9
 
@@ -105,27 +105,26 @@ export class BrowserDriver {
105
105
  }
106
106
 
107
107
  /**
108
- * Learn - ARIA snapshot ile map oluştur
108
+ * Learn - ARIA snapshot al ve dosyaya yaz (Claude Code okuyup map oluşturur)
109
109
  */
110
110
  async learn(taskDescription = "yeni bir metin postu at", actionName = "post"): Promise<SelectorMap> {
111
111
  await this.launch();
112
112
  this.cmd(`open "${this.homeUrl}"`, 30000);
113
113
  this.cmd("wait 3000");
114
114
 
115
- const screenshotPath = join(PATHS.screenshots, `${this.platform}_learn_${actionName}.png`);
115
+ const screenshotPath = join(PATHS.screenshots, `${this.platform}_learn.png`);
116
116
  this.cmd(`screenshot "${screenshotPath}"`);
117
117
 
118
- // Compact interactive snapshot
119
118
  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);
120
122
 
121
- console.log(`[${this.platform}] Screenshot: ${screenshotPath}`);
122
- console.log(`[${this.platform}] Snapshot: ${snapshot.length} bytes`);
123
- console.log(`[${this.platform}] Claude analiz ediyor...`);
124
-
125
- const map = await generateMap(this.platform, screenshotPath, snapshot, taskDescription, actionName);
126
- console.log(`[${this.platform}] "${actionName}" map oluşturuldu (${map.steps.length} adım)`);
123
+ console.log(`Screenshot: ${screenshotPath}`);
124
+ console.log(`Snapshot: ${snapshotPath}`);
125
+ console.log(`\nBu dosyaları Claude Code'a ver, map oluştursun.`);
127
126
  process.exit(0);
128
- return map;
127
+ return {} as SelectorMap;
129
128
  }
130
129
 
131
130
  navigate(url: string): void {
@@ -147,16 +146,31 @@ export class BrowserDriver {
147
146
 
148
147
  case "click": {
149
148
  const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
149
+ // 1. CSS selector ile dene
150
150
  for (const sel of sels) {
151
151
  if (sel.includes("{{")) continue;
152
152
  try { this.cmd(`click "${sel}"`); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
153
153
  }
154
- 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}`);
155
168
  }
156
169
 
157
170
  case "type": {
158
171
  const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
159
- const escaped = val.replace(/"/g, '\\"');
172
+ const escaped = val.replace(/"/g, '\\"').replace(/'/g, "\\'");
173
+ // 1. CSS selector ile dene
160
174
  for (const sel of sels) {
161
175
  if (sel.includes("{{")) continue;
162
176
  try {
@@ -165,6 +179,12 @@ export class BrowserDriver {
165
179
  return;
166
180
  } catch {}
167
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 {}
168
188
  throw new Error(`Type: ${sels[0]}`);
169
189
  }
170
190
 
@@ -176,17 +196,35 @@ export class BrowserDriver {
176
196
  case "upload": {
177
197
  const filePath = val || imagePath || "";
178
198
  if (!filePath || filePath.includes("{{")) break;
179
- // Her zaman input[type=file] ile yükle - en güvenilir yol
199
+
200
+ // 1. agent-browser upload ile dene (X'te çalışır)
180
201
  try {
181
- this.cmd(`upload "input[type='file']" "${filePath}"`, 10000);
202
+ this.cmd(`upload "input[type='file']" "${filePath}"`, 5000);
182
203
  if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
183
204
  return;
184
205
  } catch {}
185
- // Fallback: selector'larla dene
206
+
207
+ // 2. Playwright fileChooser ile dene (LinkedIn gibi native dialog kullananlar için)
186
208
  const sels = [step.selector, ...(step.fallbackSelectors || [])].filter(Boolean);
187
- for (const sel of sels) {
188
- try { this.cmd(`upload "${sel}" "${filePath}"`, 10000); if (step.waitMs) this.cmd(`wait ${step.waitMs}`); return; } catch {}
189
- }
209
+ const btnSel = sels.find(s => !s.includes("input[type")) || "button";
210
+ try {
211
+ const { dirname: dn } = await import("node:path");
212
+ const { fileURLToPath: fu } = await import("node:url");
213
+ const pkgDir = process.env.SOCIAL_AGENT_PKG || join(dn(fu(import.meta.url)), "../..");
214
+ execSync(
215
+ `npx tsx "${join(pkgDir, 'core/upload-helper.ts')}" ${CDP_PORT} "${btnSel}" "${filePath}"`,
216
+ { stdio: "pipe", timeout: 15000 }
217
+ );
218
+ this.cmd("wait 3000");
219
+ // Editör modal'ı varsa kapat
220
+ try {
221
+ this.cmd(`eval "document.querySelectorAll('button').forEach(b => { var t=b.textContent.trim().toLowerCase(); if(t==='ileri' || t==='bitti' || t==='done' || t==='next' || t==='tamam') b.click() })"`);
222
+ this.cmd("wait 1000");
223
+ } catch {}
224
+ if (step.waitMs) this.cmd(`wait ${step.waitMs}`);
225
+ return;
226
+ } catch {}
227
+
190
228
  throw new Error(`Upload: ${filePath}`);
191
229
  }
192
230
  }