fanluc 2.0.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +83 -0
  2. package/dist/assets/{abnfDiagram-O67JEVCF-hiYMdxpV.js → abnfDiagram-O67JEVCF-Di2iDQfl.js} +1 -1
  3. package/dist/assets/{arc-DokRg_wy.js → arc-wXq_yD5s.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-UzHYe9Bx.js → architectureDiagram-NJMV4G6O-CTXbg-3W.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-DqM5UILT.js → blockDiagram-BEXU5L5S-DSDoL3RP.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-Cxkdm7sV.js → c4Diagram-YGBWAQC7-3sFM3iyw.js} +1 -1
  7. package/dist/assets/channel-atnI5OIR.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-Nl4zmLlG.js → chunk-2Q5K7J3B-DUOKiJNI.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BCol78O2.js → chunk-3FUC2YCW-ablWb8_c.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-1u4YRv2Y.js → chunk-5VM5RSS4-qmlSVZ_1.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BG9YIV2h.js → chunk-GTNCS2PH-PLlC7cNS.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BTKfkJAH.js → chunk-GWA4HPMP-DK-1a6YJ.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DCjElfA7.js → chunk-JWPE2WC7-D9zynFKs.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-QoJORlVn.js → chunk-NETBCI7D-BvzPMYyE.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-Da-T8ct9.js → chunk-WEXAMYUT-CzcfZFWB.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-CLu69Uei.js → chunk-XXDRQBXY-DznN-rwG.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DIXavbI9.js → classDiagram-v2-NBCMYWYE-D4jem0hV.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-DuWclG8-.js → cose-bilkent-JH36ORCC-VALsUQJS.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-D2CG5-xd.js → cynefin-OW5HDTMX-DK77QSI8.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-goI7_Qfg.js → cynefinDiagram-VND7K2PF-CdYXfGNR.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-BmOaZ0cm.js → dagre-6A5THRUB-DeeUQzDV.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-DjDJFMw3.js → diagram-22UHCM2B-B9shZ914.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-J6z81NH3.js → diagram-3UASUU5V-gSHvaXBI.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-B1MPU8ai.js → diagram-ATOU4E4O-BrCI5HzS.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-X5Uss0Yv.js → diagram-CDSNMT55-XtVlMRq0.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-CRDqfYy1.js → diagram-MLGK6HIB-BVa6KsvA.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-bhJtyiOq.js → diagram-MPIPVDR6-C8P23P2T.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-Dn1mZAV8.js → ebnfDiagram-ZINNZB2B-BrooaQj5.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-C93BlBEU.js → elk-276RUBZZ-B-qiKlkF.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-PuzjAvhD.js → erDiagram-OPXOYQCR-CL1bp2Mf.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-oRhFgO0p.js → flowDiagram-KWPJA3E3-BMQvaQ-5.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-DKiU1XjU.js → ganttDiagram-FUAMR5RP-PLnTGRI8.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-CWBMI0vb.js → gitGraphDiagram-X574FWY7-CiTN3Joc.js} +1 -1
  34. package/dist/assets/index-BEKLwzQT.css +1 -0
  35. package/dist/assets/index-C8gDJtDK.js +2841 -0
  36. package/dist/assets/{infoDiagram-VRGFBTTK-C5Hd4Afz.js → infoDiagram-VRGFBTTK-ComkS_96.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-BdD6ulRK.js → ishikawaDiagram-OU5B5YK6-D-dNoDFJ.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-CzT-uvks.js → journeyDiagram-ZHPQQLJL-BMm9BQM8.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-BkrazFMR.js → kanban-definition-PNTS6WVX-D5If-l_D.js} +1 -1
  40. package/dist/assets/{linear-B0wSM1Dr.js → linear-DXAHb0Ag.js} +1 -1
  41. package/dist/assets/{mindmap-definition-NLK3R4M7-Df1Acqkf.js → mindmap-definition-NLK3R4M7-CIdwCXTJ.js} +1 -1
  42. package/dist/assets/{pegDiagram-GJSIUBJH-DK_F7h_0.js → pegDiagram-GJSIUBJH-MY7FoVQh.js} +1 -1
  43. package/dist/assets/{pieDiagram-5QR66LMP-BMXyNb9R.js → pieDiagram-5QR66LMP-C7lWWAtM.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-O4NWA36T-Dfu7Z8so.js → quadrantDiagram-O4NWA36T-jHqCwvi8.js} +1 -1
  45. package/dist/assets/{railroadDiagram-XR7U4H2S-CfnBD-aQ.js → railroadDiagram-XR7U4H2S-BwoIA8Ln.js} +1 -1
  46. package/dist/assets/{requirementDiagram-PLB6GJNP-DrvsZJPP.js → requirementDiagram-PLB6GJNP-Cw8w0Rwb.js} +1 -1
  47. package/dist/assets/{sankeyDiagram-IPEJSGJF-DvdVDpT1.js → sankeyDiagram-IPEJSGJF-DTRo1Yxy.js} +1 -1
  48. package/dist/assets/{sequenceDiagram-PO4LG4MO-D1UTrvZH.js → sequenceDiagram-PO4LG4MO-DqZddXsc.js} +1 -1
  49. package/dist/assets/{sizeCapture-INFHLROL-CJrOBcai.js → sizeCapture-INFHLROL-BAEQ4XP5.js} +1 -1
  50. package/dist/assets/{stateDiagram-v2-GCMORJYK-CX8VmtqD.js → stateDiagram-v2-GCMORJYK-CYB4CouD.js} +1 -1
  51. package/dist/assets/{swimlanes-2SLR337P-DUWmhiK6.js → swimlanes-2SLR337P-Dw53LU13.js} +1 -1
  52. package/dist/assets/swimlanesDiagram-TC7HE7FX-Dzkep6h_.js +8 -0
  53. package/dist/assets/{timeline-definition-EJHVYXUP-CBovVdOU.js → timeline-definition-EJHVYXUP-Chk-unGo.js} +1 -1
  54. package/dist/assets/{usecaseDiagram-POWQR4AR-LD5N5BAG.js → usecaseDiagram-POWQR4AR-D_JpD-4l.js} +1 -1
  55. package/dist/assets/{vennDiagram-UO4OBE2U-DdwfzB3A.js → vennDiagram-UO4OBE2U-BnPO8gk8.js} +1 -1
  56. package/dist/assets/{wardleyDiagram-VNRHLVJA-BMHK4bav.js → wardleyDiagram-VNRHLVJA-CD0z1l8R.js} +1 -1
  57. package/dist/assets/{xychartDiagram-PMCCYNJV-q1BNzMC-.js → xychartDiagram-PMCCYNJV-CHEqj1Pe.js} +1 -1
  58. package/dist/cli.js +52 -57
  59. package/dist/index.html +4 -4
  60. package/dist/server.cjs +2144 -841
  61. package/dist/server.cjs.map +4 -4
  62. package/dist/tui.js +3220 -3310
  63. package/docs/fanluc_ui.md +120 -40
  64. package/docs/guide-connect.md +233 -4
  65. package/docs/run-cmd-security.md +26 -0
  66. package/index.html +2 -2
  67. package/mcp.json +3 -0
  68. package/package.json +88 -92
  69. package/searxng/docker-compose.yml +3 -3
  70. package/vite.config.ts +2 -2
  71. package/dist/assets/channel-SPfNTvmj.js +0 -1
  72. package/dist/assets/index-BylpTP-E.css +0 -1
  73. package/dist/assets/index-DcpVteR3.js +0 -3237
  74. package/dist/assets/permissionHelper--88tJJlz.js +0 -3
  75. package/dist/assets/swimlanesDiagram-TC7HE7FX-DMQy1553.js +0 -8
package/docs/fanluc_ui.md CHANGED
@@ -1,42 +1,122 @@
1
- # FANLUC Interface — UI Description
2
-
3
- ## Overview
4
- The screenshot shows a minimalist, wireframe-style chat application named **FANLUC**. The design uses a black-and-white aesthetic with a serif typeface, bracketed labels (e.g., `[BUTTON]`), and simple bordered boxes instead of modern rounded UI components — giving it a retro, text-terminal-like appearance.
5
-
6
- ## Layout Breakdown
7
-
8
- ### 1. Top Navigation Bar
9
- - **Left side:**
10
- - `[HISTORY]` — a bordered button, likely opens a list of past conversations.
11
- - **FANLUC** — the application's logo/name, displayed in bold serif font next to the History button.
12
- - **Right side:**
13
- - `[SETUP_GUIDE]` — a bordered button, presumably links to onboarding or setup instructions.
14
- - `[SETTINGS]` — a bordered button for app configuration.
15
- - `[DK]` — a bordered button.
16
- - A thin horizontal black line separates the top bar from the main content area.
17
-
18
- ### 2. Main Content Area (Center)
19
- - Large, centered greeting text: **"Hey there"**, displayed in a large serif font.
20
- - The rest of this area is empty white space, suggesting this is a fresh/new conversation screen with no messages yet.
21
-
22
- ### 3. Bottom Input Section
23
- Separated from the main content by a thick horizontal black line.
24
-
25
- - **Message input box:**
26
- - A large rectangular text field spanning most of the width.
27
- - Placeholder text: *"Write a message..."*
28
- - A small resize handle icon in the bottom-right corner of the box.
29
-
30
- - **Below the input box:**
31
- - `+ [ATTACH_FILE]` — a bordered button on the left with a "+" icon, allowing file attachments.
32
- - Helper text next to it: *"(Text <= 15KB | Image <= 15MB)"* — indicating file size limits for text and image attachments.
33
- - `[SEND]` — a gray/disabled-looking bordered button on the far right, used to send the message (appears inactive until text is entered).
34
-
35
- ## Design Style Notes
36
- - Monochrome (black, white, gray) color scheme.
37
- - Serif typography throughout (buttons and headings), giving a formal/literary feel contrasted with the bracket-style button labels typical of minimalist or developer-oriented UIs.
38
- - Sharp-cornered rectangular borders (no rounded corners) on all interactive elements.
39
- - Generous whitespace in the main chat area, keeping focus on the message input at the bottom.
1
+ # FANLUC Interface — Detailed UI Description
2
+
3
+ > Read this to know exactly what the human user SEES on screen while chatting with you, and what happens on screen when YOU run a command. You cannot see the UI yourself, so rely on the exact labels below when guiding the user.
4
+
5
+ ## 0. TL;DR
6
+ - FANLUC is a retro, terminal-like chat web app. Black & white, sharp rectangular borders, ALL-CAPS bracketed labels like `[SEND]`, `[SETTINGS]`.
7
+ - Text is drawn in mono fonts, each character randomly picked from Courier Prime / VT323 / Share Tech Mono / Inconsolata (so text looks "glitchy-mixed"). Serif Lora is only used if the user enables it in Settings.
8
+ - Light mode = white background, black text. Dark mode = inverted. Toggle button: `[DK]` (switches to dark) / `[LT]` (switches to light).
9
+ - The user NEVER sees raw command syntax (`search(...)`, `fetch(...)`, `run_cmd{...}` ...). They only see a status line, a collapsible `TOOL LOGS` box, and result cards. Tool results come back to YOU as a hidden next message.
10
+ - Exceptions to the "sharp black/white" style: Weather card (rounded, colored illustration), Translation / Chart / Pie cards (dark zinc, rounded).
11
+
12
+ ## 1. Screen layout (default)
13
+ ```
14
+ +--------------------------------------------------------------+
15
+ | [HISTORY] FANLUC [FILES] [SETUP_GUIDE] [SETTINGS] [DK]| <- header, 2px bottom border, sticky
16
+ +--------------------------------------------------------------+
17
+ | |
18
+ | Hi! (big serif greeting) | <- empty chat only
19
+ | |
20
+ +==============================================================+ <- 2px top border of input area
21
+ | +----------------------------------------------------------+ |
22
+ | | Type your message... | | <- textarea, 2px border
23
+ | +----------------------------------------------------------+ |
24
+ | + [ATTACH_FILE] (All files <= 2MB | Image <= 15MB) [CLEAR][SEND] |
25
+ +--------------------------------------------------------------+
26
+ ```
27
+ - Content is centered, max width ~ 6xl. Header is sticky at top, input area sticky at bottom.
28
+ - Optional black bar `[PERF_TELEMETRY]` (dev HUD) can appear right under the header.
29
+
30
+ ## 2. Header
31
+ - Left: `[HISTORY]` (becomes `[HIDE_HIST]` while sidebar is open) toggles the chat-session sidebar. Next to it the logo text **FANLUC** (uppercase, bold, letter-spaced).
32
+ - Right (desktop): `[FILES]` or `[FILES (n)]` (opens the WORKSPACE FILES panel, n = files count), `[SETUP_GUIDE]`, `[SETTINGS]`, `[DK]`/`[LT]`.
33
+ - Mobile: header wraps into two rows; the theme button sits on the top row, the other buttons share the second row equally.
34
+ - All buttons: 1px border, uppercase bold, hover = colors invert.
35
+
36
+ ## 3. Empty state
37
+ Big centered greeting in Lora serif (3xl-6xl), no messages. Chosen randomly per chat: `Hi!`, `Sup Ya?`, plus time-based (`Morning!` 5-12h, `Afternoon!` 12-18h, `Evening!` 18-23h, `Is something up tonight?` 23-5h) and seasonal (Halloween question, `Merry Christmas!`, `Happy New Year!`, `Are you going anywhere this summer?` in Jun-Aug).
38
+ Special case: if the user's first message is just "hi" or "hello", the app answers locally ("Hi! How can I help you today?") without calling you.
39
+
40
+ ## 4. Input area
41
+ - Textarea, placeholder is one of "Type your message...", "Type a message...", "Write your message...", "Write a message...". Auto-grows up to 140px. Enter = send, Shift+Enter = new line. Disabled while you are answering.
42
+ - `+ [ATTACH_FILE]`: one file at a time. Any file <= 2MB, images <= 15MB (hint text hidden on small screens). Text files are read as text, binary files as base64, images shown to you as vision input. Errors appear in a dashed box: `[SIZE_LIMIT_EXCEEDED] ...` with `[CLEAR]`.
43
+ - After attaching: preview row with 40px thumbnail (or `[TXT]` tile), file name, `TYPE | size (Limit: ...)`, and `[REMOVE_FILE]`.
44
+ - `[CLEAR]` appears when there is text/attachment. `[SEND]` is a filled black button (white in dark mode), 40% opacity when there is nothing to send. While you are generating it turns into `[STOP]`.
45
+ - `[↓ BOTTOM]` floating button appears above the input when the user scrolled up.
46
+
47
+ ## 5. Messages
48
+ - **User bubble:** inverted colors (black bg / white text), pushed right. Header row: `[USER]`, time HH:MM:SS, `[EDIT]` (edit + `[RESEND]`/`[CANCEL]`), `[COPY]`. Attachments show `[ATTACHED_FILE: name]` with image preview or first 500 chars of text + `[CONTINUED]`. Messages > 6 lines are folded: `[SHOW +N LINES]` / `[HIDE]`.
49
+ - **Assistant bubble:** outlined (white bg / black border), pushed left. Header row: `[MODEL NAME]` (from Settings), time, `[BRANCH]` (fork chat from here), `[COPY]`.
50
+ - Inside an assistant bubble, top to bottom:
51
+ 1. Thinking block (if the model uses think tags): `Thinking...` while streaming, then collapsed `Thought for Ns`; expands into `Step 1 · 3s`, `Step 2 ...`.
52
+ 2. Text segments (Markdown: uppercase h1/h2/h3, tables with borders, blockquote with left bar, code blocks) interleaved with `TOOL LOGS` boxes, in the order you did things.
53
+ 3. Cards (map, step guide, tab, chart, pie, translation, weather, images).
54
+ 4. `[CREATED_OR_MODIFIED_FILES (n)]` file cards.
55
+ 5. `[SOURCES: n]` button (URLs you fetched).
56
+ 6. While streaming: a small spinning MSH logo GIF (black version in light mode, inverted in dark mode).
57
+ - Code blocks: black box with `[LANG]` label and `[COPY]`; > 10 lines are collapsed with `[SHOW: N LINES]`/`[HIDE]`.
58
+ - API error: dashed-border inverted box `[API_CONNECTION_ERROR]` + details + `[RETRY]` and `[VIEW_SETUP_GUIDE]`.
59
+
60
+ ## 6. What the user sees when YOU run a command
61
+ While a command runs, a plain status line appears under the last message (no spinner icon). After the turn, a collapsed `TOOL LOGS` box (click to expand) lists one row per command with an icon. Consecutive commands without text between them are merged in one box.
62
+
63
+ | Your command | Status line / TOOL LOGS row | What the user sees as result | What you receive back |
64
+ |---|---|---|---|
65
+ | `search("q")(n)` | `Searching the web: "q"...` | Nothing but the log row | `N results:` then lines `url: snippet` |
66
+ | `fetch(url)(len)` | `Fetching URL ...` | `[SOURCES: n]` button under the message (favicons; expands to domain + link + `[COPY]`) | `fetched url: ...`, `title: ...`, `text: ...` (cut to `len`, max 10000) |
67
+ | `image_search(query, count)` | `Searching images: "q"...` | Card `[IMAGE_SEARCH] "q" — N images`: black header bar, 2-3 column square thumbnails (max 6), click opens the image | one-line summary only (no URLs) |
68
+ | `show_weather(location)` | `Fetching weather for X...` | Weather card (see 7). `auto` triggers a browser geolocation permission popup | `[WEATHER RESULT: City - 30°C, Sunny]` |
69
+ | `translate(text,to)` | `Translating text: "..."...` | Translation card (see 7) | `translated "..." (from -> to): "..."` |
70
+ | `show_map([...])` | `Rendering map: ...` | Map card (see 7) | confirmation |
71
+ | `run_cmd{...}` | `Running: <first 45 chars>` | Files it creates appear as file cards and in `[FILES]` | `$ cmd` + real stdout/stderr |
72
+ | `write_file / edit_file / read_file / delete_file / move_file / glob / grep / git_diff / git_checkout` | `Creating file x...`, `Editing file x...`, `Reading file x...`, `Deleting...`, `Moving a -> b...`, `Listing directory...`, `Searching inside files...`, `Checking git diff...`, `Reverting file...` | File cards for created/modified files | result text |
73
+ | `read_docs(name)` | `Reading doc name...` | log row only | doc content |
74
+ | `list_skills` / `load_skills(id)` | `Listing available skills...` / `Loading skill(s): id...` | log row only | skill text |
75
+ | `list_history / see_history / list_chat_keywords` | `Reading chat history (part n)...` etc. | log row only | history text |
76
+ | `mcp_call(server, tool, args)` | `Calling MCP server.tool...` | log row only | tool result |
77
+ | `ask("Q")-sam(opt1, opt2)` | — | Section `[QUICK RESPONSE REQUIRED]`: one question at a time `Q1/2: ...` with option buttons; the picked answer is sent automatically as the user's next message | the answer text |
78
+ Failures show status `[FETCH_URL_FAILED]` or `[COMMAND_ERROR]` briefly; you get an error string.
79
+
80
+ ## 7. Native cards (write them directly in chat, do not repeat their content as plain text)
81
+ - **Map card** (`show_map`): full-width bordered box, gray title bar (title or "Map Card", `1/N` counter), 360-400px tall. With > 1 place: left list "Places (N)" with numbered squares, right embedded Google Map; click a place to switch.
82
+ - **Step guide** (`|Step-by-step guide|` block, 3-20 steps): one step at a time, centered text, footer `< 1/5 >`.
83
+ - **Tab card** (`|Tab display card|`, 2-8 tabs): tab strip on top, content below (supports `(checkbox)` items), footer `TAB 1/3` plus mode buttons: `[COPY]`, or `[PREV]`/`[NEXT]`/`[DONE]`→`[COMPLETED]` (need_complete), or `<` `>` (none).
84
+ - **Chart card** (`chart`, line/bar/scatter): dark zinc rounded card, title, top-right toggle pill (chart icon / table icon), SVG chart with hover tooltip and legend.
85
+ - **Pie card** (`pie_chart`): same dark style, donut/pie, legend with percentages, table toggle (Category / Value / Share %).
86
+ - **Translation card**: dark rounded card, optional title, two columns `FROM` code | `TO` code (e.g. `EN` | `VI`), each with speaker (text-to-speech) and copy icons; stacked vertically on mobile.
87
+ - **Weather card**: 300px wide white rounded card. Top 148px = animated colored illustration by condition (sunny, cloudy, windy, fog, snow, rain/storm) with big temperature `32°`, condition and location in white text; bottom strip shows time (left) and date (right).
88
+ - **Image search card**: see table above.
89
+ - **File card**: bordered row with icon (globe=HTML, file=doc, image=SVG, code), file name, category like `Code · HTML`, `DOWNLOAD` button; `DOWNLOAD ALL` when several. Click opens the file in the right panel.
90
+ - `/mcps` shows an MCP status panel (server list with `[ON]`/`[OFF]`, `[DELETE]`, `[EDIT]`, `[RELOAD]`, `[+ ADD SERVER]`, SearXNG URL field with `[SAVE]`).
91
+
92
+ ## 8. Files & artifact panel
93
+ - Long code/documents should be saved with `run_cmd` (heredoc); the user then opens them through the file card or the `[FILES]` button.
94
+ - Clicking a file card splits the screen: chat on the left (half width, input moves into this column), file viewer on the right. On mobile the viewer replaces the chat.
95
+ - Viewer toolbar: `PREVIEW` / `CODE` tabs (preview for HTML in a sandboxed iframe, Markdown, SVG, Mermaid, PDF; other types show code only), reload (HTML), `COPY`, download icon, expand/restore (desktop), close (X).
96
+ - `[FILES]` panel `WORKSPACE FILES (n)`: `+ UPLOAD` (max 2MB), `DOWNLOAD ALL`, close; list of file rows with delete; empty text `NO FILES AVAILABLE`.
97
+
98
+ ## 9. Sidebar, modals, extras
99
+ - **Sidebar** (`[HISTORY]`): drawer from the left (288-320px) over a dimmed backdrop. Title `[CHAT_SESSIONS]` + `[CLOSE]`; `+ [NEW_CHAT] [n/16]` (max 16 chats); `[EXPORT_TXT]`, `[CLEAR_ALL]`; session rows (selected = inverted; double-click or `[EDIT]` to rename; `[X]` delete, asks confirmation); footer `[VOICE_CHAT_BETA]`.
100
+ - **Confirm modal**: centered box, 2px border, title, message, `[CANCEL]` and filled `[CONFIRM]`.
101
+ - **[SETTINGS]** modal, left tab list: `[API_CONFIG]` (API URL with presets OPENAI / GEMINI / OPENROUTER / GROQ / OLLAMA, API key, model name, system prompt, temperature, top P, top K, repeat penalty, reasoning effort, max tokens, Lora serif switch, think/strip tags), `[CHAT_TEMPLATE]`, `[EFFORT]` (Fast / Cautious / Thorough / Meticulous), `[SKILLS]` (create/edit/`[ON]`/`[OFF]`), `[MEM]` (master `[ON]`/`[OFF]`, your own keyword-tagged memories, the AI's own write_mem memories, and "generate memory from chat history"), `[AGENT]` (master `[ON]`/`[OFF]`, create/edit sub-agents with their own API config + effort + tag text/image, test-prompt button, export/clear each agent's own workspace), `[MCP]`, `[DEV_MODE]`. Bottom `[SAVE_SETTINGS]`. OLLAMA fills `http://localhost:11434/v1/chat/completions`; the API key field can stay blank, and the model name should be a locally-pulled Ollama model (e.g. `llama3.1`).
102
+ - **[DEV_MODE]**: toggles `[HUD: ON/OFF]`, `[RAW: ON/OFF]` (shows raw payloads), `[ROLE_SIM: ON/OFF]`, `[CARDS: ON/OFF]` (gallery of all cards), plus a `/help` tool manual.
103
+ - **[SETUP_GUIDE]** modal: "PREREQUISITES & SETUP GUIDE", 4 steps: ngrok authtoken → Google Colab T4 GPU → run server code (llama-cpp-python + ngrok) → paste API Base URL (`https://xxxx.ngrok-free.dev/v1`) and API key into Settings.
104
+ - **Voice chat** (`[VOICE_CHAT_BETA]`): fullscreen. Left: `YOU` box (live transcript), `AI` box (`Thinking...`/`Speaking...`), language toggles (multi-select, then `[ACTIVE] <lang>`). Right: `[AUDIO_LEVEL]` 7 vertical bars and `[STATUS: ...]`. Exit `[X]` top-right. Needs microphone permission.
105
+ - **Slash commands typed by the user**: `/help` (tool manual), `/mcps`, `/compact [hint]` (summarizes the conversation; status `[COMPACTING_CONTEXT: N messages]`).
106
+ - **`[PERF_TELEMETRY]` bar** (black): TOKENS, SPEED tok/s, TOOLCALLS, SYS_PROMPT, `[DEV_CONSOLE]`, close.
107
+
108
+ ## 10. Tips for you (the AI)
109
+ - Refer to buttons by their exact bracket labels, e.g. "open [SETTINGS] → [API_CONFIG]".
110
+ - Never tell the user to type commands; just run them. Don't print command syntax.
111
+ - Cards already show their data: after a card, add only 1-2 sentences of insight.
112
+ - For long code/documents use `run_cmd` and tell the user to press `[FILES]` or the file card.
113
+ - If the API connection fails, point to `[SETUP_GUIDE]`.
114
+ - The user's screen may be a phone: keep replies compact.
115
+
116
+ ## 11. Docs access
117
+ - `read_docs(fanluc)`, `read_docs(mondk)`, `read_docs(msh)`.
118
+ - Any `search(...)` containing "fanluc" lists `https://fanluc.docs/` as the FIRST result. `fetch("https://fanluc.docs/")` returns the docs index + this file; `/mondk`, `/msh`, `/guide` return the other docs.
119
+ - Any `search(...)` containing the word "mondk" or "msh" lists `https://mondk-msh.docs/` as the FIRST result. `fetch("https://mondk-msh.docs/")` returns docs/mondk.md and docs/msh.md; `/mondk` and `/msh` return one each.
40
120
 
41
121
  ---
42
122
 
@@ -45,4 +125,4 @@ His page: https://huggingface.co/mondk
45
125
  He started working on August 5, 2026.
46
126
  He worked at hugging because Claude(a strong AI at that time) was very expensive, he just chatted... 2 times and it ran out of turns(qouta)
47
127
  His logo consists of the letters M, S, and H layered over one another.
48
- Since mondk is not proficient in English, his repository descriptions are very brief or written using Google Translate; the name "mondk" itself is derived from "moon" and "dark."
128
+ Since mondk is not proficient in English, his repository descriptions are very brief or written using Google Translate; the name "mondk" itself is derived from "moon" and "dark."
@@ -25,7 +25,10 @@ print("Installing required packages...(took ~2 min)")
25
25
  --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu122
26
26
 
27
27
  import os
28
+ import re
28
29
  import subprocess
30
+ import requests
31
+ from urllib.parse import quote
29
32
  from huggingface_hub import hf_hub_download
30
33
  from pyngrok import ngrok
31
34
 
@@ -33,13 +36,239 @@ NGROK_TOKEN = "YOUR_NGROK_AUTHTOKEN" # <-- Paste your Ngrok Authtoken here
33
36
  API_KEY = "123" # <-- Custom API Key (Change if desired)
34
37
  CONTEXT_SIZE = 24000 # <-- Recommended: 1248-4096
35
38
 
39
+ # ---- Model finder settings (the best GGUF model is picked automatically) ----
40
+ TRUSTED_AUTHORS = ["mondk", "unsloth", "bartowski", "qwen"] # <-- Priority: left to right
41
+ TARGET_MODEL = "qwen" # <-- Model family to look for
42
+ MIN_PARAMS_B = 6 # <-- Smallest model size (billions of parameters)
43
+ MAX_PARAMS_B = 14 # <-- Largest model size (billions of parameters)
44
+ MAX_SIZE_GB = 10 # <-- Largest GGUF file size allowed
45
+ MODEL_REPO_ID = "" # <-- Optional: set BOTH of these to skip the search
46
+ MODEL_FILENAME = "" # <-- and download one specific GGUF file instead
47
+
36
48
  ngrok.set_auth_token(NGROK_TOKEN)
37
49
 
50
+ HF_URL = "https://huggingface.co"
51
+ HF_HEADERS = {"User-Agent": "HuggingFaceExplorer/1.0"}
52
+ QUANT_ORDER = [
53
+ ("Q5_K_M", [r"(?:[-_.]|^)(?:ud-)?q5_k_m(?:[-_.]|\.gguf$)", r"(?:[-_.]|^)(?:ud-)?q5_k_xl(?:[-_.]|\.gguf$)"]),
54
+ ("Q4_K_M", [r"(?:[-_.]|^)(?:ud-)?q4_k_m(?:[-_.]|\.gguf$)", r"(?:[-_.]|^)(?:ud-)?q4_k_xl(?:[-_.]|\.gguf$)"]),
55
+ ("IQ4_XS", [r"(?:[-_.]|^)(?:ud-)?iq4_xs(?:[-_.]|\.gguf$)"]),
56
+ ]
57
+
58
+ def finder_log(message):
59
+ print("[model-finder] " + message)
60
+
61
+ def hf_get(url, as_json=True):
62
+ try:
63
+ response = requests.get(url, headers=HF_HEADERS, timeout=30)
64
+ if response.status_code != 200:
65
+ return None
66
+ return response.json() if as_json else response.text
67
+ except Exception as error:
68
+ finder_log("request failed: " + str(error))
69
+ return None
70
+
71
+ def extract_param_count(model):
72
+ safetensors = model.get("safetensors") or {}
73
+ total = safetensors.get("total")
74
+ if isinstance(total, (int, float)) and total > 0:
75
+ return total
76
+ units = {"B": 1e9, "M": 1e6, "K": 1e3}
77
+ # 1. Standard size token in the id: -7b-, _14b_, .8B.
78
+ match = re.search(r"(?:^|[-_/])([0-9]+(?:\.[0-9]+)?)\s*([bBmMkK])(?=[-_/.]|$)", model["id"])
79
+ if match:
80
+ return round(float(match.group(1)) * units[match.group(2).upper()])
81
+ # 2. Size token embedded in a longer name: vl8b, code7b, qwen3-8b
82
+ match = re.search(r"(?:[\W_]|code|vl|instruct|chat|it|reasoning)([0-9]+(?:\.[0-9]+)?)\s*([bBmM])(?=[\W_]|$)", model["id"], re.I)
83
+ if match:
84
+ return round(float(match.group(1)) * units[match.group(2).upper()])
85
+ # 3. Tags (includes base_model tags)
86
+ for tag in model.get("tags") or []:
87
+ match = re.search(r"(?:^|[-_:/.])([0-9]+(?:\.[0-9]+)?)\s*([bBmM])(?=[-:_/.]|\b|$)", tag)
88
+ if match and float(match.group(1)) < 1000:
89
+ return round(float(match.group(1)) * units[match.group(2).upper()])
90
+ return None
91
+
92
+ def extract_version(model_id):
93
+ # Versions like 3.5 in "Qwen3.5-9B", but never sizes like the 7 in "7B"
94
+ pattern = re.escape(TARGET_MODEL.strip()) + r"(?:[\s_.-]*(?:vl|coder|math|chat|instruct|it|agent))*[\s_.-]*(?:v)?([0-9]+(?:\.[0-9]+)*)(?![bBmMkK])"
95
+ match = re.search(pattern, model_id, re.I)
96
+ if not match:
97
+ return 1000000, "1.0"
98
+ version = match.group(1)
99
+ parts = []
100
+ for part in version.split("."):
101
+ try:
102
+ parts.append(float(part))
103
+ except ValueError:
104
+ parts.append(0.0)
105
+ if parts[0] > 20: # dates / checkpoints such as 2507
106
+ return 1000000, "1.0"
107
+ score, multiplier = 0.0, 1000000.0
108
+ for part in parts:
109
+ score += part * multiplier
110
+ multiplier /= 1000
111
+ return score, version
112
+
113
+ def matches_category(model):
114
+ # LLM, Chat & Vision Multimodal (LLM + code/reasoning + vision, no image/audio/embedding models)
115
+ pipeline = (model.get("pipeline_tag") or "").lower()
116
+ tags = [t.lower() for t in (model.get("tags") or [])]
117
+ model_id = model["id"].lower()
118
+ is_vision = (pipeline == "image-text-to-text" or "image-text-to-text" in tags or "vision" in tags
119
+ or "multimodal" in tags or "-vl" in model_id or "vision" in model_id)
120
+ is_code = (any(t in tags for t in ("code", "coding", "reasoning", "math"))
121
+ or "coder" in model_id or "reasoning" in model_id)
122
+ is_llm = (pipeline in ("text-generation", "conversational")
123
+ or any(t in tags for t in ("text-generation", "llama", "qwen")))
124
+ return is_vision or is_code or is_llm
125
+
126
+ def usable_gguf_files(siblings):
127
+ # Only real, standalone model weights: no MTP, mmproj, imatrix, adapter, LoRA or draft files
128
+ files = []
129
+ for sibling in siblings or []:
130
+ name = (sibling.get("rfilename") or "").lower()
131
+ if not name.endswith(".gguf"):
132
+ continue
133
+ if "mtp/" in name or "/mtp" in name or name.startswith("mtp") or "-mtp" in name or "_mtp" in name:
134
+ continue
135
+ if "mmproj" in name or "imatrix" in name:
136
+ continue
137
+ if "adapter" in name or "lora" in name or "draft" in name:
138
+ continue
139
+ files.append(sibling)
140
+ return files
141
+
142
+ def pick_quant(siblings, max_bytes=None):
143
+ # Q5_K_M first, then Q4_K_M, then IQ4_XS; a quant is skipped if it is larger than max_bytes
144
+ files = usable_gguf_files(siblings)
145
+ for quant, patterns in QUANT_ORDER:
146
+ matching = []
147
+ for pattern in patterns:
148
+ matching = [f for f in files if re.search(pattern, f["rfilename"], re.I)]
149
+ if matching:
150
+ break
151
+ if not matching:
152
+ continue
153
+ weights = [f for f in matching if (f.get("size") or 0) > 1024 * 1024]
154
+ chosen = weights if weights else matching
155
+ total = sum((f.get("size") or 0) for f in chosen)
156
+ if total <= 0:
157
+ continue
158
+ if max_bytes is not None and total > max_bytes:
159
+ finder_log(f" {quant} is {total / 1024 ** 3:.2f} GB (over the {MAX_SIZE_GB} GB limit)")
160
+ continue
161
+ return {"quant": quant, "files": sorted(f["rfilename"] for f in chosen), "size_bytes": total}
162
+ return None
163
+
164
+ def find_pointed_repo():
165
+ # mondk/best_model_now: its README can point to one specific repo (owner/repo)
166
+ readme = hf_get(HF_URL + "/mondk/best_model_now/raw/main/README.md", as_json=False)
167
+ if readme is None:
168
+ finder_log("mondk/best_model_now is not readable, using the normal search")
169
+ return None
170
+ for line in readme.split("\n"):
171
+ line = line.strip()
172
+ if not line or line.startswith("---") or line.startswith("license:"):
173
+ continue
174
+ match = (re.search(r"huggingface\.co/([a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+)", line)
175
+ or re.search(r"([a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+)", line))
176
+ if not match:
177
+ continue
178
+ repo = match.group(1).rstrip(".-")
179
+ if repo.lower() != "mondk/best_model_now":
180
+ return repo
181
+ finder_log("mondk/best_model_now does not name a repo, using the normal search")
182
+ return None
183
+
184
+ def find_best_model():
185
+ authors = [a.strip().lower() for a in TRUSTED_AUTHORS if a.strip()]
186
+ min_params = MIN_PARAMS_B * 1e9
187
+ max_params = MAX_PARAMS_B * 1e9
188
+ max_bytes = MAX_SIZE_GB * 1024 ** 3
189
+ finder_log(f"Looking for {TARGET_MODEL} GGUF models, {MIN_PARAMS_B}-{MAX_PARAMS_B}B, under {MAX_SIZE_GB} GB, authors: {' -> '.join(authors)}")
190
+
191
+ # Step 1: mondk/best_model_now may name the repo to use (Q5_K_M -> Q4_K_M -> IQ4_XS, no size limit)
192
+ if "mondk" in authors:
193
+ pointed = find_pointed_repo()
194
+ if pointed:
195
+ finder_log("mondk/best_model_now points to " + pointed)
196
+ info = hf_get(f"{HF_URL}/api/models/{pointed}?blobs=true")
197
+ pick = pick_quant(info.get("siblings"), None) if info else None
198
+ if pick:
199
+ return {"repo_id": pointed, "files": pick["files"], "quant": pick["quant"], "size_bytes": pick["size_bytes"]}
200
+ finder_log(pointed + " has no Q5_K_M / Q4_K_M / IQ4_XS file, using the normal search")
201
+
202
+ # Step 2: collect candidates from the trusted authors (mondk is only used as the pointer above)
203
+ search_authors = [a for a in authors if a != "mondk"]
204
+ raw, seen = [], set()
205
+ for rank, author in enumerate(search_authors):
206
+ url = f"{HF_URL}/api/models?author={quote(author, safe='')}&search={quote(TARGET_MODEL, safe='')}&limit=30&full=true"
207
+ listing = hf_get(url)
208
+ for model in listing if isinstance(listing, list) else []:
209
+ if model["id"] not in seen:
210
+ seen.add(model["id"])
211
+ model["_rank"] = rank
212
+ raw.append(model)
213
+ if len(raw) < 5:
214
+ url = f"{HF_URL}/api/models?search={quote(TARGET_MODEL + ' gguf', safe='')}&limit=40&full=true"
215
+ listing = hf_get(url)
216
+ for model in listing if isinstance(listing, list) else []:
217
+ if model["id"] not in seen:
218
+ seen.add(model["id"])
219
+ owner = model["id"].split("/")[0].lower()
220
+ model["_rank"] = search_authors.index(owner) if owner in search_authors else 99
221
+ raw.append(model)
222
+ finder_log(f"Found {len(raw)} raw candidates")
223
+
224
+ candidates = []
225
+ for model in raw:
226
+ model_id = model["id"]
227
+ lower_id = model_id.lower()
228
+ is_mtp_repo = "-mtp" in lower_id or "_mtp" in lower_id or "/mtp" in lower_id
229
+ tags = [t.lower() for t in (model.get("tags") or [])]
230
+ if not (("gguf" in lower_id or "gguf" in tags) and not is_mtp_repo):
231
+ continue
232
+ params = extract_param_count(model)
233
+ version_num, version = extract_version(model_id)
234
+ candidates.append({
235
+ "id": model_id,
236
+ "rank": model["_rank"],
237
+ "params": params,
238
+ "in_range": params is not None and min_params <= params <= max_params,
239
+ "in_category": matches_category(model),
240
+ "version_num": version_num,
241
+ "version": version,
242
+ "downloads": model.get("downloads") or 0,
243
+ })
244
+
245
+ # Order: size range, category, newest version, author priority, downloads
246
+ candidates.sort(key=lambda c: (not c["in_range"], not c["in_category"], -c["version_num"], c["rank"], -c["downloads"]))
247
+ finder_log(f"{len(candidates)} GGUF candidates, checking the best {min(15, len(candidates))}")
248
+
249
+ # Step 3: first candidate that has Q5_K_M, else Q4_K_M, else IQ4_XS within the size limit
250
+ for candidate in candidates[:15]:
251
+ size_label = "unknown size" if candidate["params"] is None else f"{candidate['params'] / 1e9:.1f}B"
252
+ finder_log(f"Checking {candidate['id']} (v{candidate['version']}, {size_label})")
253
+ info = hf_get(f"{HF_URL}/api/models/{candidate['id']}?blobs=true")
254
+ pick = pick_quant(info.get("siblings"), max_bytes) if info else None
255
+ if pick:
256
+ return {"repo_id": candidate["id"], "files": pick["files"], "quant": pick["quant"], "size_bytes": pick["size_bytes"]}
257
+ finder_log(" no fitting quant, trying the next candidate")
258
+ return None
259
+
260
+ if MODEL_REPO_ID and MODEL_FILENAME:
261
+ repo_id, filenames = MODEL_REPO_ID, [MODEL_FILENAME]
262
+ else:
263
+ best = find_best_model()
264
+ if best is None:
265
+ raise RuntimeError("No suitable GGUF model was found. Raise MAX_SIZE_GB, widen the size range, or set MODEL_REPO_ID and MODEL_FILENAME by hand.")
266
+ repo_id, filenames = best["repo_id"], best["files"]
267
+ print(f"Selected model: {repo_id} / {filenames[0]} ({best['quant']}, {best['size_bytes'] / 1024 ** 3:.2f} GB)")
268
+
38
269
  print("Installing the model...")
39
- model_path = hf_hub_download(
40
- repo_id = "unsloth/Qwen3.5-9B-GGUF", # <-- Hugging Face Repo ID
41
- filename="Qwen3.5-9B-Q5_K_M.gguf" # <-- Recommended: Q4_K_M quantization suffix
42
- )
270
+ local_paths = [hf_hub_download(repo_id=repo_id, filename=name) for name in filenames]
271
+ model_path = local_paths[0] # split models: the first part is enough, llama.cpp finds the rest next to it
43
272
  print("Starting up the server...")
44
273
 
45
274
  ngrok.kill()
@@ -0,0 +1,26 @@
1
+ # AI workspace safety (run_cmd / install_pack_v1)
2
+
3
+ Layers, from the inside out:
4
+
5
+ 1. **cwd jail** – every command starts in the workspace dir; new process each call.
6
+ 2. **Stripped env** – API keys of the server are never passed to commands.
7
+ 3. **Blocklist** – sudo, destructive rm, `/proc/*/environ`, docker.sock, .env, reverse shells, cron…
8
+ 4. **Unprivileged user** – if the server runs as root (Docker, or a VPS), every command is dropped to a non-root uid: `SANDBOX_UID` (Docker: 10001) or `nobody` (65534) by default. It cannot read the server's environment/`.env` (chmod 600 at startup) or modify the venv. Never runs AI commands as root.
9
+ 5. **firejail (optional)** – `--net=none`, no caps, seccomp, read-only venv, size/memory limits.
10
+ 6. **Container** – `docker compose up`: no-new-privileges, capabilities dropped, memory/pid limits.
11
+ 7. **Timeouts + output caps** – 20 s / 2 MB per command.
12
+
13
+ ## Installing libraries
14
+ The jail has no network and no root, so the **server** installs on the AI's behalf:
15
+ - `install_pack_v1` – really verifies the 14 essential packages (+ tesseract `vie`; includes zip/unzip), repairs what it may, prints OK/MISSING. Concurrent calls share one run.
16
+ - `pip install <pkg>` / `pip install -U <pkg>` typed in `run_cmd` is intercepted: plain PyPI names only, **wheels only** (no build scripts run), installed into a read-only venv. URLs, `-r`, `-e`, custom indexes are refused.
17
+ - `apt-get install` works only as root (or with passwordless `sudo -n`; set `DISABLE_SUDO_APT=1` to opt out), only for an allowlist (`installer.ts`), otherwise the AI is told to ask the operator. The install is tried first and `apt-get update` only runs if apt cannot find the package.
18
+ - pip/apt fail fast on a dead network (20 s timeout, 2 retries) and report a network hint. One bad package in a batch no longer blocks the others.
19
+ - Proxy / mirror / CA variables of the server (`HTTP(S)_PROXY`, `NO_PROXY`, `PIP_INDEX_URL`, `PIP_EXTRA_INDEX_URL`, `PIP_TRUSTED_HOST`, `PIP_CERT`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`) are passed to the installer (never to AI commands).
20
+
21
+ `DISABLE_RUN_CMD=1` turns all of it off.
22
+
23
+ ## Self-healing (server side, no user action needed)
24
+ - venv without pip (missing `python3-venv`) -> detected, rebuilt; installs `python3-venv` when root.
25
+ - `dpkg was interrupted` -> runs `dpkg --configure -a`, retries; broken deps -> `apt-get -f install`.
26
+ - apt lock held by another process -> waits up to 120 s.
package/index.html CHANGED
@@ -3,8 +3,8 @@
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>TOOL-NGORK</title>
7
- <meta name="description" content="Ung dung tro chuyen voi AI qua Custom REST API Endpoint phong cach co dien trang den." />
6
+ <title>FANLUC</title>
7
+ <meta name="description" content="Self-hosted AI chat with a classic black-and-white terminal style. Custom REST API endpoint." />
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
10
  <link href="https://fonts.googleapis.com/css2?family=Courier+Prime:ital,wght@0,400;0,700;1,400;1,700&family=Inconsolata:wght@200..900&family=Lora:ital,wght@0,400..700;1,400..700&family=Share+Tech+Mono&family=VT323&display=swap" rel="stylesheet">
package/mcp.json CHANGED
@@ -0,0 +1,3 @@
1
+ {
2
+ "mcpServers": {}
3
+ }