openclacky 1.5.2 → 1.5.4
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.
- checksums.yaml +4 -4
- data/.dockerignore +13 -0
- data/CHANGELOG.md +50 -0
- data/Dockerfile +21 -1
- data/README.md +26 -7
- data/README_CN.md +26 -7
- data/README_JA.md +26 -7
- data/lib/clacky/agent/message_compressor.rb +4 -29
- data/lib/clacky/agent/message_compressor_helper.rb +13 -15
- data/lib/clacky/agent/session_serializer.rb +6 -0
- data/lib/clacky/agent.rb +13 -1
- data/lib/clacky/agent_config.rb +42 -11
- data/lib/clacky/brand_config.rb +32 -12
- data/lib/clacky/client.rb +7 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +15 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +2 -6
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +11 -4
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +5 -1
- data/lib/clacky/default_parsers/xlsx_parser.py +66 -0
- data/lib/clacky/extension/api_extension.rb +3 -2
- data/lib/clacky/message_format/anthropic.rb +27 -1
- data/lib/clacky/message_format/open_ai.rb +92 -4
- data/lib/clacky/providers.rb +67 -14
- data/lib/clacky/server/http_server.rb +361 -36
- data/lib/clacky/server/project_manager.rb +150 -0
- data/lib/clacky/server/server_master.rb +7 -0
- data/lib/clacky/server/session_registry.rb +14 -1
- data/lib/clacky/session_manager.rb +5 -4
- data/lib/clacky/tools/file_reader.rb +2 -1
- data/lib/clacky/utils/model_pricing.rb +77 -18
- data/lib/clacky/utils/parser_manager.rb +115 -7
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1053 -143
- data/lib/clacky/web/app.js +19 -5
- data/lib/clacky/web/components/code-editor.js +50 -12
- data/lib/clacky/web/components/notify.js +139 -22
- data/lib/clacky/web/core/aside.js +21 -0
- data/lib/clacky/web/features/billing/view.js +2 -1
- data/lib/clacky/web/features/brand/store.js +3 -1
- data/lib/clacky/web/features/brand/view.js +29 -5
- data/lib/clacky/web/features/extensions/store.js +15 -6
- data/lib/clacky/web/features/extensions/view.js +8 -4
- data/lib/clacky/web/features/new-session/view.js +2 -2
- data/lib/clacky/web/features/trash/store.js +23 -9
- data/lib/clacky/web/features/trash/view.js +51 -0
- data/lib/clacky/web/features/workspace/store.js +9 -0
- data/lib/clacky/web/features/workspace/view.js +8 -1
- data/lib/clacky/web/i18n.js +99 -4
- data/lib/clacky/web/index.html +70 -17
- data/lib/clacky/web/projects.js +1143 -0
- data/lib/clacky/web/sessions.js +318 -537
- data/lib/clacky/web/settings.js +146 -7
- data/lib/clacky/web/ws-dispatcher.js +59 -1
- data/scripts/build/src/install_system_deps.sh.cc +11 -3
- data/scripts/install_system_deps.sh +11 -3
- metadata +5 -2
- data/lib/clacky/default_parsers/xlsx_parser.rb +0 -121
data/lib/clacky/web/settings.js
CHANGED
|
@@ -20,6 +20,7 @@ const Settings = (() => {
|
|
|
20
20
|
_loadBrand();
|
|
21
21
|
_loadBrowserStatus();
|
|
22
22
|
_initNetworkSettings();
|
|
23
|
+
_initPlatformSourceSettings();
|
|
23
24
|
if (Clacky.Backup) Clacky.Backup.load();
|
|
24
25
|
_applyAboutTabVisibility();
|
|
25
26
|
}
|
|
@@ -528,8 +529,14 @@ const Settings = (() => {
|
|
|
528
529
|
|
|
529
530
|
// Toggle API key visibility
|
|
530
531
|
document.getElementById("model-modal-toggle-key").addEventListener("click", () => {
|
|
531
|
-
const input
|
|
532
|
-
|
|
532
|
+
const input = document.getElementById("model-modal-apikey");
|
|
533
|
+
const eyeSvg = document.getElementById("model-modal-toggle-key").querySelector("svg");
|
|
534
|
+
const isPassword = input.type === "password";
|
|
535
|
+
input.type = isPassword ? "text" : "password";
|
|
536
|
+
// Switch icon: eye-off ↔ eye
|
|
537
|
+
eyeSvg.innerHTML = isPassword
|
|
538
|
+
? `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>`
|
|
539
|
+
: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"></path>`;
|
|
533
540
|
});
|
|
534
541
|
|
|
535
542
|
// Model dropdown functionality
|
|
@@ -1380,6 +1387,137 @@ const Settings = (() => {
|
|
|
1380
1387
|
|
|
1381
1388
|
// Whether the server was started with --brand-test (relaxed key validation).
|
|
1382
1389
|
let _brandTestMode = false;
|
|
1390
|
+
let _loadedPlatformSource = "";
|
|
1391
|
+
const OFFICIAL_PLATFORM_SOURCE = "https://www.openclacky.com";
|
|
1392
|
+
|
|
1393
|
+
async function _initPlatformSourceSettings() {
|
|
1394
|
+
const input = document.getElementById("settings-clacky-license-server");
|
|
1395
|
+
const saveBtn = document.getElementById("btn-save-clacky-license-server");
|
|
1396
|
+
const restoreBtn = document.getElementById("btn-restore-clacky-license-server");
|
|
1397
|
+
if (!input || !saveBtn) return;
|
|
1398
|
+
|
|
1399
|
+
try {
|
|
1400
|
+
const res = await fetch("/api/config/settings");
|
|
1401
|
+
const data = await res.json();
|
|
1402
|
+
if (data.ok) {
|
|
1403
|
+
_loadedPlatformSource = data.clacky_license_server || "";
|
|
1404
|
+
input.value = _loadedPlatformSource;
|
|
1405
|
+
}
|
|
1406
|
+
} catch (_) { /* non-critical */ }
|
|
1407
|
+
_syncPlatformSourceButtons();
|
|
1408
|
+
|
|
1409
|
+
if (!saveBtn.dataset.bound) {
|
|
1410
|
+
saveBtn.dataset.bound = "1";
|
|
1411
|
+
saveBtn.addEventListener("click", () => _savePlatformSource());
|
|
1412
|
+
input.addEventListener("keydown", e => {
|
|
1413
|
+
if (e.key === "Enter") _savePlatformSource();
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
if (restoreBtn && !restoreBtn.dataset.bound) {
|
|
1417
|
+
restoreBtn.dataset.bound = "1";
|
|
1418
|
+
restoreBtn.addEventListener("click", () => {
|
|
1419
|
+
_savePlatformSource(OFFICIAL_PLATFORM_SOURCE);
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
function _platformSourceOrigin(value) {
|
|
1425
|
+
try {
|
|
1426
|
+
return new URL(value).origin.toLowerCase();
|
|
1427
|
+
} catch (_) {
|
|
1428
|
+
return value.trim();
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function _syncPlatformSourceButtons({ busy = false } = {}) {
|
|
1433
|
+
const saveBtn = document.getElementById("btn-save-clacky-license-server");
|
|
1434
|
+
const restoreBtn = document.getElementById("btn-restore-clacky-license-server");
|
|
1435
|
+
if (saveBtn) saveBtn.disabled = busy;
|
|
1436
|
+
if (restoreBtn) {
|
|
1437
|
+
const official = _platformSourceOrigin(OFFICIAL_PLATFORM_SOURCE);
|
|
1438
|
+
restoreBtn.disabled = busy ||
|
|
1439
|
+
_platformSourceOrigin(_loadedPlatformSource) === official;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
async function _savePlatformSource(sourceOverride = null) {
|
|
1444
|
+
const input = document.getElementById("settings-clacky-license-server");
|
|
1445
|
+
const saveBtn = document.getElementById("btn-save-clacky-license-server");
|
|
1446
|
+
const status = document.getElementById("settings-clacky-license-server-status");
|
|
1447
|
+
const source = typeof sourceOverride === "string"
|
|
1448
|
+
? sourceOverride
|
|
1449
|
+
: input.value.trim();
|
|
1450
|
+
if (saveBtn.disabled) return;
|
|
1451
|
+
|
|
1452
|
+
status.textContent = "";
|
|
1453
|
+
status.className = "model-test-result";
|
|
1454
|
+
if (!source) {
|
|
1455
|
+
status.textContent = I18n.t("settings.brand.source.required");
|
|
1456
|
+
status.className = "model-test-result result-fail";
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const sourceChanged =
|
|
1461
|
+
_platformSourceOrigin(source) !== _platformSourceOrigin(_loadedPlatformSource);
|
|
1462
|
+
if (sourceChanged) {
|
|
1463
|
+
_syncPlatformSourceButtons({ busy: true });
|
|
1464
|
+
const confirmed = await Modal.confirm(I18n.t("settings.brand.source.confirm"));
|
|
1465
|
+
if (!confirmed) {
|
|
1466
|
+
_syncPlatformSourceButtons();
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
_syncPlatformSourceButtons({ busy: true });
|
|
1472
|
+
saveBtn.textContent = I18n.t("settings.brand.source.saving");
|
|
1473
|
+
|
|
1474
|
+
try {
|
|
1475
|
+
const res = await fetch("/api/config/settings", {
|
|
1476
|
+
method: "PATCH",
|
|
1477
|
+
headers: { "Content-Type": "application/json" },
|
|
1478
|
+
body: JSON.stringify({ clacky_license_server: source })
|
|
1479
|
+
});
|
|
1480
|
+
const data = await res.json();
|
|
1481
|
+
if (!res.ok || !data.ok) {
|
|
1482
|
+
throw new Error(data.error || I18n.t("settings.brand.source.invalid"));
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
_loadedPlatformSource = data.clacky_license_server;
|
|
1486
|
+
input.value = _loadedPlatformSource;
|
|
1487
|
+
|
|
1488
|
+
if (data.restarting) {
|
|
1489
|
+
status.textContent = I18n.t("settings.brand.source.reconnecting");
|
|
1490
|
+
status.className = "model-test-result result-ok";
|
|
1491
|
+
const reconnected = await _waitForPlatformSourceRestart();
|
|
1492
|
+
if (!reconnected) {
|
|
1493
|
+
throw new Error(I18n.t("settings.brand.source.reconnectFailed"));
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
await Brand.refresh();
|
|
1498
|
+
status.textContent = I18n.t("settings.brand.source.saved");
|
|
1499
|
+
status.className = "model-test-result result-ok";
|
|
1500
|
+
} catch (e) {
|
|
1501
|
+
status.textContent = e.message || I18n.t("settings.brand.source.invalid");
|
|
1502
|
+
status.className = "model-test-result result-fail";
|
|
1503
|
+
} finally {
|
|
1504
|
+
_syncPlatformSourceButtons();
|
|
1505
|
+
saveBtn.textContent = I18n.t("settings.brand.source.save");
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async function _waitForPlatformSourceRestart() {
|
|
1510
|
+
// The old Worker can still answer HTTP while the Master is shutting it
|
|
1511
|
+
// down, so an HTTP success does not prove the new source is active. A full
|
|
1512
|
+
// WebSocket disconnect/reconnect cycle identifies the replacement Worker.
|
|
1513
|
+
let disconnected = !WS.ready;
|
|
1514
|
+
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
1515
|
+
if (!WS.ready) disconnected = true;
|
|
1516
|
+
if (disconnected && WS.ready) return true;
|
|
1517
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1518
|
+
}
|
|
1519
|
+
return false;
|
|
1520
|
+
}
|
|
1383
1521
|
|
|
1384
1522
|
// Load and render the current brand/license status in Settings.
|
|
1385
1523
|
async function _loadBrand() {
|
|
@@ -1486,11 +1624,12 @@ const Settings = (() => {
|
|
|
1486
1624
|
statusCard.style.display = "none";
|
|
1487
1625
|
activateForm.style.display = "";
|
|
1488
1626
|
|
|
1489
|
-
//
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
I18n.t("settings.brand.descNamed", { name: data.product_name })
|
|
1627
|
+
// Keep the activation prompt in sync when platform branding changes.
|
|
1628
|
+
const desc = activateForm.querySelector(".brand-activate-desc");
|
|
1629
|
+
if (desc) {
|
|
1630
|
+
desc.textContent = data.product_name
|
|
1631
|
+
? I18n.t("settings.brand.descNamed", { name: data.product_name })
|
|
1632
|
+
: I18n.t("settings.brand.desc");
|
|
1494
1633
|
}
|
|
1495
1634
|
|
|
1496
1635
|
// Show "Get a serial number" link only when the brand vendor has
|
|
@@ -181,7 +181,9 @@ WS.onEvent(ev => {
|
|
|
181
181
|
// ── Session list ───────────────────────────────────────────────────
|
|
182
182
|
case "session_list": {
|
|
183
183
|
Sessions.setAll(ev.sessions || [], !!ev.has_more, ev.cron_count || 0, ev.latest_cron_updated_at || null);
|
|
184
|
+
Projects.setAll(ev.projects || []);
|
|
184
185
|
Sessions.renderList();
|
|
186
|
+
Projects.renderSection();
|
|
185
187
|
|
|
186
188
|
// Restore URL hash once on initial connect; ignore subsequent session_list events.
|
|
187
189
|
// Skip if we are already on a session view (e.g. onboard flow navigated there
|
|
@@ -257,6 +259,7 @@ WS.onEvent(ev => {
|
|
|
257
259
|
if (!sid) break;
|
|
258
260
|
Sessions.patch(sid, patch);
|
|
259
261
|
Sessions.renderList();
|
|
262
|
+
Projects.renderSection();
|
|
260
263
|
if (sid === Sessions.activeId) {
|
|
261
264
|
const current = Sessions.find(sid);
|
|
262
265
|
if (patch.status !== undefined) Sessions.updateStatusBar(patch.status);
|
|
@@ -285,6 +288,7 @@ WS.onEvent(ev => {
|
|
|
285
288
|
case "session_renamed": {
|
|
286
289
|
Sessions.patch(ev.session_id, { name: ev.name });
|
|
287
290
|
Sessions.renderList();
|
|
291
|
+
Projects.renderSection();
|
|
288
292
|
// Title is now shown only in the sidebar; chat-header element was removed.
|
|
289
293
|
break;
|
|
290
294
|
}
|
|
@@ -294,6 +298,7 @@ WS.onEvent(ev => {
|
|
|
294
298
|
Sessions.remove(ev.session_id);
|
|
295
299
|
if (ev.session_id === Sessions.activeId) Router.navigate("welcome");
|
|
296
300
|
Sessions.renderList();
|
|
301
|
+
Projects.renderSection();
|
|
297
302
|
break;
|
|
298
303
|
|
|
299
304
|
case "session_restored":
|
|
@@ -303,6 +308,7 @@ WS.onEvent(ev => {
|
|
|
303
308
|
if (ev.session) {
|
|
304
309
|
Sessions.add(ev.session);
|
|
305
310
|
Sessions.renderList();
|
|
311
|
+
Projects.renderSection();
|
|
306
312
|
}
|
|
307
313
|
break;
|
|
308
314
|
|
|
@@ -438,10 +444,62 @@ WS.onEvent(ev => {
|
|
|
438
444
|
renderErrorEvent(ev);
|
|
439
445
|
}
|
|
440
446
|
break;
|
|
447
|
+
|
|
448
|
+
// ── UI control signals ─────────────────────────────────────────────
|
|
449
|
+
case "open_aside":
|
|
450
|
+
// Triggered by the AI (via POST /api/ui/open_aside) after building an
|
|
451
|
+
// extension whose panel lives in the right-side aside column.
|
|
452
|
+
if (window.Clacky && Clacky.Aside) Clacky.Aside.open();
|
|
453
|
+
break;
|
|
454
|
+
|
|
455
|
+
case "show_ext_refresh":
|
|
456
|
+
// Triggered by the AI (via POST /api/ui/show_ext_refresh) after it edits
|
|
457
|
+
// extension files, so the user can reload extensions with one click.
|
|
458
|
+
_appendExtRefreshHint();
|
|
459
|
+
break;
|
|
460
|
+
|
|
441
461
|
}
|
|
442
462
|
});
|
|
443
463
|
|
|
444
|
-
// ──
|
|
464
|
+
// ── Ext-developer refresh hint ─────────────────────────────────────────────
|
|
465
|
+
// Appended after a completed ext-developer session so the user can reload
|
|
466
|
+
// extensions with one click instead of doing a manual page refresh.
|
|
467
|
+
|
|
468
|
+
function _appendExtRefreshHint() {
|
|
469
|
+
const messages = RenderTarget.current();
|
|
470
|
+
if (!messages) return;
|
|
471
|
+
|
|
472
|
+
const wrap = document.createElement("div");
|
|
473
|
+
wrap.className = "msg msg-ext-refresh-hint";
|
|
474
|
+
|
|
475
|
+
const btn = document.createElement("button");
|
|
476
|
+
btn.className = "btn-ext-refresh";
|
|
477
|
+
btn.innerHTML =
|
|
478
|
+
`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">` +
|
|
479
|
+
`<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>` +
|
|
480
|
+
`<path d="M21 3v5h-5"/>` +
|
|
481
|
+
`<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>` +
|
|
482
|
+
`<path d="M8 16H3v5"/>` +
|
|
483
|
+
`</svg>` +
|
|
484
|
+
`<span>${I18n.t("ext.refresh_hint")}</span>`;
|
|
485
|
+
btn.onclick = () => {
|
|
486
|
+
if (window.Clacky && Clacky.ext && typeof Clacky.ext.reload === "function") {
|
|
487
|
+
Clacky.ext.reload();
|
|
488
|
+
} else {
|
|
489
|
+
location.reload();
|
|
490
|
+
}
|
|
491
|
+
btn.disabled = true;
|
|
492
|
+
btn.style.opacity = "0.5";
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
wrap.appendChild(btn);
|
|
496
|
+
messages.appendChild(wrap);
|
|
497
|
+
|
|
498
|
+
// Scroll into view
|
|
499
|
+
messages.scrollTop = messages.scrollHeight;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
|
|
445
503
|
|
|
446
504
|
function _transformRetryWarning(message) {
|
|
447
505
|
return message;
|
|
@@ -103,13 +103,21 @@ main() {
|
|
|
103
103
|
|
|
104
104
|
assert_supported_os
|
|
105
105
|
|
|
106
|
+
# --clt-only: install just what ships /usr/bin/python3 (Xcode CLT on macOS,
|
|
107
|
+
# python3 on Linux) and skip Homebrew. Callers that only need python3 (e.g.
|
|
108
|
+
# the file parser) use this to avoid the interactive Homebrew sudo prompt.
|
|
109
|
+
local clt_only=false
|
|
110
|
+
[ "$1" = "--clt-only" ] && clt_only=true
|
|
111
|
+
|
|
106
112
|
case "$OS" in
|
|
107
113
|
macOS)
|
|
108
114
|
detect_shell
|
|
109
115
|
ensure_xcode_clt || exit 1
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
if [ "$clt_only" = false ]; then
|
|
117
|
+
detect_network_region
|
|
118
|
+
configure_homebrew_cn_mirrors
|
|
119
|
+
ensure_homebrew || exit 1
|
|
120
|
+
fi
|
|
113
121
|
;;
|
|
114
122
|
Linux) ensure_linux_deps || exit 1 ;;
|
|
115
123
|
esac
|
|
@@ -599,13 +599,21 @@ main() {
|
|
|
599
599
|
|
|
600
600
|
assert_supported_os
|
|
601
601
|
|
|
602
|
+
# --clt-only: install just what ships /usr/bin/python3 (Xcode CLT on macOS,
|
|
603
|
+
# python3 on Linux) and skip Homebrew. Callers that only need python3 (e.g.
|
|
604
|
+
# the file parser) use this to avoid the interactive Homebrew sudo prompt.
|
|
605
|
+
local clt_only=false
|
|
606
|
+
[ "$1" = "--clt-only" ] && clt_only=true
|
|
607
|
+
|
|
602
608
|
case "$OS" in
|
|
603
609
|
macOS)
|
|
604
610
|
detect_shell
|
|
605
611
|
ensure_xcode_clt || exit 1
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
612
|
+
if [ "$clt_only" = false ]; then
|
|
613
|
+
detect_network_region
|
|
614
|
+
configure_homebrew_cn_mirrors
|
|
615
|
+
ensure_homebrew || exit 1
|
|
616
|
+
fi
|
|
609
617
|
;;
|
|
610
618
|
Linux) ensure_linux_deps || exit 1 ;;
|
|
611
619
|
esac
|
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: 1.5.
|
|
4
|
+
version: 1.5.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
@@ -293,6 +293,7 @@ files:
|
|
|
293
293
|
- ".clacky/skills/gem-release/scripts/release.sh"
|
|
294
294
|
- ".clacky/skills/oss-upload/SKILL.md"
|
|
295
295
|
- ".clackyrules"
|
|
296
|
+
- ".dockerignore"
|
|
296
297
|
- ".rspec"
|
|
297
298
|
- ".rubocop.yml"
|
|
298
299
|
- CHANGELOG.md
|
|
@@ -391,7 +392,7 @@ files:
|
|
|
391
392
|
- lib/clacky/default_parsers/pdf_parser_vlm.py
|
|
392
393
|
- lib/clacky/default_parsers/pptx_parser.rb
|
|
393
394
|
- lib/clacky/default_parsers/wps_parser.rb
|
|
394
|
-
- lib/clacky/default_parsers/xlsx_parser.
|
|
395
|
+
- lib/clacky/default_parsers/xlsx_parser.py
|
|
395
396
|
- lib/clacky/default_skills/browser-setup/SKILL.md
|
|
396
397
|
- lib/clacky/default_skills/channel-manager/SKILL.md
|
|
397
398
|
- lib/clacky/default_skills/channel-manager/dingtalk_setup.rb
|
|
@@ -536,6 +537,7 @@ files:
|
|
|
536
537
|
- lib/clacky/server/epipe_safe_io.rb
|
|
537
538
|
- lib/clacky/server/git_panel.rb
|
|
538
539
|
- lib/clacky/server/http_server.rb
|
|
540
|
+
- lib/clacky/server/project_manager.rb
|
|
539
541
|
- lib/clacky/server/scheduler.rb
|
|
540
542
|
- lib/clacky/server/server_master.rb
|
|
541
543
|
- lib/clacky/server/session_registry.rb
|
|
@@ -662,6 +664,7 @@ files:
|
|
|
662
664
|
- lib/clacky/web/icon.svg
|
|
663
665
|
- lib/clacky/web/index.html
|
|
664
666
|
- lib/clacky/web/logo_nav_dark.png
|
|
667
|
+
- lib/clacky/web/projects.js
|
|
665
668
|
- lib/clacky/web/sessions.js
|
|
666
669
|
- lib/clacky/web/settings.js
|
|
667
670
|
- lib/clacky/web/skills.js
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
#
|
|
4
|
-
# Clacky XLSX Parser — CLI interface
|
|
5
|
-
#
|
|
6
|
-
# Usage:
|
|
7
|
-
# ruby xlsx_parser.rb <file_path>
|
|
8
|
-
#
|
|
9
|
-
# Output:
|
|
10
|
-
# stdout — extracted content in Markdown tables (UTF-8)
|
|
11
|
-
# stderr — error messages
|
|
12
|
-
# exit 0 — success
|
|
13
|
-
# exit 1 — failure
|
|
14
|
-
#
|
|
15
|
-
# Dependencies: rubyzip gem (gem install rubyzip)
|
|
16
|
-
#
|
|
17
|
-
# This file lives in ~/.clacky/parsers/ and can be modified by the LLM.
|
|
18
|
-
#
|
|
19
|
-
# VERSION: 1
|
|
20
|
-
|
|
21
|
-
require "zip"
|
|
22
|
-
require "rexml/document"
|
|
23
|
-
require "stringio"
|
|
24
|
-
|
|
25
|
-
def parse_row(row_node, shared_strings)
|
|
26
|
-
REXML::XPath.match(row_node, ".//c").map do |c|
|
|
27
|
-
v = REXML::XPath.first(c, "v")&.text
|
|
28
|
-
next "" unless v
|
|
29
|
-
c.attributes["t"] == "s" ? (shared_strings[v.to_i] || "") : v
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def build_markdown_table(rows)
|
|
34
|
-
col_count = rows.map(&:size).max
|
|
35
|
-
lines = []
|
|
36
|
-
rows.each_with_index do |row, i|
|
|
37
|
-
padded = row + [""] * [col_count - row.size, 0].max
|
|
38
|
-
lines << "| #{padded.join(" | ")} |"
|
|
39
|
-
lines << "|#{" --- |" * col_count}" if i == 0
|
|
40
|
-
end
|
|
41
|
-
lines.join("\n")
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# --- main ---
|
|
45
|
-
|
|
46
|
-
path = ARGV[0]
|
|
47
|
-
|
|
48
|
-
if path.nil? || path.empty?
|
|
49
|
-
warn "Usage: ruby xlsx_parser.rb <file_path>"
|
|
50
|
-
exit 1
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
unless File.exist?(path)
|
|
54
|
-
warn "File not found: #{path}"
|
|
55
|
-
exit 1
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
begin
|
|
59
|
-
body = File.binread(path)
|
|
60
|
-
shared_strings = []
|
|
61
|
-
sheet_names = {}
|
|
62
|
-
sheet_xmls = {}
|
|
63
|
-
|
|
64
|
-
Zip::File.open_buffer(StringIO.new(body)) do |zip|
|
|
65
|
-
ss_entry = zip.find_entry("xl/sharedStrings.xml")
|
|
66
|
-
if ss_entry
|
|
67
|
-
doc = REXML::Document.new(ss_entry.get_input_stream.read)
|
|
68
|
-
REXML::XPath.each(doc, "//si") do |si|
|
|
69
|
-
shared_strings << REXML::XPath.match(si, ".//t").map(&:text).compact.join
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
wb_entry = zip.find_entry("xl/workbook.xml")
|
|
74
|
-
if wb_entry
|
|
75
|
-
doc = REXML::Document.new(wb_entry.get_input_stream.read)
|
|
76
|
-
REXML::XPath.each(doc, "//sheet") do |s|
|
|
77
|
-
idx = s.attributes["sheetId"]
|
|
78
|
-
name = s.attributes["name"]
|
|
79
|
-
sheet_names[idx] = name if idx && name
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
zip.each do |entry|
|
|
84
|
-
if entry.name =~ %r{xl/worksheets/sheet(\d+)\.xml}
|
|
85
|
-
sheet_xmls[$1] = entry.get_input_stream.read
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
if sheet_xmls.empty?
|
|
91
|
-
warn "Spreadsheet appears to be empty"
|
|
92
|
-
exit 1
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
sections = []
|
|
96
|
-
sheet_xmls.keys.sort_by(&:to_i).each do |idx|
|
|
97
|
-
name = sheet_names[idx] || "Sheet#{idx}"
|
|
98
|
-
doc = REXML::Document.new(sheet_xmls[idx])
|
|
99
|
-
|
|
100
|
-
rows = []
|
|
101
|
-
REXML::XPath.each(doc, "//row") do |row|
|
|
102
|
-
cells = parse_row(row, shared_strings)
|
|
103
|
-
rows << cells unless cells.all?(&:empty?)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
next if rows.empty?
|
|
107
|
-
sections << "### #{name}\n\n#{build_markdown_table(rows)}"
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
if sections.empty?
|
|
111
|
-
warn "Spreadsheet appears to be empty"
|
|
112
|
-
exit 1
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
print sections.join("\n\n")
|
|
116
|
-
exit 0
|
|
117
|
-
rescue => e
|
|
118
|
-
warn "Failed to parse XLSX: #{e.message}"
|
|
119
|
-
warn "Tip: ensure rubyzip is installed: gem install rubyzip"
|
|
120
|
-
exit 1
|
|
121
|
-
end
|