openclacky 1.3.6 → 1.3.7
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/CHANGELOG.md +30 -0
- data/lib/clacky/agent.rb +8 -1
- data/lib/clacky/agent_profile.rb +121 -53
- data/lib/clacky/brand_config.rb +309 -6
- data/lib/clacky/cli.rb +4 -57
- data/lib/clacky/default_extensions/coding/ext.yml +18 -0
- data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
- data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
- data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
- data/lib/clacky/default_extensions/general/ext.yml +18 -0
- data/lib/clacky/default_extensions/git/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
- data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
- data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
- data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
- data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
- data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
- data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
- data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
- data/lib/clacky/extension/api_loader.rb +136 -0
- data/lib/clacky/extension/cli_commands.rb +226 -0
- data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
- data/lib/clacky/extension/hook_loader.rb +77 -0
- data/lib/clacky/extension/loader.rb +483 -0
- data/lib/clacky/extension/packager.rb +226 -0
- data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
- data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
- data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
- data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
- data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
- data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
- data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
- data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
- data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
- data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
- data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
- data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
- data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
- data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
- data/lib/clacky/extension/scaffold.rb +55 -0
- data/lib/clacky/extension/verifier.rb +196 -0
- data/lib/clacky/identity.rb +70 -0
- data/lib/clacky/locales/en.rb +19 -1
- data/lib/clacky/locales/zh.rb +19 -1
- data/lib/clacky/platform_http_client.rb +18 -15
- data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
- data/lib/clacky/server/channel.rb +6 -0
- data/lib/clacky/server/http_server.rb +313 -144
- data/lib/clacky/server/session_registry.rb +9 -2
- data/lib/clacky/skill_loader.rb +28 -1
- data/lib/clacky/ui2/ui_controller.rb +9 -6
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +748 -573
- data/lib/clacky/web/app.js +63 -15
- data/lib/clacky/web/auth.js +2 -0
- data/lib/clacky/web/components/notify.js +2 -0
- data/lib/clacky/web/components/sidebar.js +1 -5
- data/lib/clacky/web/core/ext.js +316 -79
- data/lib/clacky/web/features/backup/store.js +1 -0
- data/lib/clacky/web/features/backup/view.js +0 -1
- data/lib/clacky/web/features/brand/view.js +13 -5
- data/lib/clacky/web/features/extensions/store.js +184 -0
- data/lib/clacky/web/features/extensions/view.js +389 -0
- data/lib/clacky/web/features/new-session/store.js +186 -0
- data/lib/clacky/web/features/new-session/view.js +319 -0
- data/lib/clacky/web/features/share/view.js +63 -14
- data/lib/clacky/web/features/skills/store.js +3 -1
- data/lib/clacky/web/features/skills/view.js +1 -1
- data/lib/clacky/web/features/tasks/view.js +12 -0
- data/lib/clacky/web/features/trash/store.js +24 -6
- data/lib/clacky/web/features/trash/view.js +46 -9
- data/lib/clacky/web/features/workspace/store.js +2 -0
- data/lib/clacky/web/features/workspace/view.js +0 -1
- data/lib/clacky/web/i18n.js +118 -106
- data/lib/clacky/web/index.html +138 -188
- data/lib/clacky/web/sessions.js +64 -269
- data/lib/clacky/web/settings.js +1 -1
- data/lib/clacky/web/skills.js +2 -0
- data/lib/clacky/web/theme.js +2 -0
- data/lib/clacky/web/ws-dispatcher.js +1 -0
- data/lib/clacky/web/ws.js +2 -0
- data/lib/clacky.rb +21 -10
- metadata +55 -24
- data/benchmark/runner.rb +0 -441
- data/lib/clacky/api_extension_loader.rb +0 -168
- data/lib/clacky/default_agents/SOUL.md +0 -3
- data/lib/clacky/default_agents/USER.md +0 -1
- data/lib/clacky/default_agents/coding/profile.yml +0 -5
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_agents/general/profile.yml +0 -2
- data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
- data/lib/clacky/web/features/creator/store.js +0 -81
- data/lib/clacky/web/features/creator/view.js +0 -380
- /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
- /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
|
@@ -607,55 +607,66 @@
|
|
|
607
607
|
confirmRow, confirmYes, confirmNo, files: filesEl, diff: diffEl, diffPath: diffPathEl };
|
|
608
608
|
}
|
|
609
609
|
|
|
610
|
-
Clacky.ext.ui.mount("session.aside",
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
};
|
|
610
|
+
Clacky.ext.ui.mount("session.aside", {
|
|
611
|
+
create(ctx) {
|
|
612
|
+
const list = el("div", { class: "tm-list" });
|
|
613
|
+
const foot = el("div", { class: "tm-foot", text: t("tm.foot", "每完成一步会自动存档。点击想回到的版本即可恢复。") });
|
|
614
|
+
const root = el("div", { class: "tm-panel", "data-panel": "tm" }, list, foot);
|
|
615
|
+
|
|
616
|
+
const d = buildDrawer();
|
|
617
|
+
|
|
618
|
+
const state = {
|
|
619
|
+
sessionId: ctx.sessionId,
|
|
620
|
+
tasks: [],
|
|
621
|
+
selected: null,
|
|
622
|
+
expanded: null,
|
|
623
|
+
panelEl: root, listEl: list, footEl: foot,
|
|
624
|
+
maskEl: d.mask, drawerEl: d.drawer,
|
|
625
|
+
drawerTitleEl: d.title, drawerTimeEl: d.time,
|
|
626
|
+
restoreBtn: d.restore, filesEl: d.files, diffEl: d.diff, diffPathEl: d.diffPath, confirmRow: d.confirmRow,
|
|
627
|
+
};
|
|
629
628
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
629
|
+
const onClose = () => closeDrawer(state);
|
|
630
|
+
const onRestoreClick = () => {
|
|
631
|
+
d.confirmRow.style.display = "flex";
|
|
632
|
+
d.restore.disabled = true;
|
|
633
|
+
};
|
|
634
|
+
const onCancelClick = () => {
|
|
635
|
+
d.confirmRow.style.display = "none";
|
|
636
|
+
d.restore.disabled = false;
|
|
637
|
+
};
|
|
638
|
+
const onGoClick = () => performRestore(state);
|
|
639
|
+
const onMaskClick = () => closeDrawer(state);
|
|
640
|
+
const onKey = (e) => { if (e.key === "Escape" && d.drawer.classList.contains("open")) closeDrawer(state); };
|
|
641
|
+
|
|
642
|
+
d.close.onclick = onClose;
|
|
643
|
+
d.restore.onclick = onRestoreClick;
|
|
644
|
+
d.confirmNo.onclick = onCancelClick;
|
|
645
|
+
d.confirmYes.onclick = onGoClick;
|
|
646
|
+
d.mask.onclick = onMaskClick;
|
|
647
|
+
document.addEventListener("keydown", onKey);
|
|
648
|
+
|
|
649
|
+
_activeState = state;
|
|
650
|
+
_hookWs();
|
|
651
|
+
|
|
652
|
+
loadHistory(state);
|
|
653
|
+
|
|
654
|
+
return {
|
|
655
|
+
state,
|
|
656
|
+
root,
|
|
657
|
+
dispose() {
|
|
658
|
+
document.removeEventListener("keydown", onKey);
|
|
659
|
+
try { d.mask.remove(); } catch (_) {}
|
|
660
|
+
try { d.drawer.remove(); } catch (_) {}
|
|
661
|
+
if (_activeState === state) _activeState = null;
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
},
|
|
665
|
+
render(container, ctx, runtime) {
|
|
666
|
+
container.appendChild(runtime.root);
|
|
667
|
+
_activeState = runtime.state;
|
|
668
|
+
},
|
|
657
669
|
}, {
|
|
658
|
-
panel: "time_machine",
|
|
659
670
|
order: 20,
|
|
660
671
|
tab: { id: "tm", label: () => t("tm.tab") },
|
|
661
672
|
});
|
|
@@ -85,7 +85,7 @@ end
|
|
|
85
85
|
def saved_bot_token
|
|
86
86
|
yml_path = File.expand_path("~/.clacky/channels.yml")
|
|
87
87
|
return nil unless File.exist?(yml_path)
|
|
88
|
-
data = YAML.
|
|
88
|
+
data = YAML.safe_load(File.read(yml_path), permitted_classes: [Symbol], aliases: true) rescue nil
|
|
89
89
|
data&.dig("channels", "discord", "bot_token") || data&.dig(:channels, :discord, :bot_token)
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -5,37 +5,102 @@ description: Customize, fix, override or extend openclacky itself — change a b
|
|
|
5
5
|
|
|
6
6
|
# Extending Openclacky
|
|
7
7
|
|
|
8
|
-
Openclacky ships
|
|
9
|
-
|
|
8
|
+
Openclacky ships one unified extension mechanism — an **extension container**
|
|
9
|
+
declared by a single `ext.yml`. It survives `gem update` and never requires
|
|
10
|
+
editing the gem source.
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
**Never tell the user to `bundle show openclacky` and edit the gem.**
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
## The one entry point
|
|
15
|
+
|
|
16
|
+
Every extension lives in a container directory:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
~/.clacky/ext/local/<id>/
|
|
20
|
+
ext.yml # single manifest — declares everything the container contributes
|
|
21
|
+
panels/… # WebUI panels (JS)
|
|
22
|
+
api/handler.rb # HTTP API backend
|
|
23
|
+
skills/… # AI skills
|
|
24
|
+
agents/… # agent profiles + prompts
|
|
25
|
+
channels/… # IM adapters
|
|
26
|
+
patches/… # runtime method patches
|
|
27
|
+
hooks/… # shell hooks
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Scaffold with:
|
|
31
|
+
```bash
|
|
32
|
+
clacky ext new <id> # minimal hello-panel starter
|
|
33
|
+
clacky ext new <id> --full # kitchen-sink example with every contributes type
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The ext.yml `contributes:` map declares which of these 7 types the container
|
|
37
|
+
provides. A container may use one, several, or all.
|
|
38
|
+
|
|
39
|
+
## Pick what to add to `contributes:`
|
|
40
|
+
|
|
41
|
+
| User wants to… | contributes: field |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Add a **WebUI panel / button / settings tab / data visualisation** | `panels:` |
|
|
44
|
+
| Add an **HTTP API backend** (routes under `/api/ext/<id>/…`) | `api:` (a single `handler.rb`) |
|
|
45
|
+
| **Change behavior of a built-in method** in openclacky (e.g. `WebSearch#execute` timeout) | `patches:` |
|
|
46
|
+
| **Audit / block / observe** tool calls (block `rm -rf /`, log every shell command) | `hooks:` |
|
|
47
|
+
| Plug openclacky into a **new IM platform** (Slack, in-house IM, custom webhook) | `channels:` |
|
|
48
|
+
| Add a **new AI skill** (SKILL.md) | `skills:` |
|
|
49
|
+
| Bundle a **custom agent profile** with its own panels + skills | `agents:` |
|
|
19
50
|
|
|
20
51
|
## Authoritative documentation
|
|
21
52
|
|
|
22
|
-
|
|
53
|
+
Read the relevant reference doc with `web_fetch` before writing code —
|
|
54
|
+
don't guess field names, hook events, adapter methods, or the `Clacky.ext`
|
|
55
|
+
WebUI contract.
|
|
23
56
|
|
|
57
|
+
- Extension containers (ext.yml overview) → https://www.openclacky.com/docs/extend
|
|
58
|
+
- Panels (WebUI) → https://www.openclacky.com/docs/extend-webui
|
|
59
|
+
- API backends → https://www.openclacky.com/docs/extend-api
|
|
24
60
|
- Patches → https://www.openclacky.com/docs/extend-patches
|
|
25
61
|
- Shell Hooks → https://www.openclacky.com/docs/extend-shell-hooks
|
|
26
62
|
- Channel Adapters → https://www.openclacky.com/docs/extend-channel-adapter
|
|
27
|
-
|
|
63
|
+
|
|
64
|
+
## WebUI host services live under `Clacky.*`
|
|
65
|
+
|
|
66
|
+
The single public API surface for WebUI extensions is `window.Clacky`.
|
|
67
|
+
All host services are exposed as properties on it — reach for them there,
|
|
68
|
+
not through bare globals or `window.Xxx`:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
Clacky.Sessions.on("switched", handler); // active session store
|
|
72
|
+
Clacky.Router.go("session"); // top-level view routing
|
|
73
|
+
Clacky.I18n.t("some.key"); // translations
|
|
74
|
+
Clacky.Modal.confirm("Delete?"); // dialogs
|
|
75
|
+
Clacky.Notify.info("Saved"); // toasts
|
|
76
|
+
Clacky.Auth.passed; // auth state
|
|
77
|
+
Clacky.Workspace.list(dir); // working-directory files
|
|
78
|
+
Clacky.Skills.list(); // skill catalog
|
|
79
|
+
Clacky.Backup.load(); // backup/restore state
|
|
80
|
+
Clacky.WS.send({ type: "..." }); // send a WebSocket message to the agent
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Rules:
|
|
84
|
+
|
|
85
|
+
- Prefer `Clacky.Xxx.method(...)` — this is the recommended, forward-stable form.
|
|
86
|
+
- `window.Clacky.Xxx.method(...)` works too and is fine in defensive code.
|
|
87
|
+
- **Never** write `window.Sessions` / `typeof window.Sessions` / `"Sessions" in window`
|
|
88
|
+
— bare host names are `const` bindings, not `window` properties, so those checks
|
|
89
|
+
return `undefined` / `false` even though the module is loaded.
|
|
90
|
+
- The bare form (`Sessions.on(...)`) still works for backwards compatibility
|
|
91
|
+
but is not the pattern to teach or generate.
|
|
28
92
|
|
|
29
93
|
## Execution playbook
|
|
30
94
|
|
|
31
|
-
1. **Identify** which
|
|
32
|
-
2. **Read the doc** for
|
|
33
|
-
3. **
|
|
34
|
-
4. **Edit**
|
|
35
|
-
5. **Verify** with
|
|
95
|
+
1. **Identify** which `contributes:` fields the user's intent needs (use the table above; ask if genuinely ambiguous).
|
|
96
|
+
2. **Read the doc(s)** for those fields. The doc is the contract.
|
|
97
|
+
3. **Scaffold** with `clacky ext new <id>` (or `--full` if the user wants every type wired up as a reference).
|
|
98
|
+
4. **Edit** `ext.yml` to declare the fields, and fill in the referenced files (panel view.js, api handler.rb, patches/xxx.rb, etc.).
|
|
99
|
+
5. **Verify** with `clacky ext verify`. Surface any error/skip lines to the user verbatim.
|
|
100
|
+
6. **Reload** the WebUI page (for panel/api changes take effect on next request — no restart needed).
|
|
36
101
|
|
|
37
102
|
## When NOT to use this skill
|
|
38
103
|
|
|
39
|
-
- The user is building features in their own application that just *use* openclacky — that's normal coding, no
|
|
40
|
-
- The user wants a brand-new tool/skill for *their* project — use `.clacky/skills/` or `.clacky/tools
|
|
104
|
+
- The user is building features in their own application that just *use* openclacky — that's normal coding, no extension container needed.
|
|
105
|
+
- The user wants a brand-new tool/skill for *their* project — use `.clacky/skills/` or `.clacky/tools/` in their project, not a gem-level container.
|
|
41
106
|
- The change can be made via `clacky config set ...` — prefer config over patches.
|
|
@@ -4,16 +4,16 @@ require "json"
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
|
|
6
6
|
module Clacky
|
|
7
|
-
# Base class for
|
|
8
|
-
#
|
|
7
|
+
# Base class for HTTP API extensions declared in an ext.yml container as
|
|
8
|
+
# `contributes.api: <path/to/handler.rb>`. Subclasses use a tiny route DSL
|
|
9
9
|
# (get/post/put/patch/delete) to expose endpoints under
|
|
10
|
-
# /api/ext/<
|
|
10
|
+
# /api/ext/<ext_id>/<sub-path>
|
|
11
11
|
#
|
|
12
12
|
# The framework wires up access-key auth, timeouts, JSON error envelopes,
|
|
13
13
|
# path-parameter parsing, and a curated handler context — extension authors
|
|
14
14
|
# only fill in business logic.
|
|
15
15
|
#
|
|
16
|
-
# Minimal example (~/.clacky/
|
|
16
|
+
# Minimal example (~/.clacky/ext/local/my-dashboard/api/handler.rb):
|
|
17
17
|
#
|
|
18
18
|
# class MyDashboardExt < Clacky::ApiExtension
|
|
19
19
|
# get "/summary" do
|
|
@@ -41,8 +41,8 @@ module Clacky
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
class << self
|
|
44
|
-
#
|
|
45
|
-
# (
|
|
44
|
+
# Keyed by ext_id — every extension contributes at most one API unit
|
|
45
|
+
# (declared as `contributes.api: <path>` in ext.yml).
|
|
46
46
|
def registry
|
|
47
47
|
@registry ||= {}
|
|
48
48
|
end
|
|
@@ -81,6 +81,14 @@ module Clacky
|
|
|
81
81
|
@public_paths ||= []
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Clears accumulated route/public-path state so a force-reload (`load`) of
|
|
85
|
+
# a named-constant handler re-registers its routes from scratch instead of
|
|
86
|
+
# appending duplicates onto the reopened class.
|
|
87
|
+
def reset_routes!
|
|
88
|
+
@routes = []
|
|
89
|
+
@public_paths = []
|
|
90
|
+
end
|
|
91
|
+
|
|
84
92
|
def ext_id
|
|
85
93
|
@ext_id
|
|
86
94
|
end
|
|
@@ -115,7 +123,7 @@ module Clacky
|
|
|
115
123
|
end
|
|
116
124
|
|
|
117
125
|
# Mark a route as not requiring access-key auth. Caller must also
|
|
118
|
-
# declare `public: true`
|
|
126
|
+
# declare `public: true` at ext.yml top level for the framework to honor this.
|
|
119
127
|
def public_endpoint(pattern)
|
|
120
128
|
public_paths << normalize_pattern(pattern)
|
|
121
129
|
end
|
|
@@ -264,9 +272,11 @@ module Clacky
|
|
|
264
272
|
end
|
|
265
273
|
|
|
266
274
|
# Submit a prompt to an existing session for execution.
|
|
267
|
-
#
|
|
268
|
-
# Raises Halt (409) if the session is already running
|
|
269
|
-
|
|
275
|
+
# Returns the session_id on success.
|
|
276
|
+
# Raises Halt (409) if the session is already running and `interrupt: false`.
|
|
277
|
+
# When `interrupt: true`, supersedes the current turn (raises AgentInterrupted
|
|
278
|
+
# on the running thread, waits up to 2s for it to exit) then runs the new task.
|
|
279
|
+
def submit_task(session_id, prompt, display_message: nil, interrupt: false)
|
|
270
280
|
reg = registry
|
|
271
281
|
error!("server not ready", status: 503) unless reg
|
|
272
282
|
|
|
@@ -276,7 +286,13 @@ module Clacky
|
|
|
276
286
|
end
|
|
277
287
|
|
|
278
288
|
session = reg.get(session_id)
|
|
279
|
-
|
|
289
|
+
if session[:status] == :running
|
|
290
|
+
error!("session is busy", status: 409) unless interrupt
|
|
291
|
+
@http_server.send(:interrupt_session, session_id)
|
|
292
|
+
old_thread = nil
|
|
293
|
+
reg.with_session(session_id) { |s| old_thread = s[:thread] }
|
|
294
|
+
old_thread&.join(2)
|
|
295
|
+
end
|
|
280
296
|
|
|
281
297
|
@http_server.send(:run_session_task, session_id, prompt, display_message: display_message)
|
|
282
298
|
session_id
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
# Loads HTTP API extensions from ext.yml containers into the shared
|
|
7
|
+
# Clacky::ApiExtension registry, keyed by ext_id. Every ext contributes
|
|
8
|
+
# at most one api unit (`contributes.api: <path>` in ext.yml), so the
|
|
9
|
+
# registry key is simply the ext id.
|
|
10
|
+
#
|
|
11
|
+
# A broken extension (syntax error, missing base class, no routes) is
|
|
12
|
+
# isolated: skipped with a logged warning, never aborts sibling loads.
|
|
13
|
+
module ApiExtensionLoader
|
|
14
|
+
class << self
|
|
15
|
+
def load_all(reload: false)
|
|
16
|
+
result = Result.new(loaded: [], skipped: [])
|
|
17
|
+
Clacky::ApiExtension.reset_registry!
|
|
18
|
+
handler_mtime_cache.clear
|
|
19
|
+
|
|
20
|
+
loader_result = Clacky::ExtensionLoader.load_all
|
|
21
|
+
loader_result.api.each do |unit|
|
|
22
|
+
container = loader_result.containers[unit.ext_id]
|
|
23
|
+
load_one(unit.ext_id, unit.spec["handler_abs"], unit.dir, container, result, reload: reload)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@last_result = result
|
|
27
|
+
log_summary(result)
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Result = Struct.new(:loaded, :skipped, keyword_init: true)
|
|
32
|
+
|
|
33
|
+
def last_result
|
|
34
|
+
@last_result || load_all
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Per-request hot path used by the dispatcher. Re-loads the handler
|
|
38
|
+
# only when its file mtime has changed since the last load.
|
|
39
|
+
def ensure_fresh(ext_id)
|
|
40
|
+
loader_result = Clacky::ExtensionLoader.load_all
|
|
41
|
+
unit = loader_result.api.find { |u| u.ext_id == ext_id.to_s }
|
|
42
|
+
return unless unit
|
|
43
|
+
|
|
44
|
+
path = unit.spec["handler_abs"]
|
|
45
|
+
current_mtime = File.mtime(path).to_f
|
|
46
|
+
cache = handler_mtime_cache
|
|
47
|
+
return if cache[ext_id.to_s] == current_mtime
|
|
48
|
+
|
|
49
|
+
container = loader_result.containers[unit.ext_id]
|
|
50
|
+
r = Result.new(loaded: [], skipped: [])
|
|
51
|
+
load_one(unit.ext_id, path, unit.dir, container, r, reload: true)
|
|
52
|
+
r.skipped.each { |(id, reason)| log_skip(id, reason) }
|
|
53
|
+
cache[ext_id.to_s] = current_mtime
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
Clacky::Logger.warn("[ApiExtensionLoader] ensure_fresh(#{ext_id}) failed: #{e.message}")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private def handler_mtime_cache
|
|
59
|
+
@handler_mtime_cache ||= {}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def load_one(ext_id, handler_path, ext_dir, container, result, reload: false)
|
|
63
|
+
before = Clacky::ApiExtension.pending_subclasses.size
|
|
64
|
+
|
|
65
|
+
existing = reload ? Clacky::ApiExtension.registry[ext_id] : nil
|
|
66
|
+
existing&.reset_routes!
|
|
67
|
+
|
|
68
|
+
if reload
|
|
69
|
+
old_verbose = $VERBOSE
|
|
70
|
+
$VERBOSE = nil
|
|
71
|
+
begin
|
|
72
|
+
load(handler_path)
|
|
73
|
+
ensure
|
|
74
|
+
$VERBOSE = old_verbose
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
require(handler_path)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
new_subclasses = Clacky::ApiExtension.pending_subclasses[before..] || []
|
|
81
|
+
klass = new_subclasses.last || existing
|
|
82
|
+
|
|
83
|
+
unless klass
|
|
84
|
+
result.skipped << [ext_id, "no Clacky::ApiExtension subclass defined in handler.rb"]
|
|
85
|
+
log_skip(ext_id, result.skipped.last[1])
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
klass.ext_id = ext_id
|
|
90
|
+
klass.ext_dir = ext_dir
|
|
91
|
+
klass.meta = (container && container[:raw]) || {}
|
|
92
|
+
|
|
93
|
+
if klass.routes.empty?
|
|
94
|
+
result.skipped << [ext_id, "no routes declared (use get/post/... DSL)"]
|
|
95
|
+
log_skip(ext_id, result.skipped.last[1])
|
|
96
|
+
Clacky::ApiExtension.registry.delete(ext_id)
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if (gap = validate_public_endpoints(klass, container))
|
|
101
|
+
result.skipped << [ext_id, gap]
|
|
102
|
+
log_skip(ext_id, gap)
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
Clacky::ApiExtension.register(ext_id, klass)
|
|
107
|
+
result.loaded << ext_id
|
|
108
|
+
handler_mtime_cache[ext_id] = File.mtime(handler_path).to_f rescue nil
|
|
109
|
+
public_count = klass.public_paths.size
|
|
110
|
+
suffix = public_count > 0 ? " (#{public_count} public)" : ""
|
|
111
|
+
Clacky::Logger.info("[ApiExtensionLoader] Loaded '#{ext_id}' — #{klass.routes.size} route(s)#{suffix}")
|
|
112
|
+
rescue StandardError, ScriptError => e
|
|
113
|
+
result.skipped << [ext_id, e.message]
|
|
114
|
+
log_skip(ext_id, e.message)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private def validate_public_endpoints(klass, container)
|
|
118
|
+
return nil if klass.public_paths.empty?
|
|
119
|
+
return nil if container && container[:public] == true
|
|
120
|
+
|
|
121
|
+
"uses public_endpoint but ext.yml is missing 'public: true' at top level"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private def log_skip(ext_id, reason)
|
|
125
|
+
Clacky::Logger.warn("[ApiExtensionLoader] Skipped '#{ext_id}': #{reason}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private def log_summary(result)
|
|
129
|
+
return if result.loaded.empty? && result.skipped.empty?
|
|
130
|
+
|
|
131
|
+
total_routes = result.loaded.sum { |id| Clacky::ApiExtension.registry[id]&.routes&.size || 0 }
|
|
132
|
+
Clacky::Logger.info("[ApiExtensionLoader] #{result.loaded.size} extension(s), #{total_routes} route(s); #{result.skipped.size} skipped")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|