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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/lib/clacky/agent.rb +8 -1
  4. data/lib/clacky/agent_profile.rb +121 -53
  5. data/lib/clacky/brand_config.rb +309 -6
  6. data/lib/clacky/cli.rb +4 -57
  7. data/lib/clacky/default_extensions/coding/ext.yml +18 -0
  8. data/lib/clacky/default_extensions/ext-studio/agents/ext-developer/system_prompt.md +95 -0
  9. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +254 -0
  10. data/lib/clacky/default_extensions/ext-studio/ext.yml +32 -0
  11. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +1269 -0
  12. data/lib/clacky/default_extensions/ext-studio/skills/ext-debug/SKILL.md +71 -0
  13. data/lib/clacky/default_extensions/ext-studio/skills/ext-publish/SKILL.md +74 -0
  14. data/lib/clacky/default_extensions/ext-studio/skills/ext-scaffold/SKILL.md +65 -0
  15. data/lib/clacky/default_extensions/general/ext.yml +18 -0
  16. data/lib/clacky/default_extensions/git/ext.yml +12 -0
  17. data/lib/clacky/{default_agents/_panels/git/panel.js → default_extensions/git/panels/git/view.js} +1 -2
  18. data/lib/clacky/default_extensions/meeting/{handler.rb → api/handler.rb} +78 -26
  19. data/lib/clacky/default_extensions/meeting/ext.yml +15 -0
  20. data/lib/clacky/default_extensions/meeting/{meeting.js → panels/meeting/view.js} +266 -89
  21. data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +2 -1
  22. data/lib/clacky/default_extensions/time_machine/ext.yml +12 -0
  23. data/lib/clacky/{default_agents/_panels/time_machine/panel.js → default_extensions/time_machine/panels/time_machine/view.js} +58 -47
  24. data/lib/clacky/default_skills/channel-manager/discord_setup.rb +1 -1
  25. data/lib/clacky/default_skills/extend-openclacky/SKILL.md +83 -18
  26. data/lib/clacky/{api_extension.rb → extension/api_extension.rb} +27 -11
  27. data/lib/clacky/extension/api_loader.rb +136 -0
  28. data/lib/clacky/extension/cli_commands.rb +226 -0
  29. data/lib/clacky/{server/api_extension_dispatcher.rb → extension/dispatcher.rb} +22 -9
  30. data/lib/clacky/extension/hook_loader.rb +77 -0
  31. data/lib/clacky/extension/loader.rb +483 -0
  32. data/lib/clacky/extension/packager.rb +226 -0
  33. data/lib/clacky/{patch_loader.rb → extension/patch_loader.rb} +45 -0
  34. data/lib/clacky/extension/scaffold/template_renderer.rb +64 -0
  35. data/lib/clacky/extension/scaffold/templates/full/README.md.erb +48 -0
  36. data/lib/clacky/extension/scaffold/templates/full/agents/designer.md +5 -0
  37. data/lib/clacky/extension/scaffold/templates/full/api/handler.rb.erb +16 -0
  38. data/lib/clacky/extension/scaffold/templates/full/channels/noop.rb.erb +40 -0
  39. data/lib/clacky/extension/scaffold/templates/full/ext.yml.erb +41 -0
  40. data/lib/clacky/extension/scaffold/templates/full/hooks/audit.rb +9 -0
  41. data/lib/clacky/extension/scaffold/templates/full/panels/dashboard/view.js.erb +194 -0
  42. data/lib/clacky/extension/scaffold/templates/full/patches/audit.rb +15 -0
  43. data/lib/clacky/extension/scaffold/templates/full/skills/__slug__-skill/SKILL.md.erb +8 -0
  44. data/lib/clacky/extension/scaffold/templates/hello/api/handler.rb.erb +9 -0
  45. data/lib/clacky/extension/scaffold/templates/hello/ext.yml.erb +12 -0
  46. data/lib/clacky/extension/scaffold/templates/hello/panels/hello/view.js.erb +31 -0
  47. data/lib/clacky/extension/scaffold.rb +55 -0
  48. data/lib/clacky/extension/verifier.rb +196 -0
  49. data/lib/clacky/identity.rb +70 -0
  50. data/lib/clacky/locales/en.rb +19 -1
  51. data/lib/clacky/locales/zh.rb +19 -1
  52. data/lib/clacky/platform_http_client.rb +18 -15
  53. data/lib/clacky/server/channel/extension_adapter_loader.rb +32 -0
  54. data/lib/clacky/server/channel.rb +6 -0
  55. data/lib/clacky/server/http_server.rb +313 -144
  56. data/lib/clacky/server/session_registry.rb +9 -2
  57. data/lib/clacky/skill_loader.rb +28 -1
  58. data/lib/clacky/ui2/ui_controller.rb +9 -6
  59. data/lib/clacky/version.rb +1 -1
  60. data/lib/clacky/web/app.css +748 -573
  61. data/lib/clacky/web/app.js +63 -15
  62. data/lib/clacky/web/auth.js +2 -0
  63. data/lib/clacky/web/components/notify.js +2 -0
  64. data/lib/clacky/web/components/sidebar.js +1 -5
  65. data/lib/clacky/web/core/ext.js +316 -79
  66. data/lib/clacky/web/features/backup/store.js +1 -0
  67. data/lib/clacky/web/features/backup/view.js +0 -1
  68. data/lib/clacky/web/features/brand/view.js +13 -5
  69. data/lib/clacky/web/features/extensions/store.js +184 -0
  70. data/lib/clacky/web/features/extensions/view.js +389 -0
  71. data/lib/clacky/web/features/new-session/store.js +186 -0
  72. data/lib/clacky/web/features/new-session/view.js +319 -0
  73. data/lib/clacky/web/features/share/view.js +63 -14
  74. data/lib/clacky/web/features/skills/store.js +3 -1
  75. data/lib/clacky/web/features/skills/view.js +1 -1
  76. data/lib/clacky/web/features/tasks/view.js +12 -0
  77. data/lib/clacky/web/features/trash/store.js +24 -6
  78. data/lib/clacky/web/features/trash/view.js +46 -9
  79. data/lib/clacky/web/features/workspace/store.js +2 -0
  80. data/lib/clacky/web/features/workspace/view.js +0 -1
  81. data/lib/clacky/web/i18n.js +118 -106
  82. data/lib/clacky/web/index.html +138 -188
  83. data/lib/clacky/web/sessions.js +64 -269
  84. data/lib/clacky/web/settings.js +1 -1
  85. data/lib/clacky/web/skills.js +2 -0
  86. data/lib/clacky/web/theme.js +2 -0
  87. data/lib/clacky/web/ws-dispatcher.js +1 -0
  88. data/lib/clacky/web/ws.js +2 -0
  89. data/lib/clacky.rb +21 -10
  90. metadata +55 -24
  91. data/benchmark/runner.rb +0 -441
  92. data/lib/clacky/api_extension_loader.rb +0 -168
  93. data/lib/clacky/default_agents/SOUL.md +0 -3
  94. data/lib/clacky/default_agents/USER.md +0 -1
  95. data/lib/clacky/default_agents/coding/profile.yml +0 -5
  96. data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
  97. data/lib/clacky/default_agents/general/profile.yml +0 -2
  98. data/lib/clacky/default_extensions/meeting/meta.yml +0 -3
  99. data/lib/clacky/web/features/creator/store.js +0 -81
  100. data/lib/clacky/web/features/creator/view.js +0 -380
  101. /data/lib/clacky/{default_agents → default_extensions/coding/agents}/coding/system_prompt.md +0 -0
  102. /data/lib/clacky/{default_agents → default_extensions/general/agents}/general/system_prompt.md +0 -0
  103. /data/lib/clacky/{default_agents/base_prompt.md → prompts/base.md} +0 -0
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: ext-debug
3
+ description: Debug an OpenClacky extension that won't load, throws errors, or whose panel/api/skill/agent isn't showing up. Use when the user reports something broken with their extension, when `clacky ext verify` reports issues, or when a change didn't take effect. Reads structured verify errors and fixes manifest and file problems.
4
+ ---
5
+
6
+ # Extension Debug
7
+
8
+ Diagnose and fix a broken extension. Your primary instrument is `clacky ext verify` —
9
+ it's a compiler for extensions: every issue is structured with a `code`, `message`,
10
+ the offending `file`, and a `hint` telling you how to fix it.
11
+
12
+ ## Step 1 — Run verify
13
+
14
+ ```
15
+ clacky ext verify
16
+ ```
17
+
18
+ Read the output line by line. `[OK]` lines confirm resolved units. `[ERR]` blocks a
19
+ load; `[WARN]` is advisory. Each issue looks like:
20
+
21
+ ```
22
+ [ERR] <ext> <unit> (<code>) — <message> [<file>]
23
+ hint: <how to fix>
24
+ ```
25
+
26
+ ## Step 2 — Fix by error code
27
+
28
+ - **`loader.error`** — a file the manifest points at is missing or the container
29
+ couldn't be parsed. Check the `file` path exists and `ext.yml` is valid YAML. A skill
30
+ needs `SKILL.md` under `skills/<id>/`; an agent needs its `prompt` file; a panel needs
31
+ its `view` file; api needs `api/handler.rb`.
32
+ - **`schema.unknown_contributes`** — a top-level key under `contributes:` isn't one of
33
+ `panels api skills agents channels patches hooks`. Fix the spelling.
34
+ - **`schema.unknown_field` / `schema.unknown_key`** — a unit has a field that isn't
35
+ allowed for its type. Remove or rename it. Allowed fields:
36
+ - panel: `id title title_zh description description_zh view order attach`
37
+ - api: `id handler`
38
+ - skill: `id dir protected`
39
+ - agent: `id title title_zh description description_zh order prompt panels skills`
40
+ - channel: `id platform adapter`
41
+ - **`schema.bad_attach`** — a panel `attach:` entry isn't a valid token. Use agent ids
42
+ or `"*"` for all.
43
+ - **`ref.missing_panel`** — an agent references `panels: [id]` that no panel provides.
44
+ Fix the id, or use `<ext_id>/<panel_id>` to reference a panel in another extension.
45
+ - **`ref.missing_skill`** — an agent references `skills: [id]` that no skill provides.
46
+ Fix the id or add the skill.
47
+ - **`ref.missing_attach_agent`** — a panel's `attach:` names an agent that doesn't
48
+ exist. Fix the agent id.
49
+ - **`override`** (warning) — a unit in a higher layer is shadowing a lower one
50
+ (`local > installed > builtin`). Usually intentional; confirm with the user if not.
51
+
52
+ Fix one issue, re-run verify, repeat until clean.
53
+
54
+ ## Step 3 — "It verifies but doesn't show up"
55
+
56
+ If verify is clean but a change isn't visible:
57
+ - **Hot reload is per-request.** After editing `view.js`, `handler.rb`, or a `SKILL.md`,
58
+ the user must **reload the WebUI page** — there's no restart, but a stale tab won't
59
+ update on its own. Editing `ext.yml` also applies on the next load.
60
+ - **Panel not appearing?** Check the panel's `attach:` (or the agent that references it
61
+ via `panels: [id]`). A panel with no `attach` and no referencing agent has nothing to
62
+ mount onto.
63
+ - **API 404?** Routes are relative to `/api/ext/<ext_id>/`. Confirm the handler subclasses
64
+ `Clacky::ApiExtension` and the route pattern matches what `view.js` fetches.
65
+ - **Skill not triggering?** The AI selects skills by their `description`. Make the
66
+ description concrete about WHEN to use it.
67
+
68
+ ## Step 4 — Confirm the fix
69
+
70
+ End with a clean `clacky ext verify` and have the user reload to confirm the behavior
71
+ actually works — don't declare success on "should work."
@@ -0,0 +1,74 @@
1
+ ---
2
+ name: ext-publish
3
+ description: Pack and publish an OpenClacky extension to the marketplace, publish a new version, list published extensions, or unpublish one. Use when the user wants to ship, release, publish, update, or take down an extension. Handles packing, the license requirement, versioning, and the already-published case.
4
+ ---
5
+
6
+ # Extension Publish
7
+
8
+ Take a finished local extension and ship it to the OpenClacky marketplace, then confirm
9
+ it's live.
10
+
11
+ ## Before publishing
12
+
13
+ - The extension must live in the **local** layer (`~/.clacky/ext/local/<id>/`). Only
14
+ local containers can be packed. Marketplace-origin and encrypted (`SKILL.md.enc`)
15
+ containers are rejected.
16
+ - Publishing requires an **activated user license** — it proves creator identity, and
17
+ the platform attributes the extension to that account. If activation is missing, tell
18
+ the user to activate first; don't try to work around it.
19
+ - Run `clacky ext verify` one last time and confirm no errors before shipping.
20
+
21
+ ## Publish (first time)
22
+
23
+ ```
24
+ clacky ext publish <id>
25
+ ```
26
+
27
+ This packs the local container into a zip and uploads it. On success you'll see:
28
+
29
+ ```
30
+ Published <id> v<version> → status=<status>
31
+ ```
32
+
33
+ Options:
34
+ - `--status draft` — publish as a draft (not yet visible on the public marketplace).
35
+ Omit or use `--status published` to go live.
36
+ - `--changelog "..."` — release notes for this version.
37
+
38
+ ## Publish a new version
39
+
40
+ If the extension is already published, a plain `publish` fails with:
41
+
42
+ ```
43
+ Error: <id> already published. Re-run with --force to publish a new version.
44
+ ```
45
+
46
+ Re-run with `--force` (and ideally a `--changelog`) to publish a new version. The patch
47
+ version auto-increments on the platform side.
48
+
49
+ ```
50
+ clacky ext publish <id> --force --changelog "Fixed the weather refresh bug"
51
+ ```
52
+
53
+ ## List your published extensions
54
+
55
+ ```
56
+ clacky ext published
57
+ ```
58
+
59
+ Shows each extension with its latest version, status, and unit summary.
60
+
61
+ ## Unpublish
62
+
63
+ ```
64
+ clacky ext unpublish <id>
65
+ ```
66
+
67
+ Soft-deletes (takes down) one of your published extensions. Confirm with the user
68
+ before doing this — it removes it from the marketplace.
69
+
70
+ ## Wrap up
71
+
72
+ After a successful publish, tell the user the version and status in plain terms, and
73
+ mention they can run `clacky ext published` to see it in their list, or bump a new
74
+ version anytime with `--force`.
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: ext-scaffold
3
+ description: Scaffold a new OpenClacky extension from an idea. Use when the user wants to create, start, or bootstrap a new extension, plugin, panel, agent, or skill container. Maps the idea to the right contributes types and generates a working skeleton in the local layer.
4
+ ---
5
+
6
+ # Extension Scaffold
7
+
8
+ Turn a plain-language idea into a working extension skeleton, then read the generated
9
+ files so you know what you're working with.
10
+
11
+ ## Step 1 — Understand the idea
12
+
13
+ Figure out what the extension should DO and which contributes types it needs. Ask one
14
+ clarifying question only if it's genuinely ambiguous. Common mappings:
15
+
16
+ - "Show me X in a side panel / add a button / dashboard" → **panel** (+ **api** if it
17
+ needs a backend or to call an external service).
18
+ - "A capability the AI can invoke" (summarize, translate, format) → **skill**.
19
+ - "A specialized assistant with its own personality/tools" → **agent** (usually
20
+ bundling its own panels/skills).
21
+ - "Connect to Slack / an in-house IM" → **channel**.
22
+
23
+ Keep it minimal. Most useful extensions are one panel + one handler, or one skill.
24
+ Do NOT add `patches` or `hooks` unless the user explicitly asks — they run arbitrary
25
+ Ruby and carry supply-chain risk.
26
+
27
+ ## Step 2 — Generate the skeleton
28
+
29
+ Pick a lowercase, hyphenated id derived from the idea (e.g. `weather-panel`).
30
+
31
+ ```
32
+ clacky ext new <id>
33
+ ```
34
+
35
+ This creates `~/.clacky/ext/local/<id>/` with a working hello panel + handler:
36
+ - `ext.yml` — the manifest
37
+ - `panels/hello/view.js` — a panel that pings the backend
38
+ - `api/handler.rb` — a `Clacky::ApiExtension` subclass mounted at `/api/ext/<id>/`
39
+
40
+ Use `--full` only if the user needs the kitchen-sink reference exercising all seven
41
+ contributes types — it's a lot to read, so prefer the plain scaffold otherwise.
42
+
43
+ ## Step 3 — Read what was generated
44
+
45
+ Always read the generated `ext.yml`, `view.js`, and `handler.rb` before editing. This
46
+ is your starting point; you'll reshape it to match the idea.
47
+
48
+ ## Step 4 — Reshape to the idea
49
+
50
+ Edit the files into real, working code:
51
+ - Rename the panel id and `view.js` path to match the feature.
52
+ - Update `ext.yml` `contributes:` — add `skills:`/`agents:` blocks if needed. A skill
53
+ is a `SKILL.md` under `skills/<id>/`; an agent is a `system_prompt.md` that can
54
+ reference `panels: [id]` and `skills: [id]`.
55
+ - In `view.js`, reuse host CSS classes (`btn-primary`, `btn-secondary`, `form-input`,
56
+ `form-textarea`, `form-label`) so the panel inherits the theme automatically.
57
+ - In `handler.rb`, define routes relative to the `/api/ext/<id>/` mount.
58
+
59
+ ## Step 5 — Confirm it loads
60
+
61
+ Run `clacky ext verify` and confirm the new units resolve with no errors. Then tell
62
+ the user to reload the WebUI page — panels and api changes are live on the next request,
63
+ no restart needed.
64
+
65
+ If verify reports problems, switch to debugging (the ext-debug skill covers this).
@@ -0,0 +1,18 @@
1
+ id: general
2
+ name: General
3
+ description: General-purpose digital employee agent
4
+ version: "0.1.0"
5
+ author: OpenClacky
6
+ homepage: https://www.openclacky.com
7
+ license: MIT
8
+ origin: self
9
+ contributes:
10
+ agents:
11
+ - id: general
12
+ title: General
13
+ title_zh: 通用
14
+ description: A versatile digital employee living on your computer
15
+ description_zh: 驻留在你电脑里的多面手数字员工
16
+ order: 1
17
+ prompt: agents/general/system_prompt.md
18
+ panels: [meeting/meeting]
@@ -0,0 +1,12 @@
1
+ id: git
2
+ name: Git
3
+ description: Changes panel — a friendly view of what the AI changed in the working tree
4
+ version: "0.1.0"
5
+ author: OpenClacky
6
+ homepage: https://www.openclacky.com
7
+ license: MIT
8
+ origin: self
9
+ contributes:
10
+ panels:
11
+ - id: git
12
+ view: panels/git/view.js
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // "改动 / Changes": a non-technical view of what the AI changed, backed by the
4
4
  // built-in git API (GET/POST /api/sessions/:id/git/*). Mounted as a tab in the
5
- // "session.aside" slot, scoped to agents declaring `panels: [git]`.
5
+ // "session.aside" slot, attached to agents via `attach: [coding]` in ext.yml.
6
6
  //
7
7
  // Deliberately hides git jargon: no porcelain status codes (M/??), no
8
8
  // branch/ahead/behind unless the branch is NOT the main line (main/master) —
@@ -194,7 +194,6 @@
194
194
  refresh(ctx.sessionId, root, body, ctx);
195
195
  return root;
196
196
  }, {
197
- panel: "git",
198
197
  order: 10,
199
198
  tab: { id: "changes", label: () => t("changes.tab") },
200
199
  });
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "set"
4
5
  require "fileutils"
5
6
  require "base64"
6
7
  require "tmpdir"
@@ -12,7 +13,10 @@ class MeetingExt < Clacky::ApiExtension
12
13
 
13
14
  MEETINGS_ROOT = File.expand_path("~/.clacky/meetings")
14
15
  VOCABULARY_PATH = File.join(MEETINGS_ROOT, "vocabulary.txt")
15
- DEFAULT_VOCABULARY = "Clacky, OpenClacky, openclacky"
16
+ # Always injected into STT vocabulary — these are our own wake words / brand
17
+ # names and must not be droppable by the user's saved list.
18
+ SYSTEM_VOCABULARY = "Clacky, 小克".freeze
19
+ DEFAULT_VOCABULARY = "Clacky, 小克, OpenClacky, openclacky"
16
20
 
17
21
  # annotate is a read-only analysis: block every side-effecting tool so the
18
22
  # forked subagent can only read/think, never write files, run commands,
@@ -56,12 +60,15 @@ class MeetingExt < Clacky::ApiExtension
56
60
  end
57
61
 
58
62
  # POST /api/ext/meeting/end
59
- # body: { session_id, meeting_id }
63
+ # body: { session_id, meeting_id, display_message? }
60
64
  # Finalizes the meeting and triggers summarization via the session agent.
61
65
  post "/end" do
62
66
  sid, mid = json_body.values_at("session_id", "meeting_id")
63
67
  error!("session_id and meeting_id required", status: 422) unless sid && mid
64
68
 
69
+ display_message = json_body["display_message"].to_s
70
+ display_message = "🛑 Meeting ended — generating meeting minutes…" if display_message.strip.empty?
71
+
65
72
  dir = meeting_dir(sid, mid)
66
73
  error!("meeting not found", status: 404) unless File.directory?(dir)
67
74
 
@@ -71,33 +78,19 @@ class MeetingExt < Clacky::ApiExtension
71
78
  File.write(meta_path, JSON.pretty_generate(meta))
72
79
 
73
80
  transcript_path = File.join(dir, "transcript.jsonl")
74
- lines = File.readlines(transcript_path).map { |l| JSON.parse(l)["text"] }.reject(&:empty?)
81
+ lines = File.readlines(transcript_path).map { |l| JSON.parse(l)["text"] }.reject { |t| t.nil? || t.strip.empty? }
75
82
  transcript = lines.join("\n")
76
83
 
77
84
  logger.info("end: sid=#{sid} mid=#{mid} lines=#{lines.size} transcript_len=#{transcript.length}")
78
85
 
79
- if transcript.strip.empty?
80
- logger.warn("end: transcript is empty, skipping summarization")
81
- json(ok: true, meeting_id: mid, skipped: true)
82
- next
83
- end
84
-
85
86
  prompt = <<~PROMPT
86
- A meeting just ended. Invoke the "meeting-summarizer" skill to generate the meeting minutes from the transcript below.
87
+ A meeting just ended. Your ONLY next action is to call the tool `invoke_skill` with `name: "meeting-summarizer"` and pass the transcript below as the input. Do not use any other tool. Do not answer directly. Do not open a browser or run shell commands.
87
88
 
88
89
  Transcript:
89
90
  #{transcript}
90
91
  PROMPT
91
92
 
92
- begin
93
- submit_task(sid, prompt, display_message: "🛑 Meeting ended — generating meeting minutes…")
94
- logger.info("end: submit_task succeeded sid=#{sid}")
95
- rescue => e
96
- logger.error("end: submit_task failed sid=#{sid} error=#{e.message}")
97
- json(ok: false, meeting_id: mid, error: e.message)
98
- next
99
- end
100
-
93
+ submit_task(sid, prompt, display_message: display_message, interrupt: true)
101
94
  json(ok: true, meeting_id: mid)
102
95
  end
103
96
 
@@ -116,16 +109,19 @@ class MeetingExt < Clacky::ApiExtension
116
109
 
117
110
  mime = json_body["mime_type"].to_s.split(";").first.strip
118
111
  mime = "audio/webm" if mime.empty?
119
- vocabulary = json_body["vocabulary"].to_s.strip
112
+ vocabulary = merge_vocabulary(json_body["vocabulary"])
120
113
  result = call_stt(audio_b64, mime, vocabulary)
121
114
 
122
115
  if result["success"]
123
116
  text = result["text"].to_s.strip
124
- unless text.empty?
117
+ transcript_path = File.join(dir, "transcript.jsonl")
118
+ if !text.empty? && !hallucinated_transcript?(text, vocabulary, transcript_path)
125
119
  entry = { ts: Time.now.utc.iso8601, text: text }
126
- File.open(File.join(dir, "transcript.jsonl"), "a") { |f| f.puts(JSON.generate(entry)) }
120
+ File.open(transcript_path, "a") { |f| f.puts(JSON.generate(entry)) }
121
+ json(text: text)
122
+ else
123
+ json(text: "")
127
124
  end
128
- json(text: text)
129
125
  else
130
126
  error!(result["error"] || "STT failed", status: 502)
131
127
  end
@@ -138,12 +134,22 @@ class MeetingExt < Clacky::ApiExtension
138
134
  # Submits a question to the session agent with recent transcript as context.
139
135
  post "/ask" do
140
136
  sid, mid = json_body.values_at("session_id", "meeting_id")
141
- question = json_body["question"]
142
- error!("session_id, meeting_id, question required", status: 422) unless sid && mid && question
137
+ question = json_body["question"].to_s.strip
138
+ error!("session_id, meeting_id, question required", status: 422) unless sid && mid && !question.empty?
143
139
 
144
140
  dir = meeting_dir(sid, mid)
145
141
  error!("meeting not found", status: 404) unless File.directory?(dir)
146
142
 
143
+ meaningful = question.gsub(/[^\p{L}\p{N}]/, "")
144
+ if meaningful.length < 4
145
+ logger.warn("ask: question too short, dropping (question=#{question.inspect})")
146
+ next json(ok: false, dropped: true, reason: "question_too_short")
147
+ end
148
+ if question.match?(/\A(open)?clacky\z/i) || question.match?(/\A小?克\z/) || question.match?(/\A克拉奇\z/)
149
+ logger.warn("ask: question is a bare wake/brand word, dropping (question=#{question.inspect})")
150
+ next json(ok: false, dropped: true, reason: "question_is_brand_word")
151
+ end
152
+
147
153
  context = recent_transcript(dir, minutes: 5)
148
154
 
149
155
  prompt = <<~PROMPT
@@ -157,7 +163,7 @@ class MeetingExt < Clacky::ApiExtension
157
163
  #{question}
158
164
  PROMPT
159
165
 
160
- submit_task(sid, prompt, display_message: "🎤 #{question}")
166
+ submit_task(sid, prompt, display_message: "🎤 #{question}", interrupt: true)
161
167
  json(ok: true)
162
168
  end
163
169
 
@@ -286,6 +292,52 @@ class MeetingExt < Clacky::ApiExtension
286
292
  saved.empty? ? "" : saved
287
293
  end
288
294
 
295
+ private def merge_vocabulary(user_terms)
296
+ parts = SYSTEM_VOCABULARY.split(/\s*,\s*/) + user_terms.to_s.split(/\s*,\s*/)
297
+ parts.map(&:strip).reject(&:empty?).uniq.join(", ")
298
+ end
299
+
300
+ HALLUCINATION_PHRASES = Set.new(%w[
301
+ no no. yes yes. ok okay you bye . .. ...
302
+ thanks thank\ you
303
+ uh um hmm mm mm-hmm yeah yeah. yep but and so oh ah ahh huh
304
+ an a i the more well right hi hey wow
305
+ 嗯 啊 哦 呃 谢谢 谢谢观看 谢谢大家 好 好的 对
306
+ ]).freeze
307
+
308
+ DEDUP_WINDOW_SECONDS = 3.0
309
+
310
+ private def hallucinated_transcript?(text, vocabulary, transcript_path)
311
+ normalized = normalize_transcript(text)
312
+ return true if normalized.empty?
313
+ return true if HALLUCINATION_PHRASES.include?(normalized)
314
+ return true if only_vocabulary_term?(normalized, vocabulary)
315
+ return true if recent_duplicate?(normalized, transcript_path)
316
+ false
317
+ end
318
+
319
+ private def normalize_transcript(text)
320
+ text.to_s.downcase.gsub(/[[:space:][:punct:]]+/, " ").strip
321
+ end
322
+
323
+ private def only_vocabulary_term?(normalized, vocabulary)
324
+ terms = vocabulary.to_s.split(/\s*,\s*/).map { |t| normalize_transcript(t) }.reject(&:empty?)
325
+ terms.include?(normalized)
326
+ end
327
+
328
+ private def recent_duplicate?(normalized, transcript_path)
329
+ return false unless File.exist?(transcript_path)
330
+
331
+ cutoff = Time.now.utc - DEDUP_WINDOW_SECONDS
332
+ File.foreach(transcript_path).to_a.last(5).any? do |line|
333
+ entry = JSON.parse(line) rescue nil
334
+ next false unless entry
335
+ ts = Time.parse(entry["ts"].to_s) rescue nil
336
+ next false unless ts && ts >= cutoff
337
+ normalize_transcript(entry["text"]) == normalized
338
+ end
339
+ end
340
+
289
341
  private def recent_transcript(dir, minutes: 5)
290
342
  path = File.join(dir, "transcript.jsonl")
291
343
  return "" unless File.exist?(path)
@@ -0,0 +1,15 @@
1
+ id: meeting
2
+ name: Meeting
3
+ description: Real-time meeting transcription and AI assistant
4
+ version: "0.1.0"
5
+ author: OpenClacky
6
+ homepage: https://www.openclacky.com
7
+ license: MIT
8
+ origin: self
9
+ contributes:
10
+ api: api/handler.rb
11
+ panels:
12
+ - id: meeting
13
+ view: panels/meeting/view.js
14
+ skills:
15
+ - id: meeting-summarizer