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.
- package/README.md +83 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-hiYMdxpV.js → abnfDiagram-O67JEVCF-Di2iDQfl.js} +1 -1
- package/dist/assets/{arc-DokRg_wy.js → arc-wXq_yD5s.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-UzHYe9Bx.js → architectureDiagram-NJMV4G6O-CTXbg-3W.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DqM5UILT.js → blockDiagram-BEXU5L5S-DSDoL3RP.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Cxkdm7sV.js → c4Diagram-YGBWAQC7-3sFM3iyw.js} +1 -1
- package/dist/assets/channel-atnI5OIR.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Nl4zmLlG.js → chunk-2Q5K7J3B-DUOKiJNI.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BCol78O2.js → chunk-3FUC2YCW-ablWb8_c.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-1u4YRv2Y.js → chunk-5VM5RSS4-qmlSVZ_1.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BG9YIV2h.js → chunk-GTNCS2PH-PLlC7cNS.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BTKfkJAH.js → chunk-GWA4HPMP-DK-1a6YJ.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DCjElfA7.js → chunk-JWPE2WC7-D9zynFKs.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-QoJORlVn.js → chunk-NETBCI7D-BvzPMYyE.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-Da-T8ct9.js → chunk-WEXAMYUT-CzcfZFWB.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CLu69Uei.js → chunk-XXDRQBXY-DznN-rwG.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DIXavbI9.js → classDiagram-v2-NBCMYWYE-D4jem0hV.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-DuWclG8-.js → cose-bilkent-JH36ORCC-VALsUQJS.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-D2CG5-xd.js → cynefin-OW5HDTMX-DK77QSI8.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-goI7_Qfg.js → cynefinDiagram-VND7K2PF-CdYXfGNR.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BmOaZ0cm.js → dagre-6A5THRUB-DeeUQzDV.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DjDJFMw3.js → diagram-22UHCM2B-B9shZ914.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-J6z81NH3.js → diagram-3UASUU5V-gSHvaXBI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-B1MPU8ai.js → diagram-ATOU4E4O-BrCI5HzS.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-X5Uss0Yv.js → diagram-CDSNMT55-XtVlMRq0.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CRDqfYy1.js → diagram-MLGK6HIB-BVa6KsvA.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-bhJtyiOq.js → diagram-MPIPVDR6-C8P23P2T.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Dn1mZAV8.js → ebnfDiagram-ZINNZB2B-BrooaQj5.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-C93BlBEU.js → elk-276RUBZZ-B-qiKlkF.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-PuzjAvhD.js → erDiagram-OPXOYQCR-CL1bp2Mf.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-oRhFgO0p.js → flowDiagram-KWPJA3E3-BMQvaQ-5.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DKiU1XjU.js → ganttDiagram-FUAMR5RP-PLnTGRI8.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-CWBMI0vb.js → gitGraphDiagram-X574FWY7-CiTN3Joc.js} +1 -1
- package/dist/assets/index-BEKLwzQT.css +1 -0
- package/dist/assets/index-C8gDJtDK.js +2841 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-C5Hd4Afz.js → infoDiagram-VRGFBTTK-ComkS_96.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BdD6ulRK.js → ishikawaDiagram-OU5B5YK6-D-dNoDFJ.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CzT-uvks.js → journeyDiagram-ZHPQQLJL-BMm9BQM8.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BkrazFMR.js → kanban-definition-PNTS6WVX-D5If-l_D.js} +1 -1
- package/dist/assets/{linear-B0wSM1Dr.js → linear-DXAHb0Ag.js} +1 -1
- package/dist/assets/{mindmap-definition-NLK3R4M7-Df1Acqkf.js → mindmap-definition-NLK3R4M7-CIdwCXTJ.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DK_F7h_0.js → pegDiagram-GJSIUBJH-MY7FoVQh.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-BMXyNb9R.js → pieDiagram-5QR66LMP-C7lWWAtM.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Dfu7Z8so.js → quadrantDiagram-O4NWA36T-jHqCwvi8.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-CfnBD-aQ.js → railroadDiagram-XR7U4H2S-BwoIA8Ln.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DrvsZJPP.js → requirementDiagram-PLB6GJNP-Cw8w0Rwb.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-DvdVDpT1.js → sankeyDiagram-IPEJSGJF-DTRo1Yxy.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-D1UTrvZH.js → sequenceDiagram-PO4LG4MO-DqZddXsc.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CJrOBcai.js → sizeCapture-INFHLROL-BAEQ4XP5.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-CX8VmtqD.js → stateDiagram-v2-GCMORJYK-CYB4CouD.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DUWmhiK6.js → swimlanes-2SLR337P-Dw53LU13.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Dzkep6h_.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-CBovVdOU.js → timeline-definition-EJHVYXUP-Chk-unGo.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-LD5N5BAG.js → usecaseDiagram-POWQR4AR-D_JpD-4l.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-DdwfzB3A.js → vennDiagram-UO4OBE2U-BnPO8gk8.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-BMHK4bav.js → wardleyDiagram-VNRHLVJA-CD0z1l8R.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-q1BNzMC-.js → xychartDiagram-PMCCYNJV-CHEqj1Pe.js} +1 -1
- package/dist/cli.js +52 -57
- package/dist/index.html +4 -4
- package/dist/server.cjs +2144 -841
- package/dist/server.cjs.map +4 -4
- package/dist/tui.js +3220 -3310
- package/docs/fanluc_ui.md +120 -40
- package/docs/guide-connect.md +233 -4
- package/docs/run-cmd-security.md +26 -0
- package/index.html +2 -2
- package/mcp.json +3 -0
- package/package.json +88 -92
- package/searxng/docker-compose.yml +3 -3
- package/vite.config.ts +2 -2
- package/dist/assets/channel-SPfNTvmj.js +0 -1
- package/dist/assets/index-BylpTP-E.css +0 -1
- package/dist/assets/index-DcpVteR3.js +0 -3237
- package/dist/assets/permissionHelper--88tJJlz.js +0 -3
- 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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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."
|
package/docs/guide-connect.md
CHANGED
|
@@ -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
|
-
|
|
40
|
-
|
|
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>
|
|
7
|
-
<meta name="description" content="
|
|
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