openclacky 0.9.7 → 0.9.9

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/gem-release/SKILL.md +16 -1
  3. data/CHANGELOG.md +51 -0
  4. data/docs/browser-cdp-native-design.md +195 -0
  5. data/docs/session-management-redesign.md +202 -0
  6. data/docs/system-skill-authoring-guide.md +47 -0
  7. data/lib/clacky/agent/cost_tracker.rb +2 -1
  8. data/lib/clacky/agent/message_compressor_helper.rb +2 -2
  9. data/lib/clacky/agent/session_serializer.rb +36 -3
  10. data/lib/clacky/agent/skill_manager.rb +68 -5
  11. data/lib/clacky/agent/system_prompt_builder.rb +30 -13
  12. data/lib/clacky/agent/tool_executor.rb +8 -12
  13. data/lib/clacky/agent.rb +172 -30
  14. data/lib/clacky/brand_config.rb +90 -0
  15. data/lib/clacky/cli.rb +37 -8
  16. data/lib/clacky/default_parsers/doc_parser.rb +69 -0
  17. data/lib/clacky/default_parsers/docx_parser.rb +172 -0
  18. data/lib/clacky/default_parsers/pdf_parser.rb +79 -0
  19. data/lib/clacky/default_parsers/pptx_parser.rb +140 -0
  20. data/lib/clacky/default_parsers/xlsx_parser.rb +121 -0
  21. data/lib/clacky/default_skills/browser-setup/SKILL.md +158 -0
  22. data/lib/clacky/default_skills/channel-setup/SKILL.md +139 -42
  23. data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +582 -0
  24. data/lib/clacky/default_skills/channel-setup/weixin_setup.rb +274 -0
  25. data/lib/clacky/default_skills/onboard/SKILL.md +132 -6
  26. data/lib/clacky/default_skills/personal-website/SKILL.md +113 -0
  27. data/lib/clacky/default_skills/personal-website/publish.rb +214 -0
  28. data/lib/clacky/default_skills/skill-add/SKILL.md +6 -7
  29. data/lib/clacky/default_skills/skill-add/scripts/install_from_zip.rb +1 -1
  30. data/lib/clacky/default_skills/skill-creator/SKILL.md +3 -3
  31. data/lib/clacky/json_ui_controller.rb +5 -0
  32. data/lib/clacky/message_format/open_ai.rb +7 -1
  33. data/lib/clacky/plain_ui_controller.rb +5 -0
  34. data/lib/clacky/server/browser_manager.rb +308 -0
  35. data/lib/clacky/server/channel/adapters/feishu/adapter.rb +80 -15
  36. data/lib/clacky/server/channel/adapters/feishu/bot.rb +216 -7
  37. data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +12 -0
  38. data/lib/clacky/server/channel/adapters/wecom/adapter.rb +2 -8
  39. data/lib/clacky/server/channel/adapters/weixin/adapter.rb +391 -0
  40. data/lib/clacky/server/channel/adapters/weixin/api_client.rb +374 -0
  41. data/lib/clacky/server/channel/channel_config.rb +6 -0
  42. data/lib/clacky/server/channel/channel_manager.rb +50 -15
  43. data/lib/clacky/server/channel/channel_ui_controller.rb +18 -0
  44. data/lib/clacky/server/channel.rb +1 -0
  45. data/lib/clacky/server/http_server.rb +210 -45
  46. data/lib/clacky/server/scheduler.rb +1 -1
  47. data/lib/clacky/server/session_registry.rb +131 -40
  48. data/lib/clacky/server/web_ui_controller.rb +21 -2
  49. data/lib/clacky/session_manager.rb +43 -73
  50. data/lib/clacky/skill.rb +64 -42
  51. data/lib/clacky/tools/browser.rb +728 -183
  52. data/lib/clacky/tools/invoke_skill.rb +18 -11
  53. data/lib/clacky/tools/shell.rb +16 -7
  54. data/lib/clacky/tools/web_fetch.rb +3 -1
  55. data/lib/clacky/ui2/README.md +1 -1
  56. data/lib/clacky/ui2/components/input_area.rb +5 -2
  57. data/lib/clacky/ui2/components/welcome_banner.rb +23 -0
  58. data/lib/clacky/ui2/ui_controller.rb +16 -2
  59. data/lib/clacky/ui_interface.rb +7 -0
  60. data/lib/clacky/utils/file_processor.rb +116 -139
  61. data/lib/clacky/utils/model_pricing.rb +5 -4
  62. data/lib/clacky/utils/parser_manager.rb +93 -0
  63. data/lib/clacky/utils/workspace_rules.rb +46 -0
  64. data/lib/clacky/version.rb +1 -1
  65. data/lib/clacky/web/app.css +696 -62
  66. data/lib/clacky/web/app.js +181 -51
  67. data/lib/clacky/web/brand.js +14 -0
  68. data/lib/clacky/web/channels.js +10 -1
  69. data/lib/clacky/web/i18n.js +49 -7
  70. data/lib/clacky/web/index.html +67 -10
  71. data/lib/clacky/web/onboard.js +2 -1
  72. data/lib/clacky/web/sessions.js +255 -69
  73. data/lib/clacky/web/settings.js +113 -1
  74. data/lib/clacky/web/skills.js +16 -3
  75. data/lib/clacky/web/tasks.js +11 -6
  76. data/lib/clacky/web/weixin-qr.html +104 -0
  77. data/lib/clacky.rb +2 -0
  78. data/scripts/install.sh +41 -11
  79. metadata +48 -7
  80. data/lib/clacky/default_skills/pdf-reader/SKILL.md +0 -90
  81. data/lib/clacky/utils/file_parser/docx_parser.rb +0 -156
  82. data/lib/clacky/utils/file_parser/pptx_parser.rb +0 -116
  83. data/lib/clacky/utils/file_parser/xlsx_parser.rb +0 -95
  84. data/lib/clacky/utils/file_parser/zip_parser.rb +0 -60
  85. data/scripts/install_agent_browser.sh +0 -67
@@ -12,6 +12,7 @@ const Settings = (() => {
12
12
  function open() {
13
13
  _load();
14
14
  _loadBrand();
15
+ _loadBrowserStatus();
15
16
  }
16
17
 
17
18
  // ── Data Loading ────────────────────────────────────────────────────────────
@@ -446,6 +447,80 @@ const Settings = (() => {
446
447
  }
447
448
  }
448
449
 
450
+ // ── Browser Setup ─────────────────────────────────────────────────────────────
451
+
452
+ async function _loadBrowserStatus() {
453
+ try {
454
+ const res = await fetch("/api/browser/status");
455
+ const data = await res.json();
456
+ const desc = document.getElementById("browser-status-desc");
457
+ const btn = document.getElementById("btn-browser-setup");
458
+ const toggleWrap = document.getElementById("browser-toggle-wrap");
459
+ const toggleInput = document.getElementById("browser-toggle-input");
460
+
461
+ // File doesn't exist → not set up yet
462
+ if (data.enabled === undefined || data.enabled === null) {
463
+ desc.textContent = I18n.t("settings.browser.desc");
464
+ btn.textContent = I18n.t("settings.browser.btn");
465
+ toggleWrap.style.display = "none";
466
+ return;
467
+ }
468
+
469
+ // Configured — show toggle + reconfigure button
470
+ const version = data.chrome_version ? ` (Chrome v${data.chrome_version})` : "";
471
+ desc.textContent = I18n.t(data.enabled ? "settings.browser.configured" : "settings.browser.disabled") + version;
472
+ btn.textContent = I18n.t("settings.browser.btn.reconfigure");
473
+ toggleWrap.style.display = "inline-block";
474
+ toggleInput.checked = data.enabled;
475
+
476
+ // Only bind once
477
+ if (!toggleInput.dataset.bound) {
478
+ toggleInput.dataset.bound = "1";
479
+ toggleInput.addEventListener("change", _toggleBrowser);
480
+ }
481
+ } catch (_) { /* non-critical */ }
482
+ }
483
+
484
+ async function _toggleBrowser() {
485
+ const toggleInput = document.getElementById("browser-toggle-input");
486
+ toggleInput.disabled = true;
487
+ try {
488
+ const res = await fetch("/api/browser/toggle", { method: "POST" });
489
+ const data = await res.json();
490
+ if (!data.ok) throw new Error(data.error || "toggle failed");
491
+ await _loadBrowserStatus();
492
+ } catch (_) {
493
+ // Revert on failure
494
+ toggleInput.checked = !toggleInput.checked;
495
+ } finally {
496
+ toggleInput.disabled = false;
497
+ }
498
+ }
499
+
500
+ async function _setupBrowser() {
501
+ const btn = document.getElementById("btn-browser-setup");
502
+ btn.disabled = true;
503
+ btn.textContent = I18n.t("settings.browser.btn.starting");
504
+ try {
505
+ const res = await fetch("/api/sessions", {
506
+ method: "POST",
507
+ headers: { "Content-Type": "application/json" },
508
+ body: JSON.stringify({ name: "🌐 Browser Setup", source: "setup" })
509
+ });
510
+ const data = await res.json();
511
+ const session = data.session;
512
+ if (!session) throw new Error("No session returned");
513
+ Sessions.add(session);
514
+ Sessions.setTab("setup");
515
+ Sessions.renderList();
516
+ Sessions.setPendingMessage(session.id, "/browser-setup");
517
+ Sessions.select(session.id);
518
+ } catch (e) {
519
+ btn.disabled = false;
520
+ btn.textContent = I18n.t("settings.browser.btn");
521
+ }
522
+ }
523
+
449
524
  // ── Brand & License ───────────────────────────────────────────────────────────
450
525
 
451
526
  // Whether the server was started with --brand-test (relaxed key validation).
@@ -494,6 +569,37 @@ const Settings = (() => {
494
569
  expiresEl.textContent = "—";
495
570
  }
496
571
 
572
+ // Show homepage link if available
573
+ const homepageRow = document.getElementById("brand-status-homepage-row");
574
+ const homepageLink = document.getElementById("brand-status-homepage");
575
+ if (info.homepage_url && homepageRow && homepageLink) {
576
+ homepageLink.href = info.homepage_url;
577
+ homepageLink.textContent = info.homepage_url;
578
+ homepageRow.style.display = "";
579
+ } else if (homepageRow) {
580
+ homepageRow.style.display = "none";
581
+ }
582
+
583
+ // Show support contact if available
584
+ const contactWrap = document.getElementById("brand-support-contact");
585
+ const contactLink = document.getElementById("brand-support-contact-link");
586
+ if (info.support_contact && contactWrap && contactLink) {
587
+ const contact = info.support_contact;
588
+ contactLink.textContent = contact;
589
+ // Auto-detect mailto / http link
590
+ if (contact.startsWith("http://") || contact.startsWith("https://")) {
591
+ contactLink.href = contact;
592
+ } else if (contact.includes("@")) {
593
+ contactLink.href = "mailto:" + contact;
594
+ } else {
595
+ contactLink.href = "#";
596
+ contactLink.style.cursor = "default";
597
+ }
598
+ contactWrap.style.display = "";
599
+ } else if (contactWrap) {
600
+ contactWrap.style.display = "none";
601
+ }
602
+
497
603
  // Show support QR code if available
498
604
  const qrContainer = document.getElementById("brand-support-qr");
499
605
  const qrImg = document.getElementById("brand-support-qr-img");
@@ -603,12 +709,15 @@ const Settings = (() => {
603
709
  function init() {
604
710
  document.getElementById("btn-add-model").addEventListener("click", _addModel);
605
711
  document.getElementById("btn-rerun-onboard").addEventListener("click", _rerunOnboard);
712
+ document.getElementById("btn-browser-setup").addEventListener("click", _setupBrowser);
606
713
 
607
714
  document.getElementById("btn-settings-activate").addEventListener("click", _activateLicense);
608
715
  document.getElementById("settings-license-key").addEventListener("keydown", e => {
609
716
  if (e.key === "Enter") _activateLicense();
610
717
  });
611
- document.getElementById("btn-rebind-license").addEventListener("click", () => {
718
+ document.getElementById("btn-rebind-license").addEventListener("click", async () => {
719
+ const confirmed = await Modal.confirm(I18n.t("settings.brand.confirmRebind"));
720
+ if (!confirmed) return;
612
721
  // Show the form again so user can enter a new key
613
722
  document.getElementById("brand-status-card").style.display = "none";
614
723
  document.getElementById("brand-activate-form").style.display = "";
@@ -617,6 +726,9 @@ const Settings = (() => {
617
726
  });
618
727
 
619
728
  _initLangBtns();
729
+
730
+ // Re-render model cards when language changes (dynamic HTML, not data-i18n)
731
+ document.addEventListener("langchange", () => _renderCards());
620
732
  }
621
733
 
622
734
  // ── QR Code Lightbox ───────────────────────────────────────────────────
@@ -241,7 +241,7 @@ const Skills = (() => {
241
241
  const res = await fetch("/api/sessions", {
242
242
  method: "POST",
243
243
  headers: { "Content-Type": "application/json" },
244
- body: JSON.stringify({ name: "Session " + (maxN + 1) })
244
+ body: JSON.stringify({ name: "Session " + (maxN + 1), source: "setup" })
245
245
  });
246
246
  const data = await res.json();
247
247
  if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return; }
@@ -255,6 +255,7 @@ const Skills = (() => {
255
255
  }
256
256
 
257
257
  Sessions.add(session);
258
+ Sessions.setTab("setup");
258
259
  Sessions.renderList();
259
260
  Sessions.setPendingMessage(session.id, "/" + name);
260
261
  Sessions.select(session.id);
@@ -559,6 +560,9 @@ const Skills = (() => {
559
560
  fetch("/api/brand/status")
560
561
  .then(res => res.json())
561
562
  .then(data => {
563
+ const prevLicensed = _userLicensed;
564
+ const prevActivated = _brandActivated;
565
+
562
566
  _brandActivated = data.branded && !data.needs_activation;
563
567
  _userLicensed = !!data.user_licensed;
564
568
 
@@ -566,6 +570,13 @@ const Skills = (() => {
566
570
  // license — the tab itself will show an activation prompt in that case.
567
571
  const brandTab = $("tab-brand-skills");
568
572
  if (brandTab) brandTab.style.display = data.branded ? "" : "none";
573
+
574
+ // Re-render my-skills tab if user_licensed status changed so that the
575
+ // upload buttons appear immediately on first load (they were hidden during
576
+ // the initial synchronous render when _userLicensed was still false).
577
+ if (prevLicensed !== _userLicensed || prevActivated !== _brandActivated) {
578
+ _renderMySkills();
579
+ }
569
580
  })
570
581
  .catch(() => {
571
582
  // On network error, keep whatever is currently shown
@@ -737,7 +748,7 @@ const Skills = (() => {
737
748
  const res = await fetch("/api/sessions", {
738
749
  method: "POST",
739
750
  headers: { "Content-Type": "application/json" },
740
- body: JSON.stringify({ name: "Session " + (maxN + 1) })
751
+ body: JSON.stringify({ name: "Session " + (maxN + 1), source: "setup" })
741
752
  });
742
753
  const data = await res.json();
743
754
  if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return; }
@@ -748,6 +759,7 @@ const Skills = (() => {
748
759
  if (!WS.ready) { WS.connect(); Tasks.load(); }
749
760
 
750
761
  Sessions.add(session);
762
+ Sessions.setTab("setup");
751
763
  Sessions.renderList();
752
764
  Sessions.setPendingMessage(session.id, `/skill-add ${url}`);
753
765
  Sessions.select(session.id);
@@ -766,7 +778,7 @@ const Skills = (() => {
766
778
  const res = await fetch("/api/sessions", {
767
779
  method: "POST",
768
780
  headers: { "Content-Type": "application/json" },
769
- body: JSON.stringify({ name: "Session " + (maxN + 1) })
781
+ body: JSON.stringify({ name: "Session " + (maxN + 1), source: "setup" })
770
782
  });
771
783
  const data = await res.json();
772
784
  if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return; }
@@ -783,6 +795,7 @@ const Skills = (() => {
783
795
  }
784
796
 
785
797
  Sessions.add(session);
798
+ Sessions.setTab("setup");
786
799
  Sessions.renderList();
787
800
  Sessions.setPendingMessage(session.id, "/skill-creator");
788
801
  Sessions.select(session.id);
@@ -47,11 +47,14 @@ const Tasks = (() => {
47
47
 
48
48
  row.innerHTML = `
49
49
  <div class="task-col task-col-name">
50
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="task-icon">
50
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="task-icon task-name-icon">
51
51
  <circle cx="12" cy="12" r="10"/>
52
52
  <polyline points="12 6 12 12 16 14"/>
53
53
  </svg>
54
- <span class="task-name-text">${escapeHtml(t.name)}</span>
54
+ <div class="task-name-info">
55
+ <span class="task-name-text">${escapeHtml(t.name)}</span>
56
+ <span class="task-name-sched">${schedLabel}</span>
57
+ </div>
55
58
  </div>
56
59
  <div class="task-col task-col-schedule">${schedLabel}</div>
57
60
  <div class="task-col task-col-content">${previewText}</div>
@@ -59,13 +62,13 @@ const Tasks = (() => {
59
62
  <button class="task-btn task-btn-run" title="${I18n.t("tasks.btn.run")}">
60
63
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
61
64
  <polygon points="6 3 20 12 6 21 6 3"/>
62
- </svg> ${I18n.t("tasks.btn.run")}
65
+ </svg><span class="btn-label"> ${I18n.t("tasks.btn.run")}</span>
63
66
  </button>
64
67
  <button class="task-btn task-btn-edit" title="${I18n.t("tasks.btn.edit")}">
65
68
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
66
69
  <path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/>
67
70
  <path d="m15 5 4 4"/>
68
- </svg> ${I18n.t("tasks.btn.edit")}
71
+ </svg><span class="btn-label"> ${I18n.t("tasks.btn.edit")}</span>
69
72
  </button>
70
73
  <button class="task-btn task-btn-del" title="Delete">
71
74
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-sm">
@@ -198,7 +201,7 @@ const Tasks = (() => {
198
201
  const res = await fetch("/api/sessions", {
199
202
  method: "POST",
200
203
  headers: { "Content-Type": "application/json" },
201
- body: JSON.stringify({ name: "Session " + (maxN + 1) })
204
+ body: JSON.stringify({ name: "Session " + (maxN + 1), source: "setup" })
202
205
  });
203
206
  const data = await res.json();
204
207
  if (!res.ok) { alert(I18n.t("tasks.sessionError") + (data.error || "unknown")); return; }
@@ -215,6 +218,7 @@ const Tasks = (() => {
215
218
  }
216
219
 
217
220
  Sessions.add(session);
221
+ Sessions.setTab("setup");
218
222
  Sessions.renderList();
219
223
  Sessions.setPendingMessage(session.id, "/cron-task-creator");
220
224
  Sessions.select(session.id);
@@ -229,7 +233,7 @@ const Tasks = (() => {
229
233
  const res = await fetch("/api/sessions", {
230
234
  method: "POST",
231
235
  headers: { "Content-Type": "application/json" },
232
- body: JSON.stringify({ name: "Session " + (maxN + 1) })
236
+ body: JSON.stringify({ name: "Session " + (maxN + 1), source: "setup" })
233
237
  });
234
238
  const data = await res.json();
235
239
  if (!res.ok) { alert("Error creating session: " + (data.error || "unknown")); return; }
@@ -243,6 +247,7 @@ const Tasks = (() => {
243
247
  }
244
248
 
245
249
  Sessions.add(session);
250
+ Sessions.setTab("setup");
246
251
  Sessions.renderList();
247
252
  Sessions.setPendingMessage(session.id, `/cron-task-creator I'm editing ${name} task`);
248
253
  Sessions.select(session.id);
@@ -0,0 +1,104 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>微信扫码登录</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
11
+ background: #f5f5f5;
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ min-height: 100vh;
16
+ }
17
+ .card {
18
+ background: #fff;
19
+ border-radius: 16px;
20
+ padding: 40px 48px;
21
+ text-align: center;
22
+ box-shadow: 0 4px 24px rgba(0,0,0,.08);
23
+ max-width: 360px;
24
+ width: 100%;
25
+ }
26
+ .logo {
27
+ width: 48px;
28
+ height: 48px;
29
+ background: linear-gradient(135deg, #2dc100, #1aad19);
30
+ border-radius: 12px;
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ font-size: 24px;
35
+ margin: 0 auto 16px;
36
+ }
37
+ h1 {
38
+ font-size: 18px;
39
+ font-weight: 600;
40
+ color: #111;
41
+ margin-bottom: 6px;
42
+ }
43
+ p {
44
+ font-size: 13px;
45
+ color: #888;
46
+ margin-bottom: 24px;
47
+ line-height: 1.5;
48
+ }
49
+ #qrcode {
50
+ display: flex;
51
+ justify-content: center;
52
+ margin-bottom: 20px;
53
+ }
54
+ #qrcode canvas, #qrcode img {
55
+ border-radius: 8px;
56
+ border: 1px solid #eee;
57
+ }
58
+ .hint {
59
+ font-size: 12px;
60
+ color: #bbb;
61
+ }
62
+ .error {
63
+ color: #e53e3e;
64
+ font-size: 13px;
65
+ padding: 12px;
66
+ background: #fff5f5;
67
+ border-radius: 8px;
68
+ }
69
+ </style>
70
+ </head>
71
+ <body>
72
+ <div class="card">
73
+ <div class="logo">微</div>
74
+ <h1>微信扫码登录</h1>
75
+ <p>使用微信扫描下方二维码<br>在手机上点击「确认登录」</p>
76
+ <div id="qrcode"></div>
77
+ <div class="hint">二维码有效期 5 分钟</div>
78
+ </div>
79
+
80
+ <script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
81
+ <script>
82
+ const params = new URLSearchParams(location.search);
83
+ const url = params.get("url");
84
+ const el = document.getElementById("qrcode");
85
+
86
+ if (!url) {
87
+ el.innerHTML = '<div class="error">缺少 url 参数</div>';
88
+ } else {
89
+ try {
90
+ new QRCode(el, {
91
+ text: url,
92
+ width: 220,
93
+ height: 220,
94
+ colorDark: "#111111",
95
+ colorLight: "#ffffff",
96
+ correctLevel: QRCode.CorrectLevel.M
97
+ });
98
+ } catch (e) {
99
+ el.innerHTML = '<div class="error">二维码生成失败: ' + e.message + '</div>';
100
+ }
101
+ }
102
+ </script>
103
+ </body>
104
+ </html>
data/lib/clacky.rb CHANGED
@@ -59,6 +59,7 @@ require_relative "clacky/agent"
59
59
 
60
60
  require_relative "clacky/server/session_registry"
61
61
  require_relative "clacky/server/web_ui_controller"
62
+ require_relative "clacky/server/browser_manager"
62
63
  require_relative "clacky/cli"
63
64
 
64
65
  module Clacky
@@ -66,4 +67,5 @@ module Clacky
66
67
  class AgentError < StandardError; end
67
68
  class RetryableError < StandardError; end # Transient errors that should be retried (5xx, HTML response, rate limit)
68
69
  class ToolCallError < AgentError; end # Raised when tool call fails due to invalid parameters
70
+ # BrowserManager singleton: Clacky::BrowserManager.instance
69
71
  end
data/scripts/install.sh CHANGED
@@ -251,7 +251,7 @@ install_via_gem() {
251
251
 
252
252
  if [ $? -eq 0 ]; then
253
253
  print_success "${DISPLAY_NAME} installed successfully!"
254
- install_agent_browser
254
+ install_chrome_devtools_mcp
255
255
  return 0
256
256
  else
257
257
  print_error "Gem installation failed"
@@ -305,7 +305,8 @@ install_macos_dependencies() {
305
305
  print_info "Added mise activation to $SHELL_RC"
306
306
 
307
307
  export PATH="$HOME/.local/bin:$PATH"
308
- eval "$(~/.local/bin/mise activate $CURRENT_SHELL)"
308
+ # Always activate using bash syntax here (this script runs under bash)
309
+ eval "$(~/.local/bin/mise activate bash)"
309
310
 
310
311
  print_success "mise installed successfully"
311
312
  else
@@ -319,8 +320,8 @@ install_macos_dependencies() {
319
320
  # Install Ruby 3 via mise
320
321
  print_info "Installing Ruby 3 via mise..."
321
322
  if ~/.local/bin/mise use -g ruby@3; then
322
- # Reload mise using the current shell
323
- eval "$(~/.local/bin/mise activate $CURRENT_SHELL)"
323
+ # Reload mise using bash syntax (this script runs under bash)
324
+ eval "$(~/.local/bin/mise activate bash)"
324
325
  print_success "Ruby 3 installed successfully"
325
326
  else
326
327
  print_error "Failed to install Ruby 3"
@@ -376,7 +377,8 @@ install_ubuntu_dependencies() {
376
377
  print_info "Added mise activation to $SHELL_RC"
377
378
 
378
379
  export PATH="$HOME/.local/bin:$PATH"
379
- eval "$(~/.local/bin/mise activate $CURRENT_SHELL)"
380
+ # Always activate using bash syntax here (this script runs under bash)
381
+ eval "$(~/.local/bin/mise activate bash)"
380
382
 
381
383
  print_success "mise installed successfully"
382
384
  else
@@ -390,8 +392,8 @@ install_ubuntu_dependencies() {
390
392
  # Install Ruby 3 via mise
391
393
  print_info "Installing Ruby 3 via mise..."
392
394
  if ~/.local/bin/mise use -g ruby@3; then
393
- # Reload mise using the current shell
394
- eval "$(~/.local/bin/mise activate $CURRENT_SHELL)"
395
+ # Reload mise using bash syntax (this script runs under bash)
396
+ eval "$(~/.local/bin/mise activate bash)"
395
397
  print_success "Ruby 3 installed successfully"
396
398
  else
397
399
  print_error "Failed to install Ruby 3"
@@ -634,10 +636,38 @@ main() {
634
636
 
635
637
  # Install agent-browser (browser automation tool)
636
638
  # This step is optional — failures are silently skipped with a hint.
637
- install_agent_browser() {
638
- local script_dir
639
- script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
640
- bash "$script_dir/install_agent_browser.sh" || true
639
+ install_chrome_devtools_mcp() {
640
+ print_step "Installing chrome-devtools-mcp..."
641
+
642
+ # 尝试找到 npm;如果没有,通过 mise 安装 Node.js
643
+ if ! command_exists npm; then
644
+ local mise_bin=""
645
+ if command_exists mise; then
646
+ mise_bin="mise"
647
+ elif [ -x "$HOME/.local/bin/mise" ]; then
648
+ mise_bin="$HOME/.local/bin/mise"
649
+ fi
650
+
651
+ if [ -n "$mise_bin" ]; then
652
+ print_info "Installing Node.js via mise..."
653
+ "$mise_bin" install node@22 > /dev/null 2>&1 || true
654
+ "$mise_bin" use -g node@22 > /dev/null 2>&1 || true
655
+ eval "$("$mise_bin" activate bash 2>/dev/null)" 2>/dev/null || true
656
+ fi
657
+ fi
658
+
659
+ if ! command_exists npm; then
660
+ print_warning "Node.js/npm not found. Browser automation will not be available."
661
+ print_info "To enable browser support, install Node.js and run: npm install -g chrome-devtools-mcp"
662
+ return 0
663
+ fi
664
+
665
+ if npm install -g chrome-devtools-mcp > /dev/null 2>&1; then
666
+ print_success "chrome-devtools-mcp installed"
667
+ else
668
+ print_warning "chrome-devtools-mcp installation failed. Browser automation may not work."
669
+ print_info "To install manually: npm install -g chrome-devtools-mcp"
670
+ fi
641
671
  }
642
672
 
643
673
  # Post-installation information
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.7
4
+ version: 0.9.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '2.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-multipart
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
26
40
  - !ruby/object:Gem::Dependency
27
41
  name: thor
28
42
  requirement: !ruby/object:Gem::Requirement
@@ -191,6 +205,20 @@ dependencies:
191
205
  - - "~>"
192
206
  - !ruby/object:Gem::Version
193
207
  version: '3.0'
208
+ - !ruby/object:Gem::Dependency
209
+ name: chunky_png
210
+ requirement: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - "~>"
213
+ - !ruby/object:Gem::Version
214
+ version: '1.4'
215
+ type: :runtime
216
+ prerelease: false
217
+ version_requirements: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - "~>"
220
+ - !ruby/object:Gem::Version
221
+ version: '1.4'
194
222
  description: OpenClacky is a Ruby CLI tool for interacting with AI models via OpenAI-compatible
195
223
  APIs. It provides chat functionality and autonomous AI agent capabilities with tool
196
224
  use.
@@ -221,13 +249,16 @@ files:
221
249
  - docs/HOW-TO-USE-CN.md
222
250
  - docs/HOW-TO-USE.md
223
251
  - docs/agent-first-ui-design.md
252
+ - docs/browser-cdp-native-design.md
224
253
  - docs/channel-architecture.md
225
254
  - docs/config.example.yml
226
255
  - docs/deploy_subagent_design.md
227
256
  - docs/memory-architecture.md
228
257
  - docs/openclacky_cloud_api_reference.md
229
258
  - docs/security-design.md
259
+ - docs/session-management-redesign.md
230
260
  - docs/session-skill-invocation.md
261
+ - docs/system-skill-authoring-guide.md
231
262
  - docs/time_machine_design.md
232
263
  - docs/ui2-architecture.md
233
264
  - docs/why-developer.md
@@ -262,7 +293,15 @@ files:
262
293
  - lib/clacky/default_agents/coding/system_prompt.md
263
294
  - lib/clacky/default_agents/general/profile.yml
264
295
  - lib/clacky/default_agents/general/system_prompt.md
296
+ - lib/clacky/default_parsers/doc_parser.rb
297
+ - lib/clacky/default_parsers/docx_parser.rb
298
+ - lib/clacky/default_parsers/pdf_parser.rb
299
+ - lib/clacky/default_parsers/pptx_parser.rb
300
+ - lib/clacky/default_parsers/xlsx_parser.rb
301
+ - lib/clacky/default_skills/browser-setup/SKILL.md
265
302
  - lib/clacky/default_skills/channel-setup/SKILL.md
303
+ - lib/clacky/default_skills/channel-setup/feishu_setup.rb
304
+ - lib/clacky/default_skills/channel-setup/weixin_setup.rb
266
305
  - lib/clacky/default_skills/code-explorer/SKILL.md
267
306
  - lib/clacky/default_skills/cron-task-creator/SKILL.md
268
307
  - lib/clacky/default_skills/cron-task-creator/evals/evals.json
@@ -282,7 +321,8 @@ files:
282
321
  - lib/clacky/default_skills/new/scripts/create_rails_project.sh
283
322
  - lib/clacky/default_skills/new/scripts/rails_env_checker.sh
284
323
  - lib/clacky/default_skills/onboard/SKILL.md
285
- - lib/clacky/default_skills/pdf-reader/SKILL.md
324
+ - lib/clacky/default_skills/personal-website/SKILL.md
325
+ - lib/clacky/default_skills/personal-website/publish.rb
286
326
  - lib/clacky/default_skills/product-help/SKILL.md
287
327
  - lib/clacky/default_skills/recall-memory/SKILL.md
288
328
  - lib/clacky/default_skills/skill-add/SKILL.md
@@ -310,6 +350,7 @@ files:
310
350
  - lib/clacky/message_history.rb
311
351
  - lib/clacky/plain_ui_controller.rb
312
352
  - lib/clacky/providers.rb
353
+ - lib/clacky/server/browser_manager.rb
313
354
  - lib/clacky/server/channel.rb
314
355
  - lib/clacky/server/channel/adapters/base.rb
315
356
  - lib/clacky/server/channel/adapters/feishu/adapter.rb
@@ -320,6 +361,8 @@ files:
320
361
  - lib/clacky/server/channel/adapters/wecom/adapter.rb
321
362
  - lib/clacky/server/channel/adapters/wecom/media_downloader.rb
322
363
  - lib/clacky/server/channel/adapters/wecom/ws_client.rb
364
+ - lib/clacky/server/channel/adapters/weixin/adapter.rb
365
+ - lib/clacky/server/channel/adapters/weixin/api_client.rb
323
366
  - lib/clacky/server/channel/channel_config.rb
324
367
  - lib/clacky/server/channel/channel_manager.rb
325
368
  - lib/clacky/server/channel/channel_ui_controller.rb
@@ -380,18 +423,16 @@ files:
380
423
  - lib/clacky/utils/encoding.rb
381
424
  - lib/clacky/utils/environment_detector.rb
382
425
  - lib/clacky/utils/file_ignore_helper.rb
383
- - lib/clacky/utils/file_parser/docx_parser.rb
384
- - lib/clacky/utils/file_parser/pptx_parser.rb
385
- - lib/clacky/utils/file_parser/xlsx_parser.rb
386
- - lib/clacky/utils/file_parser/zip_parser.rb
387
426
  - lib/clacky/utils/file_processor.rb
388
427
  - lib/clacky/utils/gitignore_parser.rb
389
428
  - lib/clacky/utils/limit_stack.rb
390
429
  - lib/clacky/utils/logger.rb
391
430
  - lib/clacky/utils/model_pricing.rb
431
+ - lib/clacky/utils/parser_manager.rb
392
432
  - lib/clacky/utils/path_helper.rb
393
433
  - lib/clacky/utils/string_matcher.rb
394
434
  - lib/clacky/utils/trash_directory.rb
435
+ - lib/clacky/utils/workspace_rules.rb
395
436
  - lib/clacky/version.rb
396
437
  - lib/clacky/web/app.css
397
438
  - lib/clacky/web/app.js
@@ -409,9 +450,9 @@ files:
409
450
  - lib/clacky/web/tasks.js
410
451
  - lib/clacky/web/theme.js
411
452
  - lib/clacky/web/version.js
453
+ - lib/clacky/web/weixin-qr.html
412
454
  - lib/clacky/web/ws.js
413
455
  - scripts/install.sh
414
- - scripts/install_agent_browser.sh
415
456
  - scripts/uninstall.sh
416
457
  - sig/clacky.rbs
417
458
  homepage: https://github.com/yafeilee/clacky