diff --git a/.gitignore b/.gitignore index 9d176cb..dd8227a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,10 @@ build/ .chela/ *.db +# OKF knowledge bundle — LOCAL fleet data, never committed (docs/OKF.md → Security) +knowledge/ +*.okf/ + # Cloudflare Wrangler local cache (holds account_id; deploy-only, not source) .wrangler/ diff --git a/README.md b/README.md index a41feb0..a52a3d1 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,20 @@ literal Escape. --- +## Roadmap + +- **OKF knowledge layer + viewer** — export the fleet's accumulated knowledge + (dispatch runs, schedules, agent recaps, PR links) as a portable + [Open Knowledge Format](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf) + bundle (vendor-neutral markdown + YAML), with a glance / browse / search / + graph viewer — embedded in the dashboard and shipped self-contained inside + each bundle. Full design: [docs/OKF.md](docs/OKF.md). +- Agent personas (customizable behavior templates). +- Custom functions / harnesses (per-project agent config). +- Scheduling integration. + +--- + ## Credits The work-item dispatcher is an adaptation of OpenAI's **Symphony** pattern diff --git a/chela/dashboard/app.py b/chela/dashboard/app.py index 164d6bf..e07a966 100644 --- a/chela/dashboard/app.py +++ b/chela/dashboard/app.py @@ -26,7 +26,7 @@ from chela import config from chela.config import DISPATCH_WORKFLOWS, CHELA_DIR, TMUX_SESSION, NOTIFY_INTERVAL -from chela import agent_manager, context, discovery, dispatcher, launcher, messenger, notify, scheduler, starter, transcripts, userconfig +from chela import agent_manager, context, discovery, dispatcher, launcher, messenger, notify, okf, scheduler, starter, transcripts, userconfig from chela.backlog import _BULLET_RE, parse_backlog from chela.sources import get_source from chela.sources.markdown import OPEN_RE @@ -452,18 +452,64 @@ def _terminals_port_map() -> dict: # the dashboard's scrollbar (style.css :root --border #21262d, hover #30363d) so the # wall's scrollbars match the rest of the UI. Literal hex — the ttyd page has no # CSS vars. -# Bundled icon-only Nerd Font, served from /static and injected as an @font-face -# into the ttyd page so xterm.js can resolve the PUA glyphs (lazygit/yazi file & -# git icons) on ANY viewer — no device-side font install needed. The ttyd -# fontFamily stack (see scripts/agent-terminals.sh) already lists "Symbols Nerd -# Font" as a fallback; this @font-face supplies it. font-display:block avoids an -# icon-flash where glyphs briefly render as boxes before the font loads. The URL -# is same-origin absolute (/static/...) so it resolves against the dashboard, not -# the iframe's /term// base path. +# Bundled fonts, served from /static and injected as @font-face rules into the +# ttyd page so xterm.js resolves every glyph on ANY viewer — no device-side font +# install needed. URLs are same-origin absolute (/static/...) so they resolve +# against the dashboard, not the iframe's /term// base path. +# +# WHY bundle rather than rely on installed fonts: CSS font matching is PER-GLYPH, +# so the browser walks the whole family stack for every character. On a viewer +# without these fonts installed, a Latin letter would fall past everything and +# land on the Hebrew font — and a proportional Hebrew font there breaks monospace +# alignment for ENGLISH too. Bundling a real Latin monospace pins English on +# every device, leaving the Hebrew face to handle only Hebrew. +# +# The Settings > Terminal font picker chooses one Latin face × one Hebrew face +# (+ size); the injected _TERM_FONT_PREF_SHIM builds window.term's fontFamily +# from the selection. Every option is declared here, but only the SELECTED faces +# are actually downloaded — an unused @font-face never fetches. See +# static/fonts/README.md for per-font licenses (all OFL-1.1 except Miriam Mono +# CLM, GPL-2 — the only free MONOSPACE Hebrew font, hence its inclusion). +# +# Manifest: (family, filename, variable?, weight-if-static). Variable fonts get a +# 100–900 weight range in one face; static fonts get one @font-face per weight. +_TERM_FONTS = [ + # Icons — font-display:block avoids a box-flash before the icon font loads. + ("Symbols Nerd Font", "SymbolsNerdFontMono-Regular.ttf", None, None), + # English / Latin monospace (picker: English face) + ("JetBrains Mono", "JetBrainsMono.ttf", True, None), + ("Fira Code", "FiraCode.ttf", True, None), + ("IBM Plex Mono", "IBMPlexMono-Regular.ttf", False, "normal"), + ("IBM Plex Mono", "IBMPlexMono-Bold.ttf", False, "bold"), + ("Source Code Pro", "SourceCodePro.ttf", True, None), + ("Cascadia Code", "CascadiaCode.ttf", True, None), + # Hebrew (picker: Hebrew face). Miriam is the only monospace one. + ("Miriam Mono CLM", "MiriamMonoCLM-Book.ttf", False, "normal"), + ("Miriam Mono CLM", "MiriamMonoCLM-Bold.ttf", False, "bold"), + ("Noto Sans Hebrew", "NotoSansHebrew.ttf", True, None), + ("Heebo", "Heebo.ttf", True, None), + ("Assistant", "Assistant.ttf", True, None), + ("Rubik", "Rubik.ttf", True, None), + ("Frank Ruhl Libre", "FrankRuhlLibre.ttf", True, None), + ("David Libre", "DavidLibre-Regular.ttf", False, "normal"), + ("David Libre", "DavidLibre-Bold.ttf", False, "bold"), +] + + +def _term_font_face(family, filename, variable, weight): + if variable is None: # Symbols icon font — block to avoid glyph-box flash + disp = "font-display:block" + wgt = "" + else: + disp = "font-display:swap" + wgt = "font-weight:100 900;" if variable else "font-weight:%s;" % weight + return ("@font-face{font-family:'%s';%s" + "src:url('/static/fonts/%s') format('truetype');%s}" + % (family, wgt, filename, disp)) + + _TERM_FONT_CSS = ( - "" + "" ) _TERM_SCROLLBAR_CSS = ( @@ -479,6 +525,56 @@ def _terminals_port_map() -> dict: "" ) +# Font-preference shim. Two jobs in one: +# (1) Apply the user's Settings > Terminal font choice (family + size) to this +# ttyd's xterm live. ttyd exposes the Terminal as `window.term`; we set its +# fontFamily/fontSize, then re-fit (fontSize changes the cell grid) and +# refresh. The choice lives in localStorage (chela_term_latin / +# chela_term_font / chela_term_fontsize), shared same-origin with the +# dashboard, so a `storage` event fires here whenever the settings panel +# changes it — live switching, no reload. `window.chelaApplyTermPrefs` is +# also exposed so the parent frame can poke it directly for instant feedback. +# (2) Fix the FOUT/atlas bug: xterm rasterises glyphs into a texture atlas ONCE +# at first paint, using whatever font was ready then. Our @font-faces load +# async (font-display:swap), so the first atlas uses the fallback and never +# rebuilds — the real font only appeared on cells xterm re-drew (a text +# selection/scroll), i.e. the font "changed" on select. apply() awaits the +# chosen faces via the CSS Font Loading API, then clears the atlas +# (clearTextureAtlas) so the next render rebuilds it correctly. +# Polls for `window.term` because the terminal is created after page scripts run. +_TERM_FONT_PREF_SHIM = ( + "" +) + @app.route("/term//", defaults={"rest": ""}, methods=["GET", "POST"]) @app.route("/term//", methods=["GET", "POST"]) @@ -518,8 +614,9 @@ def term_http(wid, rest): ctype = (resp.headers.get("Content-Type") or "") if "text/html" in ctype.lower(): html = body.decode("utf-8", "replace") - shims = (_TERM_FONT_CSS + _TERM_PASTE_SHIM + _TERM_PASTE_KEY_SHIM - + _TERM_PALETTE_KEY_SHIM + _TERM_SCROLL_SHIM + _TERM_SCROLLBAR_CSS) + shims = (_TERM_FONT_CSS + _TERM_FONT_PREF_SHIM + _TERM_PASTE_SHIM + + _TERM_PASTE_KEY_SHIM + _TERM_PALETTE_KEY_SHIM + + _TERM_SCROLL_SHIM + _TERM_SCROLLBAR_CSS) html = (html.replace("", shims + "", 1) if "" in html else html + shims) body = html.encode("utf-8") @@ -1141,6 +1238,80 @@ def api_schedules_toggle(task_id): return jsonify({"error": str(e)}), 500 +# --------------------------------------------------------------------------- +# API: Knowledge (OKF viewer — read-only) +# +# Serves the embedded "Knowledge" view from an on-disk OKF bundle. The bundle is +# PRIVATE fleet data (docs/OKF.md → Security): these routes inherit the +# dashboard's loopback bind (tailnet-only remote) and add no new listener. The +# producer (okf.export_bundle) and consumer (okf.read_*) live in chela.okf; this +# layer is a thin jsonify wrapper plus an export-on-demand cache. +# --------------------------------------------------------------------------- + +def _knowledge_dir() -> Path: + """The bundle the embedded viewer reads — the default export dir + (~/.chela/knowledge), shared with `chela knowledge export` (no --out).""" + return okf.DEFAULT_OUT + + +def _ensure_bundle(force: bool = False) -> Path: + """Export the bundle if it's missing (or ``force``), then return its dir. + + First view auto-exports so the page isn't empty; thereafter it's served from + disk and the UI's Refresh button forces a re-export. Export reads chela's own + state only (never home-root private memory).""" + root = _knowledge_dir() + if force or not (root / "index.md").exists(): + okf.export_bundle(root) + return root + + +@app.route("/api/knowledge/tree") +@require_auth +def api_knowledge_tree(): + """Browse + glance payload: concepts by directory, counts by type, log.""" + return jsonify(okf.read_tree(_ensure_bundle())) + + +@app.route("/api/knowledge/concept") +@require_auth +def api_knowledge_concept(): + """One concept: frontmatter + raw body + outbound links + computed backlinks.""" + rel = request.args.get("path", "") + try: + return jsonify(okf.read_concept(_knowledge_dir(), rel)) + except ValueError: + abort(400) + except (FileNotFoundError, OSError): + abort(404) + + +@app.route("/api/knowledge/search") +@require_auth +def api_knowledge_search(): + """Substring search over frontmatter + body, filterable by type / tag.""" + return jsonify(okf.read_search( + _knowledge_dir(), + request.args.get("q", ""), + request.args.get("type", ""), + request.args.get("tag", ""), + )) + + +@app.route("/api/knowledge/graph") +@require_auth +def api_knowledge_graph(): + """Concepts as nodes, markdown links as edges.""" + return jsonify(okf.read_graph(_ensure_bundle())) + + +@app.route("/api/knowledge/export", methods=["POST"]) +@require_auth +def api_knowledge_export(): + """Force a re-export (the Knowledge view's Refresh), returning fresh tree.""" + return jsonify(okf.read_tree(_ensure_bundle(force=True))) + + # --------------------------------------------------------------------------- # API: System cron (read-only) # --------------------------------------------------------------------------- diff --git a/chela/dashboard/static/fonts/Assistant.ttf b/chela/dashboard/static/fonts/Assistant.ttf new file mode 100644 index 0000000..97aea54 Binary files /dev/null and b/chela/dashboard/static/fonts/Assistant.ttf differ diff --git a/chela/dashboard/static/fonts/CascadiaCode.ttf b/chela/dashboard/static/fonts/CascadiaCode.ttf new file mode 100644 index 0000000..b222dbd Binary files /dev/null and b/chela/dashboard/static/fonts/CascadiaCode.ttf differ diff --git a/chela/dashboard/static/fonts/DavidLibre-Bold.ttf b/chela/dashboard/static/fonts/DavidLibre-Bold.ttf new file mode 100644 index 0000000..3a76685 Binary files /dev/null and b/chela/dashboard/static/fonts/DavidLibre-Bold.ttf differ diff --git a/chela/dashboard/static/fonts/DavidLibre-Regular.ttf b/chela/dashboard/static/fonts/DavidLibre-Regular.ttf new file mode 100644 index 0000000..8e05da9 Binary files /dev/null and b/chela/dashboard/static/fonts/DavidLibre-Regular.ttf differ diff --git a/chela/dashboard/static/fonts/FiraCode.ttf b/chela/dashboard/static/fonts/FiraCode.ttf new file mode 100644 index 0000000..83f2b38 Binary files /dev/null and b/chela/dashboard/static/fonts/FiraCode.ttf differ diff --git a/chela/dashboard/static/fonts/FrankRuhlLibre.ttf b/chela/dashboard/static/fonts/FrankRuhlLibre.ttf new file mode 100644 index 0000000..ff5f0b4 Binary files /dev/null and b/chela/dashboard/static/fonts/FrankRuhlLibre.ttf differ diff --git a/chela/dashboard/static/fonts/GPL-2.txt b/chela/dashboard/static/fonts/GPL-2.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/chela/dashboard/static/fonts/GPL-2.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/chela/dashboard/static/fonts/Heebo.ttf b/chela/dashboard/static/fonts/Heebo.ttf new file mode 100644 index 0000000..b51ed9b Binary files /dev/null and b/chela/dashboard/static/fonts/Heebo.ttf differ diff --git a/chela/dashboard/static/fonts/IBMPlexMono-Bold.ttf b/chela/dashboard/static/fonts/IBMPlexMono-Bold.ttf new file mode 100644 index 0000000..17b406d Binary files /dev/null and b/chela/dashboard/static/fonts/IBMPlexMono-Bold.ttf differ diff --git a/chela/dashboard/static/fonts/IBMPlexMono-Regular.ttf b/chela/dashboard/static/fonts/IBMPlexMono-Regular.ttf new file mode 100644 index 0000000..0c9770d Binary files /dev/null and b/chela/dashboard/static/fonts/IBMPlexMono-Regular.ttf differ diff --git a/chela/dashboard/static/fonts/JetBrainsMono.ttf b/chela/dashboard/static/fonts/JetBrainsMono.ttf new file mode 100644 index 0000000..aa310be Binary files /dev/null and b/chela/dashboard/static/fonts/JetBrainsMono.ttf differ diff --git a/chela/dashboard/static/fonts/MiriamMonoCLM-Bold.ttf b/chela/dashboard/static/fonts/MiriamMonoCLM-Bold.ttf new file mode 100644 index 0000000..6b75cb6 Binary files /dev/null and b/chela/dashboard/static/fonts/MiriamMonoCLM-Bold.ttf differ diff --git a/chela/dashboard/static/fonts/MiriamMonoCLM-Book.ttf b/chela/dashboard/static/fonts/MiriamMonoCLM-Book.ttf new file mode 100644 index 0000000..ccdff90 Binary files /dev/null and b/chela/dashboard/static/fonts/MiriamMonoCLM-Book.ttf differ diff --git a/chela/dashboard/static/fonts/NotoSansHebrew.ttf b/chela/dashboard/static/fonts/NotoSansHebrew.ttf new file mode 100644 index 0000000..f31f73b Binary files /dev/null and b/chela/dashboard/static/fonts/NotoSansHebrew.ttf differ diff --git a/chela/dashboard/static/fonts/OFL-Assistant.txt b/chela/dashboard/static/fonts/OFL-Assistant.txt new file mode 100644 index 0000000..c8f36f7 --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-Assistant.txt @@ -0,0 +1,95 @@ +Copyright 2020 The Assistant Project Authors (https://github.com/hafontia/Assistant). +Copyright 2010 The Source Sans Pro Authors (https://github.com/adobe-fonts/source-sans-pro), with Reserved Font Name 'Source'. +Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/chela/dashboard/static/fonts/OFL-CascadiaCode.txt b/chela/dashboard/static/fonts/OFL-CascadiaCode.txt new file mode 100644 index 0000000..40f856f --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-CascadiaCode.txt @@ -0,0 +1,94 @@ +Copyright (c) 2019 - Present, Microsoft Corporation, +with Reserved Font Name Cascadia Code. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/chela/dashboard/static/fonts/OFL-DavidLibre.txt b/chela/dashboard/static/fonts/OFL-DavidLibre.txt new file mode 100644 index 0000000..6fa7b79 --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-DavidLibre.txt @@ -0,0 +1,93 @@ +Copyright 2016 The David Libre Project Authors (https://github.com/meirsadan/david-libre), with reserved font name "Hadash", "Gentium" and 'SIL'. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org/ + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/chela/dashboard/static/fonts/OFL-FiraCode.txt b/chela/dashboard/static/fonts/OFL-FiraCode.txt new file mode 100644 index 0000000..67a0efa --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-FiraCode.txt @@ -0,0 +1,93 @@ +Copyright 2014-2020 The Fira Code Project Authors (https://github.com/tonsky/FiraCode) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/chela/dashboard/static/fonts/OFL-FrankRuhlLibre.txt b/chela/dashboard/static/fonts/OFL-FrankRuhlLibre.txt new file mode 100644 index 0000000..ff7a239 --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-FrankRuhlLibre.txt @@ -0,0 +1,105 @@ +Copyright 2015 The Frank Ruhl Libre Project Authors (https://github.com/fontef/frankruhllibre) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. +© 2022 GitHub, Inc. +Terms +Privacy +Security +Status +Docs +Contact GitHub +Pricing +API +Training +Blog +About diff --git a/chela/dashboard/static/fonts/OFL-Heebo.txt b/chela/dashboard/static/fonts/OFL-Heebo.txt new file mode 100644 index 0000000..79ed1e5 --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-Heebo.txt @@ -0,0 +1,93 @@ +Copyright 2014 The Heebo Project Authors (https://github.com/OdedEzer/heebo) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/chela/dashboard/static/fonts/OFL-IBMPlexMono.txt b/chela/dashboard/static/fonts/OFL-IBMPlexMono.txt new file mode 100644 index 0000000..c35c4c6 --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-IBMPlexMono.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/chela/dashboard/static/fonts/OFL-JetBrainsMono.txt b/chela/dashboard/static/fonts/OFL-JetBrainsMono.txt new file mode 100644 index 0000000..821a3da --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-JetBrainsMono.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/chela/dashboard/static/fonts/OFL-NotoSansHebrew.txt b/chela/dashboard/static/fonts/OFL-NotoSansHebrew.txt new file mode 100644 index 0000000..56662b6 --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-NotoSansHebrew.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Noto Project Authors (https://github.com/notofonts/hebrew) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/chela/dashboard/static/fonts/OFL-Rubik.txt b/chela/dashboard/static/fonts/OFL-Rubik.txt new file mode 100644 index 0000000..03af63b --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-Rubik.txt @@ -0,0 +1,93 @@ +Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/chela/dashboard/static/fonts/OFL-SourceCodePro.txt b/chela/dashboard/static/fonts/OFL-SourceCodePro.txt new file mode 100644 index 0000000..d154618 --- /dev/null +++ b/chela/dashboard/static/fonts/OFL-SourceCodePro.txt @@ -0,0 +1,93 @@ +Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/chela/dashboard/static/fonts/README.md b/chela/dashboard/static/fonts/README.md new file mode 100644 index 0000000..1b8d0ef --- /dev/null +++ b/chela/dashboard/static/fonts/README.md @@ -0,0 +1,51 @@ +# Bundled terminal fonts — attribution & licenses + +These fonts are bundled and served (via `@font-face` in `app.py` +`_TERM_FONT_CSS`) so the web terminal renders correctly on **any** viewer, +regardless of what's installed locally. They power the **Settings › Terminal +font** picker (English/Latin face × Hebrew face × size). Only the *selected* +faces are actually downloaded by the browser. + +Each font is an **independent, separately-licensed work** merely aggregated into +this repository and served verbatim. Bundling them does **not** place the +chelamux source (MIT) under their licenses. All are permissive **OFL-1.1** except +Miriam Mono CLM (**GPL-2**), which is the only freely-licensed *monospace* Hebrew +font — see its note below. + +## Icons + +| Font | Files | License | Copyright / source | +|------|-------|---------|--------------------| +| Symbols Nerd Font | `SymbolsNerdFontMono-Regular.ttf` | MIT | Nerd Fonts — https://github.com/ryanoasis/nerd-fonts | + +## English / monospace (Latin) + +| Font | Files | License | Copyright / source | +|------|-------|---------|--------------------| +| JetBrains Mono | `JetBrainsMono.ttf` | OFL-1.1 (`OFL-JetBrainsMono.txt`) | © 2020 The JetBrains Mono Project Authors | +| Fira Code | `FiraCode.ttf` | OFL-1.1 (`OFL-FiraCode.txt`) | © The Fira Code Project Authors | +| IBM Plex Mono | `IBMPlexMono-Regular.ttf`, `-Bold.ttf` | OFL-1.1 (`OFL-IBMPlexMono.txt`) | © IBM Corp. (Mike Abbink, Bold Monday) | +| Source Code Pro | `SourceCodePro.ttf` | OFL-1.1 (`OFL-SourceCodePro.txt`) | © Adobe (Paul D. Hunt) | +| Cascadia Code | `CascadiaCode.ttf` | OFL-1.1 (`OFL-CascadiaCode.txt`) | © Microsoft Corporation | + +## Hebrew + +| Font | Files | License | Copyright / source | +|------|-------|---------|--------------------| +| Miriam Mono CLM | `MiriamMonoCLM-Book.ttf`, `-Bold.ttf` | **GPL-2** (`GPL-2.txt`) | Culmus — © Maxim Iorsh, Yoram Gnat, (URW)++ | +| Noto Sans Hebrew | `NotoSansHebrew.ttf` | OFL-1.1 (`OFL-NotoSansHebrew.txt`) | © The Noto Project Authors | +| Heebo | `Heebo.ttf` | OFL-1.1 (`OFL-Heebo.txt`) | © The Heebo Project Authors (Oded Ezer) | +| Assistant | `Assistant.ttf` | OFL-1.1 (`OFL-Assistant.txt`) | © The Assistant Project Authors | +| Rubik | `Rubik.ttf` | OFL-1.1 (`OFL-Rubik.txt`) | © The Rubik Project Authors | +| Frank Ruhl Libre | `FrankRuhlLibre.ttf` | OFL-1.1 (`OFL-FrankRuhlLibre.txt`) | © The Frank Ruhl Libre Project Authors | +| David Libre | `DavidLibre-Regular.ttf`, `-Bold.ttf` | OFL-1.1 (`OFL-DavidLibre.txt`) | © The David Libre Project Authors | + +## Note on Miriam Mono CLM (GPL-2) + +Miriam Mono CLM is under the GNU GPL v2 **without** the Culmus font exception. +It is included because it is the only freely-licensed *monospace* Hebrew font, so +it is the one Hebrew option that aligns perfectly on the terminal's fixed grid. +It remains a separate GPL-2 work; the chelamux codebase stays MIT. To ship a +fully OFL-only build, drop `MiriamMonoCLM-*.ttf` + `GPL-2.txt`, remove the +`miriam` option from the picker (`app.py` `_TERM_FONTS` / `_TERM_FONT_PREF_SHIM` +and `nav.js` `TERM_FONT_LABELS`), and pick an OFL Hebrew font as the default. diff --git a/chela/dashboard/static/fonts/Rubik.ttf b/chela/dashboard/static/fonts/Rubik.ttf new file mode 100644 index 0000000..a59aeb6 Binary files /dev/null and b/chela/dashboard/static/fonts/Rubik.ttf differ diff --git a/chela/dashboard/static/fonts/SourceCodePro.ttf b/chela/dashboard/static/fonts/SourceCodePro.ttf new file mode 100644 index 0000000..4d66828 Binary files /dev/null and b/chela/dashboard/static/fonts/SourceCodePro.ttf differ diff --git a/chela/dashboard/static/js/knowledge.js b/chela/dashboard/static/js/knowledge.js new file mode 100644 index 0000000..37426b2 --- /dev/null +++ b/chela/dashboard/static/js/knowledge.js @@ -0,0 +1,420 @@ +// --------------------------------------------------------------------------- +// Render: Knowledge (OKF viewer) +// +// Read-only browser over the exported OKF bundle (chela's fleet knowledge as +// typed markdown + frontmatter). Four surfaces per docs/OKF.md: glance (counts / +// freshest / activity log), browse (concepts by directory → a concept with its +// frontmatter header card + computed BACKLINKS), search, and graph. +// +// The bundle is PRIVATE local data; the routes it reads (/api/knowledge/*) are +// loopback-only. This module is vanilla JS like the rest of the dashboard. The +// markdown render + path resolve helpers (knMd/knResolve) are deliberately +// self-contained so the portable viewer.html (Phase 4) can reuse them verbatim. +// --------------------------------------------------------------------------- + +const _kn = { tree: null, view: 'glance', path: null, q: '' }; + +// Entry point — called by the global refresh loop while the Knowledge tab is +// active. Loads the tree once; never clobbers a concept the user is reading. +async function refreshKnowledge() { + if (!_kn.tree) { + await knLoadTree(); + return; + } + if (_kn.view === 'glance') knRenderGlance(); +} + +async function knLoadTree() { + const el = $('#kn-content'); + if (el && !_kn.tree) el.innerHTML = '
Loading knowledge…
'; + _kn.tree = await api('/api/knowledge/tree'); + if (_kn.view === 'glance') knRenderGlance(); +} + +async function knRefresh(btn) { + if (btn) { btn.disabled = true; btn.textContent = 'Exporting…'; } + _kn.tree = await api('/api/knowledge/export', { method: 'POST' }); + if (btn) { btn.disabled = false; btn.textContent = 'Refresh'; } + knBackToGlance(); +} + +function knBackToGlance() { + _kn.view = 'glance'; + _kn.path = null; + const s = $('#kn-search'); if (s) s.value = ''; + knRenderGlance(); +} + +// --- Glance + Browse ------------------------------------------------------- + +function knRenderGlance() { + const t = _kn.tree; + const el = $('#kn-content'); + if (!el) return; + if (!t || t.exported === false) { + el.innerHTML = ` +
+
+
No knowledge bundle yet
+
+ chela exports its fleet's working knowledge — agents, runs, schedules and the + projects they touch — as an Open Knowledge Format bundle: typed markdown you can + glance at, browse, and follow by backlink. +
+ Export the bundle → +
`; + return; + } + + // Split concepts by role so the glance can lead with the live entities + // (agents → what they're doing) instead of repeating one flat list twice. + const all = [].concat(...Object.values(t.dirs || {})); + const agents = all.filter(c => knTypeClass(c.type) === 'agent') + .sort((a, b) => a.title.localeCompare(b.title)); + const projects = all.filter(c => knTypeClass(c.type) === 'project') + .sort((a, b) => a.title.localeCompare(b.title)); + const others = all.filter(c => !['agent', 'project'].includes(knTypeClass(c.type))); + const projByTitle = {}; + projects.forEach(p => { projByTitle[p.title] = p.path; }); + + // Synthesized one-line digest — the "what's in here right now" answer. + const prCount = all.filter(c => c.pr_url).length; + const freshTs = all.map(c => c.timestamp).filter(Boolean).sort().slice(-1)[0]; + const bits = []; + if (agents.length) bits.push(`${agents.length} agent${agents.length === 1 ? '' : 's'}`); + if (projects.length) bits.push(`${projects.length} project${projects.length === 1 ? '' : 's'}`); + if (prCount) bits.push(`${prCount} PR${prCount === 1 ? '' : 's'}`); + const otherCounts = {}; + others.forEach(c => { otherCounts[c.type] = (otherCounts[c.type] || 0) + 1; }); + Object.keys(otherCounts).sort().forEach(ty => bits.push(`${otherCounts[ty]} ${escHtml(ty.toLowerCase())}${otherCounts[ty] === 1 ? '' : 's'}`)); + const digest = bits.join(' · ') + (freshTs ? ` · updated ${relativeTime(freshTs)}` : ''); + + // Sections that only render when they have content (graceful when sparse). + const sessions = agents.length ? ` +
Sessions · what the fleet is working on
+ ${agents.map(a => knAgentRow(a, projByTitle)).join('')}` : ''; + + const projChips = projects.length ? ` +
Projects
+
${projects.map(knProjectChip).join('')}
` : ''; + + const otherSections = Object.keys(otherCounts).sort().map(ty => { + const items = others.filter(c => c.type === ty) + .sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || '')); + return `
${escHtml(ty)}s
${items.map(knCardRow).join('')}`; + }).join(''); + + const activity = (t.log && /\n-\s/.test(t.log)) ? ` +
Activity
+
${knMd(t.log, 'log.md')}
` : ''; + + // Full flat listing kept for completeness, but folded away — the sections + // above are the primary read. + const dirs = Object.keys(t.dirs || {}).sort(); + const browse = dirs.map(d => ` +
+
${escHtml(d === '.' ? 'root' : d)} + ${t.dirs[d].length}
+ ${t.dirs[d].slice().sort((a, b) => a.title.localeCompare(b.title)).map(knCardRow).join('')} +
`).join(''); + + el.innerHTML = ` +
+
Glance + · OKF v${escHtml(t.okf_version || '')}
+
${digest || 'empty bundle'}
+ ${sessions} + ${projChips} + ${otherSections} + ${activity} +
+ Browse all ${t.total} concepts + ${browse || '
No concepts.
'} +
+
`; +} + +// A standard compact card (search results, backlinks, the flat browse list). +function knCardRow(c) { + const ts = c.timestamp ? `${relativeTime(c.timestamp)}` : ''; + return ` +
+ ${escHtml(c.type || 'concept')} + ${escHtml(c.title)} + ${c.description ? `${escHtml(c.description)}` : ''} + ${ts} +
`; +} + +// A richer agent row for the glance feed: name → project, what it's doing +// (recap-derived description), and its latest PR — the insight, not boilerplate. +function knAgentRow(a, projByTitle) { + const projPath = a.project && projByTitle[a.project]; + const proj = a.project + ? (projPath + ? `${escHtml(a.project)}` + : `${escHtml(a.project)}`) + : ''; + const ts = a.timestamp ? `${relativeTime(a.timestamp)}` : ''; + const pr = a.pr_url + ? `PR ↗` : ''; + return ` +
+ Agent +
+
+ ${escHtml(a.title)} + ${proj ? '' + proj : ''} + ${ts} +
+ ${a.description ? `
${escHtml(a.description)}
` : ''} +
+ ${pr} +
`; +} + +function knProjectChip(p) { + return ` + `; +} + +// --- Concept detail (frontmatter card + body + backlinks) ------------------ + +async function knOpen(path) { + _kn.view = 'concept'; + _kn.path = path; + const el = $('#kn-content'); + if (el) el.innerHTML = '
Loading…
'; + let c; + try { + c = await api('/api/knowledge/concept?path=' + encodeURIComponent(path)); + } catch (e) { + if (el) el.innerHTML = '
Could not load concept.
'; + return; + } + if (!el) return; + if (c.error || !c.path) { + el.innerHTML = `
← Knowledge +
Concept not found.
`; + return; + } + + const fm = c.frontmatter || {}; + const resource = fm.resource + ? `${escHtml(fm.resource)}` + : ''; + const tags = Array.isArray(fm.tags) && fm.tags.length + ? `
${fm.tags.map(t => `${escHtml(String(t))}`).join('')}
` : ''; + const ts = fm.timestamp ? `${relativeTime(fm.timestamp)}` : ''; + + // Backlinks — the headline feature: what links TO this concept. + const back = (c.backlinks || []).length + ? c.backlinks.map(b => ` +
+ ${escHtml(b.type || 'concept')} + ${escHtml(b.title)} +
`).join('') + : '
Nothing links here yet.
'; + + // Raw frontmatter (preserve unknown keys per OKF consumer spec). + const rawRows = Object.keys(fm).map(k => + `${escHtml(k)}${escHtml(knScalar(fm[k]))}`).join(''); + + el.innerHTML = ` +
+ ← Knowledge +
+
+ ${escHtml(c.type || 'concept')} +

${escHtml(c.title)}

+ ${ts} +
+ ${fm.description ? `
${escHtml(fm.description)}
` : ''} + ${resource} + ${tags} +
+
${knMd(c.body || '', c.path)}
+
+
Referenced by ${(c.backlinks || []).length}
+ ${back} +
+
+ Raw frontmatter (${Object.keys(fm).length}) + ${rawRows}
+
+
`; +} + +// --- Search ---------------------------------------------------------------- + +let _knSearchTimer = null; +function knOnSearch(q) { + _kn.q = q; + clearTimeout(_knSearchTimer); + _knSearchTimer = setTimeout(() => knRunSearch(q.trim()), 180); +} + +function knFilterType(type) { + const s = $('#kn-search'); + if (s) s.value = ''; + _kn.q = ''; + knRunSearch('', type); +} + +async function knRunSearch(q, type) { + if (!q && !type) { knBackToGlance(); return; } + _kn.view = 'search'; + const el = $('#kn-content'); + let params = '/api/knowledge/search?q=' + encodeURIComponent(q || ''); + if (type) params += '&type=' + encodeURIComponent(type); + const rows = await api(params); + if (!el) return; + const head = `
Search + · ${rows.length} result${rows.length === 1 ? '' : 's'}${type ? ' · type ' + escHtml(type) : ''} + ← Knowledge
`; + const body = rows.length ? rows.map(r => ` +
+ ${escHtml(r.type || 'concept')} + ${escHtml(r.title)} + ${r.description ? `${escHtml(r.description)}` : ''} + ${r.snippet ? `
${escHtml(r.snippet)}
` : ''} +
`).join('') : '
No matches.
'; + el.innerHTML = `
${head}${body}
`; +} + +// --- Graph ----------------------------------------------------------------- + +async function knShowGraph() { + _kn.view = 'graph'; + const el = $('#kn-content'); + if (el) el.innerHTML = '
Building graph…
'; + const g = await api('/api/knowledge/graph'); + if (!el) return; + const nodes = g.nodes || [], edges = g.edges || []; + if (!nodes.length) { + el.innerHTML = '
← Knowledge' + + '
No concepts to graph.
'; + return; + } + // Circular layout: cheap, deterministic, dependency-free. Position nodes on a + // ring, draw edges as lines, label each node; click to open the concept. + const W = 760, H = 520, cx = W / 2, cy = H / 2, R = Math.min(W, H) / 2 - 70; + const pos = {}; + nodes.forEach((n, i) => { + const a = (i / nodes.length) * 2 * Math.PI - Math.PI / 2; + pos[n.id] = { x: cx + R * Math.cos(a), y: cy + R * Math.sin(a) }; + }); + const lines = edges.map(e => { + const a = pos[e.source], b = pos[e.target]; + if (!a || !b) return ''; + return ``; + }).join(''); + const dots = nodes.map(n => { + const p = pos[n.id]; + const anchor = p.x < cx - 20 ? 'end' : (p.x > cx + 20 ? 'start' : 'middle'); + const dx = p.x < cx - 20 ? -8 : (p.x > cx + 20 ? 8 : 0); + return ` + + ${escHtml(n.title)} + `; + }).join(''); + el.innerHTML = ` +
+
Graph + · ${nodes.length} concepts · ${edges.length} links + ← Knowledge
+
+ + ${lines}${dots} + +
+
`; +} + +// --- Shared helpers (also reused by the portable viewer, Phase 4) ---------- + +// Short, deterministic class suffix for a type badge / node colour. +function knTypeClass(type) { + const t = (type || '').toLowerCase(); + if (t.includes('agent')) return 'agent'; + if (t.includes('run')) return 'run'; + if (t.includes('schedul')) return 'sched'; + if (t.includes('project')) return 'project'; + return 'other'; +} + +function knScalar(v) { + if (v === null || v === undefined) return ''; + if (Array.isArray(v)) return v.join(', '); + if (typeof v === 'object') return JSON.stringify(v); + return String(v); +} + +// Resolve a bundle markdown link (relative or /absolute) to a root-relative +// posix path, given the linking file's own path. Mirrors okf._resolve_link. +function knResolve(base, href) { + href = href.split('#')[0]; + if (href.startsWith('/')) return href.replace(/^\/+/, ''); + const dir = base.includes('/') ? base.slice(0, base.lastIndexOf('/')) : ''; + const parts = (dir ? dir.split('/') : []).concat(href.split('/')); + const out = []; + for (const p of parts) { + if (p === '' || p === '.') continue; + if (p === '..') out.pop(); else out.push(p); + } + return out.join('/'); +} + +function knLink(text, href, base) { + if (/^(https?:|mailto:)/i.test(href)) { + return `${text}`; + } + if (href.startsWith('#')) return text; + if (href.split('#')[0].endsWith('.md')) { + return `${text}`; + } + return `${text}`; +} + +// Inline markdown on an already-HTML-escaped string: code, links, bold. +function knInline(s, base) { + s = escHtml(s); + s = s.replace(/`([^`]+)`/g, '$1'); + s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (m, t, href) => knLink(t, href, base)); + s = s.replace(/\*\*([^*]+)\*\*/g, '$1'); + return s; +} + +// Minimal block-level markdown → HTML for OKF bodies (headings, lists, +// blockquotes, fenced code, paragraphs). Intentionally tiny and dependency-free. +function knMd(src, base) { + const lines = (src || '').split('\n'); + let html = '', inList = false, inCode = false; + const closeList = () => { if (inList) { html += ''; inList = false; } }; + for (const raw of lines) { + if (/^```/.test(raw)) { + closeList(); + if (inCode) { html += ''; inCode = false; } + else { html += '
'; inCode = true; }
+            continue;
+        }
+        if (inCode) { html += escHtml(raw) + '\n'; continue; }
+        const line = raw.replace(/\s+$/, '');
+        if (line === '') { closeList(); continue; }
+        const h = line.match(/^(#{1,4})\s+(.*)$/);
+        if (h) { closeList(); const lv = h[1].length; html += `${knInline(h[2], base)}`; continue; }
+        if (/^>\s?/.test(line)) { closeList(); html += `
${knInline(line.replace(/^>\s?/, ''), base)}
`; continue; } + const li = line.match(/^[-*]\s+(.*)$/); + if (li) { if (!inList) { html += '
    '; inList = true; } html += `
  • ${knInline(li[1], base)}
  • `; continue; } + closeList(); + html += `

    ${knInline(line, base)}

    `; + } + closeList(); + if (inCode) html += '
'; + return html; +} diff --git a/chela/dashboard/static/js/main.js b/chela/dashboard/static/js/main.js index 138bcc3..6c2c438 100644 --- a/chela/dashboard/static/js/main.js +++ b/chela/dashboard/static/js/main.js @@ -12,6 +12,7 @@ async function refresh() { if (typeof refreshLauncher === 'function') refreshLauncher(); if (currentTab === 'agents') await refreshAgents(); else if (currentTab === 'schedules') await refreshSchedules(); + else if (currentTab === 'knowledge') await refreshKnowledge(); else if (currentTab === 'agent-detail') renderAgentDetail(); else if (currentTab === 'terminals') await renderTerminals(); // Dispatcher and Kanban views own their own polling timers; the global diff --git a/chela/dashboard/static/js/nav.js b/chela/dashboard/static/js/nav.js index 171e1cd..0bbdf49 100644 --- a/chela/dashboard/static/js/nav.js +++ b/chela/dashboard/static/js/nav.js @@ -45,6 +45,9 @@ function selectView(view) { else { stopKanbanTimer(); } if (TERMINALS_ON && view === 'terminals') startTermTimer(); else if (TERMINALS_ON) stopTermTimer(); + // Entering Knowledge from the nav lands on the glance overview, not whatever + // concept was last open. + if (view === 'knowledge' && typeof knBackToGlance === 'function' && _kn.tree) knBackToGlance(); closeSidebar(); // navigating dismisses the mobile drawer (no-op on desktop) refresh(); @@ -389,6 +392,9 @@ function renderSettings(focus) { const body = document.getElementById('drawer-body'); if (!body) return; const theme = localStorage.getItem('chela_theme') || 'dark'; + const termLatin = localStorage.getItem('chela_term_latin') || 'jetbrains'; + const termFont = localStorage.getItem('chela_term_font') || 'miriam'; + const termSize = localStorage.getItem('chela_term_fontsize') || '14'; body.innerHTML = `

Projects folder

@@ -441,6 +447,38 @@ function renderSettings(focus) {
+
+

Terminal font

+

Applies live to every open terminal, saved per browser. + Pick the English (monospace) and Hebrew faces + independently. Only Miriam Mono keeps Hebrew on the grid — the + other Hebrew faces are proportional: nicer letters, slight drift in the fixed cells.

+
+ English font + +
+
+ Hebrew font + +
+
+ Size + +
+
+

Terminal wall

@@ -497,6 +535,58 @@ function setTheme(t) { document.body.dataset.theme = t; } +// Terminal font options. Keys are stored in localStorage and mapped to real +// family names by the shim injected into each ttyd page (app.py +// _TERM_FONT_PREF_SHIM) — keep the keys here in sync with LAT/HEB there. +// English (Latin) faces are all monospace; the Hebrew list has one monospace +// (Miriam) and the rest proportional (trade grid alignment for nicer letters). +const TERM_LATIN_LABELS = { + jetbrains: 'JetBrains Mono', + firacode: 'Fira Code · ligatures', + plex: 'IBM Plex Mono', + source: 'Source Code Pro', + cascadia: 'Cascadia Code · ligatures', +}; + +const TERM_FONT_LABELS = { + miriam: 'Miriam Mono · aligned', + noto: 'Noto Sans Hebrew · modern', + heebo: 'Heebo · rounded', + assistant: 'Assistant · humanist', + rubik: 'Rubik · rounded', + frankruhl: 'Frank Ruhl · serif', + david: 'David Libre · classic', +}; + +// Terminal font + size are per-viewer prefs (like the theme), stored in +// localStorage and applied live to every ttyd iframe. The iframes are +// same-origin, so writing localStorage fires a `storage` event inside each of +// them (the shim listens); we ALSO call into each iframe directly for instant +// feedback in the frame that made the change. +function setTermLatin(v) { + localStorage.setItem('chela_term_latin', v); + applyTermPrefsToIframes(); +} + +function setTermFont(v) { + localStorage.setItem('chela_term_font', v); + applyTermPrefsToIframes(); +} + +function setTermSize(v) { + localStorage.setItem('chela_term_fontsize', v); + applyTermPrefsToIframes(); +} + +function applyTermPrefsToIframes() { + document.querySelectorAll('iframe').forEach(f => { + try { + const w = f.contentWindow; + if (w && typeof w.chelaApplyTermPrefs === 'function') w.chelaApplyTermPrefs(); + } catch (e) { /* not-yet-loaded — the storage event covers it */ } + }); +} + // --- "+ new" popover ------------------------------------------------------- function openNewMenu(ev) { @@ -589,7 +679,7 @@ function _paletteItems() { const items = []; const views = []; if (TERMINALS_ON) views.push(['terminals', 'Wall']); - views.push(['agents', 'Agents'], ['dispatcher', 'Dispatch'], ['kanban', 'Kanban'], ['schedules', 'Schedules']); + views.push(['agents', 'Agents'], ['dispatcher', 'Dispatch'], ['kanban', 'Kanban'], ['schedules', 'Schedules'], ['knowledge', 'Knowledge']); views.forEach(([v, label]) => items.push({ icon: '▦', title: label, sub: 'view', run: () => selectView(v) })); (_agentsCache || []).forEach(a => { diff --git a/chela/dashboard/static/style.css b/chela/dashboard/static/style.css index 889904f..3d35d42 100644 --- a/chela/dashboard/static/style.css +++ b/chela/dashboard/static/style.css @@ -382,6 +382,16 @@ body.pane-is-maximized .grid-stack-item:not(:has(.pane-maximized)) { } .min-dock-label { color: var(--text-dim); font-size: 11px; margin-right: 2px; } +/* Pin the minimized dock to the bottom of the wall. The panel fills the canvas + as a flex column and #term-stage grows to take the slack, so the dock sits at + the bottom even when EVERY pane is minimized — without this the emptied grid + collapses to ~0 and the dock rides up under the toolbar. Desktop/wall only: + phones force single mode (no dock) with a fixed keybar, so leave them alone. */ +@media (min-width: 769px) { + #panel-terminals.active { display: flex; flex-direction: column; min-height: 100%; } + #panel-terminals.active #term-stage { flex: 1 1 auto; min-height: 0; } +} + /* Live session-activity dot on pane headers + dock chips. Driven by `claude agents --json` status: busy = green pulse, waiting = amber (needs input), idle/unknown = dim. */ @@ -2173,3 +2183,136 @@ input[type="text"]:focus, textarea:focus, select:focus { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } } + +/* --------------------------------------------------------------------------- + * Knowledge (OKF viewer) — glance / browse / concept / search / graph. + * ------------------------------------------------------------------------- */ +.kn-loading, .kn-dim { color: var(--text-dim); font-size: 12px; padding: 8px 0; } +.kn-section-title { font-size: 13px; font-weight: 600; margin: 18px 0 8px; } +.kn-section-title:first-child { margin-top: 4px; } +.kn-sub { font-size: 11px; font-weight: 600; color: var(--text-dim); + text-transform: uppercase; letter-spacing: .04em; margin: 4px 0 8px; } + +/* type chips (counts) */ +.kn-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 6px; } +.kn-chip { background: var(--surface); border: 1px solid var(--border); + color: var(--text); border-radius: 999px; padding: 4px 12px; font-size: 12px; + cursor: pointer; font-family: var(--font); } +.kn-chip:hover { border-color: var(--accent); } +.kn-chip-n { color: var(--accent); font-weight: 700; margin-right: 4px; } + +.kn-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-bottom: 6px; } +@media (max-width: 720px) { .kn-cols { grid-template-columns: 1fr; } } + +/* concept cards (rows in glance / browse / search / backlinks) */ +.kn-card { display: flex; align-items: center; gap: 8px; padding: 8px 10px; + border: 1px solid var(--border); border-radius: 8px; background: var(--surface); + margin-bottom: 6px; cursor: pointer; } +.kn-card:hover { border-color: var(--accent); } +.kn-card-title { font-weight: 600; font-size: 13px; white-space: nowrap; } +.kn-card-desc { color: var(--text-dim); font-size: 12px; overflow: hidden; + text-overflow: ellipsis; white-space: nowrap; flex: 1; } +.kn-card-ts { margin-left: auto; color: var(--text-dim); font-size: 11px; flex-shrink: 0; } +.kn-snippet { color: var(--text-dim); font-size: 11px; width: 100%; margin-top: 4px; + border-left: 2px solid var(--border); padding-left: 8px; } + +.kn-dir { margin-bottom: 14px; } +.kn-dir-head { font-size: 12px; font-weight: 600; color: var(--text); margin: 8px 0 6px; + text-transform: lowercase; } +.kn-dir-head .kn-dim { padding: 0; margin-left: 4px; } + +.kn-log :is(h1,h2) { font-size: 12px; margin: 10px 0 4px; color: var(--text-dim); } +.kn-log :is(ul) { margin: 0 0 6px; padding-left: 16px; } +.kn-log li { font-size: 12px; margin: 2px 0; } + +/* type badges + graph node colours */ +.kn-badge { font-size: 10px; font-weight: 700; text-transform: uppercase; + letter-spacing: .03em; padding: 2px 7px; border-radius: 5px; flex-shrink: 0; + border: 1px solid transparent; } +.kn-badge-agent { color: var(--green); border-color: var(--green); background: color-mix(in srgb, var(--green) 12%, transparent); } +.kn-badge-run { color: var(--accent); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); } +.kn-badge-sched { color: var(--yellow); border-color: var(--yellow); background: color-mix(in srgb, var(--yellow) 12%, transparent); } +.kn-badge-project { color: #a371f7; border-color: #a371f7; background: color-mix(in srgb, #a371f7 14%, transparent); } +.kn-badge-other { color: var(--text-dim); border-color: var(--border); background: var(--surface); } + +/* concept detail */ +.kn-back { color: var(--accent); cursor: pointer; font-size: 12px; display: inline-block; margin-bottom: 12px; } +.kn-back:hover { text-decoration: underline; } +.kn-head-card { border: 1px solid var(--border); border-radius: 10px; + background: var(--surface); padding: 14px 16px; margin-bottom: 16px; } +.kn-head-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.kn-title { font-size: 18px; margin: 0; font-weight: 700; } +.kn-resource { display: inline-block; margin-top: 8px; color: var(--accent); + font-size: 12px; word-break: break-all; } +.kn-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; } +.kn-tag { font-size: 11px; color: var(--text-dim); border: 1px solid var(--border); + border-radius: 999px; padding: 1px 9px; } + +.kn-body { font-size: 13px; line-height: 1.6; max-width: 760px; } +.kn-body :is(.kn-mh) { font-size: 14px; font-weight: 700; margin: 16px 0 6px; } +.kn-body p { margin: 6px 0; } +.kn-body .kn-ul { margin: 6px 0; padding-left: 20px; } +.kn-body li { margin: 3px 0; } +.kn-body blockquote { border-left: 3px solid var(--border); margin: 8px 0; + padding: 2px 0 2px 12px; color: var(--text-dim); } +.kn-body code { background: var(--bg); border: 1px solid var(--border); + border-radius: 4px; padding: 1px 5px; font-size: 12px; } +.kn-body .kn-code { background: var(--bg); border: 1px solid var(--border); + border-radius: 8px; padding: 10px 12px; overflow-x: auto; } +.kn-body .kn-code code { background: none; border: none; padding: 0; } +.kn-link { color: var(--accent); cursor: pointer; } +.kn-link:hover { text-decoration: underline; } + +.kn-panel { margin-top: 20px; border-top: 1px solid var(--border); padding-top: 14px; } +.kn-raw { margin-top: 18px; } +.kn-raw summary { cursor: pointer; color: var(--text-dim); font-size: 12px; } +.kn-raw-tbl { width: 100%; max-width: 760px; margin-top: 8px; border-collapse: collapse; font-size: 12px; } +.kn-raw-tbl td { border: 1px solid var(--border); padding: 4px 8px; vertical-align: top; } +.kn-raw-k { color: var(--text-dim); white-space: nowrap; width: 1%; } + +/* graph */ +.kn-graph-wrap { border: 1px solid var(--border); border-radius: 10px; + background: var(--surface); padding: 8px; } +.kn-graph { width: 100%; height: auto; } +.kn-edge { stroke: var(--border); stroke-width: 1; } +.kn-node { cursor: pointer; } +.kn-node text { font-size: 10px; fill: var(--text-dim); font-family: var(--font); } +.kn-node:hover text { fill: var(--text); } +.kn-node circle { stroke: var(--bg); stroke-width: 1.5; } +.kn-node-agent circle { fill: var(--green); } +.kn-node-run circle { fill: var(--accent); } +.kn-node-sched circle { fill: var(--yellow); } +.kn-node-project circle { fill: #a371f7; } +.kn-node-other circle { fill: var(--text-dim); } + +/* Knowledge glance — digest + sessions feed + project chips */ +.kn-digest { font-size: 13px; color: var(--text); margin: 2px 0 18px; + padding-bottom: 12px; border-bottom: 1px solid var(--border); } +.kn-feed-row { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; + border: 1px solid var(--border); border-radius: 8px; background: var(--surface); + margin-bottom: 8px; cursor: pointer; } +.kn-feed-row:hover { border-color: var(--accent); } +.kn-feed-main { flex: 1; min-width: 0; } +.kn-feed-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +.kn-feed-arrow { color: var(--text-dim); } +.kn-feed-proj { color: var(--accent); font-size: 12px; cursor: pointer; } +.kn-feed-proj:hover { text-decoration: underline; } +.kn-feed-top .kn-card-ts { margin-left: auto; } +.kn-feed-desc { color: var(--text-dim); font-size: 12px; margin-top: 3px; + overflow: hidden; text-overflow: ellipsis; display: -webkit-box; + -webkit-line-clamp: 2; -webkit-box-orient: vertical; } +.kn-pr { flex-shrink: 0; align-self: center; color: var(--accent); font-size: 11px; + font-weight: 600; border: 1px solid var(--border); border-radius: 6px; + padding: 2px 8px; text-decoration: none; } +.kn-pr:hover { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); } +.kn-pchips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 6px; } +.kn-pchip { display: inline-flex; align-items: baseline; gap: 8px; background: var(--surface); + border: 1px solid var(--border); border-radius: 8px; padding: 7px 12px; cursor: pointer; + font-family: var(--font); color: var(--text); } +.kn-pchip:hover { border-color: #a371f7; } +.kn-pchip-name { font-weight: 600; font-size: 13px; } +.kn-pchip .kn-dim { padding: 0; font-size: 11px; } +.kn-browse { margin-top: 20px; border-top: 1px solid var(--border); padding-top: 12px; } +.kn-browse > summary { cursor: pointer; color: var(--text-dim); font-size: 12px; + font-weight: 600; text-transform: uppercase; letter-spacing: .04em; } +.kn-browse[open] > summary { margin-bottom: 10px; } diff --git a/chela/dashboard/templates/index.html b/chela/dashboard/templates/index.html index 7facaf3..550fd42 100644 --- a/chela/dashboard/templates/index.html +++ b/chela/dashboard/templates/index.html @@ -81,6 +81,9 @@ Schedules -
+
+ Knowledge +
@@ -183,6 +186,17 @@ + +
+
+ + + +
+
+
+
@@ -443,6 +457,7 @@

Init a repo for the dispatcher

+ {% if terminals_enabled %}{% endif %} diff --git a/chela/main.py b/chela/main.py index e62dc60..17ee249 100644 --- a/chela/main.py +++ b/chela/main.py @@ -12,8 +12,9 @@ import os import sys import time +from pathlib import Path -from chela import agent_manager, discovery, dispatcher, messenger, notify, scheduler +from chela import agent_manager, discovery, dispatcher, messenger, notify, okf, scheduler from chela.config import ( TMUX_SESSION, SCHEDULER_POLL_INTERVAL, @@ -191,6 +192,23 @@ def cmd_dispatch_runs(args) -> None: print(f" {r['task_id']} {r['status']:<16} attempt={r['attempt']} {r.get('window_name') or '-':<24} {title}") +def cmd_knowledge_export(args) -> None: + """Export the fleet's knowledge as an OKF v0.1 bundle. + + The bundle is LOCAL fleet data — keep it out of version control (the default + output is ``~/.chela/knowledge``, outside any repo). See docs/OKF.md. + """ + out = Path(args.out).expanduser() if args.out else None + summary = okf.export_bundle(out_dir=out, since=args.since) + print(f"OKF v{summary['okf_version']} bundle → {summary['out']}") + print( + f" agents={summary['agents']} runs={summary['runs']} " + f"schedules={summary['schedules']} projects={summary['projects']}" + ) + if summary.get("since"): + print(f" (runs filtered since {summary['since']})") + + def cmd_install_statusline(args) -> None: """Print (or write) the Claude Code statusLine hook that feeds the context bar. @@ -200,7 +218,6 @@ def cmd_install_statusline(args) -> None: settings file and refuses to clobber an existing statusLine without ``--force``. """ import json as _json - from pathlib import Path script = Path(__file__).resolve().parent.parent / "scripts" / "cache-statusline.sh" snippet = {"type": "command", "command": str(script)} @@ -329,6 +346,13 @@ def main() -> None: # dispatch-runs (inspection) sub.add_parser("dispatch-runs", help="List dispatcher runs") + # knowledge — export the fleet's knowledge as an OKF bundle (local data; see docs/OKF.md) + p_know = sub.add_parser("knowledge", help="Export fleet knowledge as an OKF bundle") + know_sub = p_know.add_subparsers(dest="know_cmd") + p_kexp = know_sub.add_parser("export", help="Write an OKF v0.1 bundle of runs/schedules/agents/projects") + p_kexp.add_argument("--out", default=None, help="Output dir (default: ~/.chela/knowledge)") + p_kexp.add_argument("--since", default=None, help="Only include runs started on/after this ISO date") + # install-statusline — wire the context-bar producer into Claude Code p_sl = sub.add_parser( "install-statusline", @@ -373,6 +397,11 @@ def main() -> None: cmd_dispatch(args) elif args.command == "dispatch-runs": cmd_dispatch_runs(args) + elif args.command == "knowledge": + if args.know_cmd == "export": + cmd_knowledge_export(args) + else: + p_know.print_help() elif args.command == "install-statusline": cmd_install_statusline(args) elif args.command == "dashboard": diff --git a/chela/okf.py b/chela/okf.py new file mode 100644 index 0000000..0f66f9f --- /dev/null +++ b/chela/okf.py @@ -0,0 +1,734 @@ +"""OKF serializer — export chela's fleet knowledge as an Open Knowledge Format bundle. + +Turns chela's runtime state (dispatcher ``runs``, scheduled ``tasks``, live agent +windows, the projects they touch) into a portable `Open Knowledge Format +`_ (OKF +v0.1) bundle: a directory of typed markdown files with YAML frontmatter, plus the +reserved ``index.md`` (per-directory listing) and ``log.md`` (date-grouped +history) files. See ``docs/OKF.md`` for the design. + +Conformance is deliberately small: every non-reserved ``.md`` carries frontmatter +with a non-empty ``type``. We emit the recommended soft fields too (``title``, +``description``, ``resource``, ``tags``, ``timestamp``) and extension keys; a +consumer must tolerate unknown keys / broken links / missing optionals. + +⚠️ Boundary (see ``docs/OKF.md`` → Security / exposure): the *bundle is local +fleet data and is never published.* The default output lives outside the repo +(``~/.chela/knowledge/``); exporting into a git working tree is flagged. + +Pure serializer: state in → files out. No daemon, no new deps (``pyyaml`` is +already required for WORKFLOW.md parsing). +""" +from __future__ import annotations + +import logging +import posixpath +import re +from datetime import datetime, timezone +from pathlib import Path + +import yaml + +from chela import discovery, dispatcher, scheduler, transcripts +from chela.config import CHELA_DIR + +log = logging.getLogger(__name__) + +OKF_VERSION = "0.1" +DEFAULT_OUT = CHELA_DIR / "knowledge" + +# OKF type names for chela's concepts (see docs/OKF.md producer table). +TYPE_RUN = "Dispatch Run" +TYPE_SCHEDULE = "Scheduled Task" +TYPE_AGENT = "Agent" +TYPE_PROJECT = "Project" + +_SLUG_RE = re.compile(r"[^A-Za-z0-9._-]+") + + +# --------------------------------------------------------------------------- +# Small serialization helpers +# --------------------------------------------------------------------------- + +def _now() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _slug(value: str) -> str: + """Filesystem-safe stem for a concept filename (never empty).""" + s = _SLUG_RE.sub("-", (value or "").strip()).strip("-.") + return s or "unnamed" + + +def _excerpt(text: str | None, limit: int = 160) -> str: + """First meaningful line of a prose blob, condensed to a one-line summary. + + Used to turn an agent's latest recap into its ``description`` so a glance card + reads as *what the agent is doing* rather than boilerplate. Skips blank and + markdown-heading lines; collapses whitespace; truncates on a word boundary. + """ + for raw in (text or "").splitlines(): + line = raw.strip() + if not line or line.startswith("#"): # skip blanks + markdown headings → reach the prose + continue + line = re.sub(r"\s+", " ", line) + if len(line) <= limit: + return line + return line[:limit].rsplit(" ", 1)[0].rstrip(",.;:") + "…" + return "" + + +def _frontmatter(meta: dict) -> str: + """Render an OKF frontmatter block, dropping empty optional fields. + + ``type`` is always kept (it's the one required field); other keys are + omitted when their value is empty so the bundle stays terse. + """ + clean = {k: v for k, v in meta.items() if k == "type" or v not in (None, "", [], {})} + body = yaml.safe_dump(clean, sort_keys=False, allow_unicode=True, default_flow_style=False) + return f"---\n{body}---\n" + + +def _rel(from_rel: str, to_rel: str) -> str: + """Bundle-relative markdown link from one file to another (both repo-root-relative). + + Relative (``../runs/x.md``) rather than absolute (``/runs/x.md``) so links + resolve under ``file://`` for the portable viewer too. + """ + rel = posixpath.relpath(to_rel, posixpath.dirname(from_rel)) + return rel if rel.startswith(".") else f"./{rel}" + + +def _doc(meta: dict, *body: str, citations: list[str] | None = None) -> str: + """Assemble a concept file: frontmatter + body sections + optional citations.""" + parts = [_frontmatter(meta), ""] + parts.extend(s for s in body if s) + if citations: + parts.append("\n# Citations\n") + parts.extend(f"- {c}" for c in citations) + return "\n".join(parts).rstrip() + "\n" + + +def _index(title: str, description: str, entries: list[str], *, root: bool = False) -> str: + """A reserved ``index.md`` — a progressive-disclosure listing. + + Per spec, index files carry NO frontmatter except the bundle-root index, + which is the only place ``okf_version`` is declared. + """ + head = "" + if root: + head = _frontmatter({ + "okf_version": OKF_VERSION, + "title": title, + "description": description, + "timestamp": _now(), + }) + "\n" + lines = [head, f"# {title}\n", description, ""] + lines.extend(entries or ["_(empty)_"]) + return "\n".join(ln for ln in lines if ln is not None).rstrip() + "\n" + + +def _listing(title: str, url: str, desc: str = "") -> str: + """One ``index.md`` entry: ``* [Title](url) - description`` (reserved format).""" + line = f"* [{title}]({url})" + return f"{line} - {desc}" if desc else line + + +def _within_git_repo(path: Path) -> Path | None: + """The nearest ancestor that is a git working tree, or None. + + Used to warn when an export would land inside version control — the bundle + is private fleet data and must not be committed (docs/OKF.md → Security). + """ + for parent in [path, *path.parents]: + if (parent / ".git").exists(): + return parent + return None + + +# --------------------------------------------------------------------------- +# Producers — one per OKF type +# --------------------------------------------------------------------------- + +def _run_doc(run: dict) -> str: + title = run.get("title") or run.get("task_id") or "run" + status = run.get("status") or "unknown" + workflow = Path(run.get("workflow_path") or "").name + window = run.get("window_name") or "" + body = [f"**Status:** `{status}` · attempt {run.get('attempt', 1)}"] + if workflow: + body.append(f"**Workflow:** `{workflow}`") + if run.get("branch_name"): + body.append(f"**Branch:** `{run['branch_name']}`") + if run.get("pr_url"): + body.append(f"**PR:** {run['pr_url']}" + (f" ({run['pr_state']})" if run.get("pr_state") else "")) + if window: + body.append(f"**Agent:** [{window}]({_rel('runs/x.md', f'agents/{_slug(window)}.md')})") + if run.get("last_error"): + body.append(f"\n> error: {run['last_error']}") + meta = { + "type": TYPE_RUN, + "title": title, + "description": f"{status} — {workflow}" if workflow else status, + "resource": run.get("pr_url") or "", + "timestamp": run.get("ended_at") or run.get("started_at") or "", + "tags": [t for t in (status, workflow) if t], + # extension keys (consumers preserve unknown keys) + "status": status, + "workflow_path": run.get("workflow_path") or "", + "window_name": window, + "branch_name": run.get("branch_name") or "", + "pr_state": run.get("pr_state") or "", + "started_at": run.get("started_at") or "", + "ended_at": run.get("ended_at") or "", + } + citations = [f"`{run['worktree_path']}`"] if run.get("worktree_path") else None + return _doc(meta, "\n".join(body), citations=citations) + + +def _schedule_doc(task) -> str: + first_line = (task.prompt or "").strip().splitlines()[0] if task.prompt else "" + title = f"{task.agent_name}: {task.schedule_type} {task.schedule_value}" + body = [ + f"**Agent:** [{task.agent_name}]({_rel('schedules/x.md', f'agents/{_slug(task.agent_name)}.md')})", + f"**Schedule:** `{task.schedule_type}` = `{task.schedule_value}` · " + f"{'enabled' if task.enabled else 'disabled'}", + f"**Last run:** {task.last_run or '—'} · **Next run:** {task.next_run or '—'}", + "\n## Prompt\n", + (task.prompt or "").strip() or "_(empty)_", + ] + meta = { + "type": TYPE_SCHEDULE, + "title": title, + "description": first_line[:140], + "timestamp": task.next_run or "", + "tags": [t for t in (task.schedule_type, task.agent_name) if t], + "agent_name": task.agent_name, + "schedule_type": task.schedule_type, + "schedule_value": task.schedule_value, + "enabled": bool(task.enabled), + "last_run": task.last_run or "", + "next_run": task.next_run or "", + } + return _doc(meta, "\n".join(body)) + + +def _agent_doc(name: str, wid: str, runs_by_window: dict[str, list[dict]]) -> tuple[str, str | None]: + """Return (markdown, cwd). cwd is surfaced so the caller can build projects.""" + self_rel = f"agents/{_slug(name)}.md" + cwd = discovery.get_window_cwd(name) + recap = None + pr = None + tpath = transcripts._resolve_agent_transcript(name) + if tpath: + try: + recap = transcripts.latest_recap(tpath) + pr = transcripts.latest_pr(tpath) + except OSError: + log.debug("Could not read transcript for agent %s", name) + + body = [f"**Window:** `{wid}`" + (f" · **CWD:** `{cwd}`" if cwd else "")] + if cwd: + proj = Path(cwd).name + body.append(f"**Project:** [{proj}]({_rel(self_rel, f'projects/{_slug(proj)}.md')})") + if pr: + body.append(f"**Latest PR:** {pr.url}") + if recap: + body.append("\n## Latest recap\n") + body.append(recap.strip()) + + my_runs = runs_by_window.get(name, []) + if my_runs: + body.append("\n## Runs\n") + body.extend( + _listing( + r.get("title") or r.get("task_id") or "run", + _rel(self_rel, f"runs/{_slug(r['task_id'])}.md"), + r.get("status") or "", + ) + for r in my_runs + ) + + project = Path(cwd).name if cwd else "" + # description = what the agent is actually doing (its latest recap), so a + # glance card reads as insight; fall back to a plain locator when there's no + # transcript (e.g. a bare shell window). + description = _excerpt(recap) or ( + f"agent window {name}" + (f" @ {project}" if project else "")) + meta = { + "type": TYPE_AGENT, + "title": name, + "description": description, + "resource": f"file://{cwd}" if cwd else "", + "timestamp": _now(), + "tags": ["agent"], + "window_id": wid, + "cwd": cwd or "", + # extension keys (consumers preserve unknown keys) — let the viewer + # surface the agent's project + latest PR without parsing the body. + "project": project, + "pr_url": pr.url if pr else "", + } + citations = [f"`{tpath}`"] if tpath else None + return _doc(meta, "\n".join(body), citations=citations), cwd + + +def _project_doc(name: str, path: str, agents: list[str], runs: list[dict]) -> str: + self_rel = f"projects/{_slug(name)}.md" + body = [f"**Path:** `{path}`" if path else ""] + if agents: + body.append("\n## Agents\n") + body.extend(_listing(a, _rel(self_rel, f"agents/{_slug(a)}.md")) for a in sorted(set(agents))) + if runs: + body.append("\n## Runs\n") + body.extend( + _listing( + r.get("title") or r.get("task_id") or "run", + _rel(self_rel, f"runs/{_slug(r['task_id'])}.md"), + r.get("status") or "", + ) + for r in runs + ) + n_agents = len(set(agents)) + n_runs = len(runs) + rollup = " · ".join(filter(None, [ + f"{n_agents} agent{'s' if n_agents != 1 else ''}" if n_agents else "", + f"{n_runs} run{'s' if n_runs != 1 else ''}" if n_runs else "", + ])) or (f"project at {path}" if path else f"project {name}") + meta = { + "type": TYPE_PROJECT, + "title": name, + "description": rollup, + "resource": f"file://{path}" if path else "", + "timestamp": _now(), + "tags": ["project"], + "path": path or "", + "agent_count": n_agents, + "run_count": n_runs, + } + return _doc(meta, "\n".join(body)) + + +# --------------------------------------------------------------------------- +# log.md — date-grouped fleet activity, newest first +# --------------------------------------------------------------------------- + +def _log_md(runs: list[dict]) -> str: + """Build the reserved ``log.md``: ``## YYYY-MM-DD`` headings, newest first.""" + events: list[tuple[str, str, str]] = [] # (iso_ts, day, line) + for r in runs: + ts = r.get("ended_at") or r.get("started_at") + if not ts: + continue + day = ts[:10] + verb = "finished" if r.get("ended_at") else "started" + title = r.get("title") or r.get("task_id") or "run" + link = f"runs/{_slug(r['task_id'])}.md" + events.append((ts, day, f"- {verb} **[{title}]({link})** — `{r.get('status') or '?'}`")) + + events.sort(key=lambda e: e[0], reverse=True) + out = ["# Activity log\n", "Fleet activity, newest first.\n"] + current_day = None + for _ts, day, line in events: + if day != current_day: + out.append(f"\n## {day}\n") + current_day = day + out.append(line) + if not events: + out.append("_(no activity recorded)_") + return "\n".join(out).rstrip() + "\n" + + +# --------------------------------------------------------------------------- +# Orchestration +# --------------------------------------------------------------------------- + +def export_bundle(out_dir: Path | None = None, since: str | None = None) -> dict: + """Export the full OKF bundle. Returns a summary dict of what was written. + + ``since`` (ISO date/datetime) filters runs (and therefore the activity log) + to those started on/after it; agents/schedules/projects reflect current + state regardless. + """ + out = Path(out_dir) if out_dir else DEFAULT_OUT + out = out.expanduser() + + repo = _within_git_repo(out) + if repo: + log.warning( + "OKF export target %s is inside a git repo (%s) — the bundle is " + "PRIVATE fleet data and must not be committed (docs/OKF.md → Security).", + out, repo, + ) + + # --- gather state --- + runs = dispatcher.list_runs() + if since: + runs = [r for r in runs if (r.get("started_at") or "") >= since] + tasks = scheduler.list_tasks() + windows = discovery.get_all_windows() # {name: wid} + + runs_by_window: dict[str, list[dict]] = {} + for r in runs: + if r.get("window_name"): + runs_by_window.setdefault(r["window_name"], []).append(r) + + # --- write concept files --- + written = {"runs": 0, "schedules": 0, "agents": 0, "projects": 0} + projects: dict[str, dict] = {} # name -> {path, agents:set, runs:list} + + def _project(name: str, path: str = "") -> dict: + p = projects.setdefault(name, {"path": "", "agents": set(), "runs": []}) + if path and not p["path"]: + p["path"] = path + return p + + (out / "runs").mkdir(parents=True, exist_ok=True) + run_entries = [] + for r in runs: + fname = f"{_slug(r['task_id'])}.md" + (out / "runs" / fname).write_text(_run_doc(r), encoding="utf-8") + run_entries.append(_listing( + r.get("title") or r["task_id"], f"./{fname}", r.get("status") or "", + )) + written["runs"] += 1 + # attribute the run to a project via its workflow path + wf = r.get("workflow_path") + if wf: + pname = Path(wf).parent.name or Path(wf).stem + _project(pname)["runs"].append(r) + + (out / "schedules").mkdir(parents=True, exist_ok=True) + sched_entries = [] + for t in tasks: + fname = f"{t.id}.md" + (out / "schedules" / fname).write_text(_schedule_doc(t), encoding="utf-8") + sched_entries.append(_listing( + f"{t.agent_name}: {t.schedule_value}", f"./{fname}", + "enabled" if t.enabled else "disabled", + )) + written["schedules"] += 1 + + (out / "agents").mkdir(parents=True, exist_ok=True) + agent_entries = [] + for name, wid in sorted(windows.items()): + md, cwd = _agent_doc(name, wid, runs_by_window) + (out / "agents" / f"{_slug(name)}.md").write_text(md, encoding="utf-8") + agent_entries.append(_listing(name, f"./{_slug(name)}.md", f"window {wid}")) + written["agents"] += 1 + if cwd: + _project(Path(cwd).name, cwd)["agents"].add(name) + + (out / "projects").mkdir(parents=True, exist_ok=True) + proj_entries = [] + for name, data in sorted(projects.items()): + md = _project_doc(name, data["path"], list(data["agents"]), data["runs"]) + (out / "projects" / f"{_slug(name)}.md").write_text(md, encoding="utf-8") + proj_entries.append(_listing(name, f"./{_slug(name)}.md", data["path"])) + written["projects"] += 1 + + # --- reserved files --- + (out / "runs" / "index.md").write_text( + _index("Dispatch Runs", "Work-item dispatcher runs.", run_entries), encoding="utf-8") + (out / "schedules" / "index.md").write_text( + _index("Scheduled Tasks", "Time-based scheduled prompts.", sched_entries), encoding="utf-8") + (out / "agents" / "index.md").write_text( + _index("Agents", "Live agent windows.", agent_entries), encoding="utf-8") + (out / "projects" / "index.md").write_text( + _index("Projects", "Repos the fleet works in.", proj_entries), encoding="utf-8") + + (out / "log.md").write_text(_log_md(runs), encoding="utf-8") + + root_entries = [ + _listing("Agents", "./agents/index.md", f"{written['agents']} live"), + _listing("Dispatch Runs", "./runs/index.md", f"{written['runs']} recorded"), + _listing("Scheduled Tasks", "./schedules/index.md", f"{written['schedules']} tasks"), + _listing("Projects", "./projects/index.md", f"{written['projects']} repos"), + _listing("Activity log", "./log.md", "date-grouped, newest first"), + ] + (out / "index.md").write_text( + _index( + "chela fleet knowledge", + "chela's accumulated fleet knowledge as an OKF v0.1 bundle.", + root_entries, root=True, + ), + encoding="utf-8", + ) + + summary = {"out": str(out), "okf_version": OKF_VERSION, **written, "since": since or ""} + log.info("OKF export → %s (%s)", out, ", ".join(f"{k}={v}" for k, v in written.items())) + return summary + + +# --------------------------------------------------------------------------- +# Reader — the consumer half: parse a bundle back for the viewer. +# +# Pure functions, root in → JSON-able dicts out (no Flask, no chela runtime +# state), so they serve both the embedded dashboard routes and the portable +# viewer's data model, and stay unit-testable against a hand-written bundle. +# +# Per the OKF spec a consumer MUST be liberal: tolerate missing/broken +# frontmatter (treat as no metadata), unknown ``type`` (pass through), broken +# links (render dangling, never raise), and preserve unknown keys. +# --------------------------------------------------------------------------- + +RESERVED = {"index.md", "log.md"} + +_FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n?(.*)\Z", re.DOTALL) +_LINK_RE = re.compile(r"\[([^\]]*)\]\(([^)]+)\)") + + +def parse_doc(text: str) -> tuple[dict, str]: + """Split an OKF markdown file into ``(frontmatter, body)``. + + A file with no / blank / unparseable frontmatter yields ``({}, text)`` — the + consumer treats it as an untyped concept rather than failing (spec: tolerate + missing optionals). + """ + m = _FRONTMATTER_RE.match(text) + if not m: + return {}, text + try: + fm = yaml.safe_load(m.group(1)) + except yaml.YAMLError: + return {}, text + if not isinstance(fm, dict): + return {}, text + return fm, m.group(2) + + +def _title_from_path(rel: str) -> str: + """Derive a display title from a filename when frontmatter has no ``title``.""" + return posixpath.splitext(posixpath.basename(rel))[0] + + +def _is_bundle_md_link(href: str) -> bool: + """True for an in-bundle markdown link (skip http(s)/mailto/anchors/non-md).""" + if not href or href.startswith(("http://", "https://", "mailto:", "#", "//")): + return False + return href.split("#", 1)[0].endswith(".md") + + +def _resolve_link(from_rel: str, href: str) -> str: + """Resolve a markdown link to a bundle-root-relative posix path. + + Handles both relative (``../agents/x.md``) and absolute bundle links + (``/agents/x.md``); strips any ``#anchor``. + """ + href = href.split("#", 1)[0] + if href.startswith("/"): + return href.lstrip("/") + return posixpath.normpath(posixpath.join(posixpath.dirname(from_rel), href)) + + +def _concept_rel_paths(root: Path): + """Yield ``(rel_posix, Path)`` for every non-reserved ``.md`` in the bundle.""" + for p in sorted(root.rglob("*.md")): + rel = p.relative_to(root).as_posix() + if posixpath.basename(rel) in RESERVED: + continue + yield rel, p + + +def _safe_concept_path(root: Path, rel: str) -> Path: + """Resolve ``rel`` under ``root`` for reading, refusing escapes / non-md. + + Path-traversal guard: the dashboard routes pass an untrusted ``?path=``, so a + ``../../etc/passwd`` (or absolute) value must never read outside the bundle. + Raises ``ValueError`` on an unsafe / non-``.md`` path, ``FileNotFoundError`` + when the (safe) target is missing. + """ + rel = (rel or "").lstrip("/") + if not rel.endswith(".md") or "\x00" in rel: + raise ValueError("not a markdown concept path") + root = root.resolve() + target = (root / rel).resolve() + if target != root and root not in target.parents: + raise ValueError("path escapes bundle") + if not target.is_file(): + raise FileNotFoundError(rel) + return target + + +def _summary(rel: str, fm: dict) -> dict: + """A lightweight concept card for tree/search listings (no body). + + Carries a few soft/extension fields (``resource``, ``project``, ``pr_url``) + so the glance feed can render *what an agent is doing* and group agents under + their project without a second per-concept fetch. + """ + return { + "path": rel, + "title": fm.get("title") or _title_from_path(rel), + "type": fm.get("type") or "", + "description": fm.get("description") or "", + "timestamp": fm.get("timestamp") or "", + "tags": fm.get("tags") if isinstance(fm.get("tags"), list) else [], + "resource": fm.get("resource") or "", + "project": fm.get("project") or "", + "pr_url": fm.get("pr_url") or "", + } + + +def read_tree(root: Path) -> dict: + """Browse + glance data: concepts grouped by directory, counts by type, log. + + One call powers both the glance overview (counts / freshest by timestamp / + activity log) and the browse pane (``dirs``). Missing bundle → empty shape + with ``exported: False`` so the viewer can show an export CTA instead of an + error. + """ + root = Path(root) + if not (root / "index.md").exists(): + return {"exported": False, "okf_version": "", "total": 0, + "counts": {}, "dirs": {}, "log": ""} + + root_fm, _ = parse_doc((root / "index.md").read_text(encoding="utf-8")) + dirs: dict[str, list] = {} + counts: dict[str, int] = {} + total = 0 + for rel, p in _concept_rel_paths(root): + fm, _ = parse_doc(p.read_text(encoding="utf-8")) + card = _summary(rel, fm) + dirs.setdefault(posixpath.dirname(rel) or ".", []).append(card) + counts[card["type"] or "(untyped)"] = counts.get(card["type"] or "(untyped)", 0) + 1 + total += 1 + + log_path = root / "log.md" + log_body = log_path.read_text(encoding="utf-8") if log_path.exists() else "" + return { + "exported": True, + "okf_version": root_fm.get("okf_version") or OKF_VERSION, + "total": total, + "counts": counts, + "dirs": dirs, + "log": log_body, + } + + +def read_concept(root: Path, rel: str) -> dict: + """One concept: frontmatter + raw body + outbound links + computed backlinks. + + Backlinks (what links *to* this concept) are the headline feature — they + can't be seen by ``ls``-ing the bundle. Computed by inverting the link graph: + scan every other concept's body for a markdown link that resolves to ``rel``. + Bundle is small; a full scan per open is fine. + """ + root = Path(root) + target = _safe_concept_path(root, rel) + rel = target.relative_to(root.resolve()).as_posix() + fm, body = parse_doc(target.read_text(encoding="utf-8")) + + outbound = [] + for title, href in _LINK_RE.findall(body): + if not _is_bundle_md_link(href): + continue + tgt = _resolve_link(rel, href) + outbound.append({ + "title": title, "path": tgt, + "exists": (root / tgt).is_file(), + }) + + backlinks = [] + for other_rel, p in _concept_rel_paths(root): + if other_rel == rel: + continue + _, obody = parse_doc(p.read_text(encoding="utf-8")) + ofm = None + for title, href in _LINK_RE.findall(obody): + if _is_bundle_md_link(href) and _resolve_link(other_rel, href) == rel: + if ofm is None: + ofm, _ = parse_doc(p.read_text(encoding="utf-8")) + backlinks.append({ + "path": other_rel, + "title": ofm.get("title") or _title_from_path(other_rel), + "type": ofm.get("type") or "", + "context": title, + }) + break # one backlink per source concept + + return { + "path": rel, + "title": fm.get("title") or _title_from_path(rel), + "type": fm.get("type") or "", + "frontmatter": fm, + "body": body, + "outbound": outbound, + "backlinks": backlinks, + } + + +def read_search(root: Path, q: str = "", type_: str = "", tag: str = "") -> list[dict]: + """Full-text-ish search over frontmatter + body, filterable by type / tag. + + Substring match (case-insensitive) across title / description / tags / type / + body — local, no index needed for a bundle this size. Title/description hits + rank above body-only hits. Returns concept cards (+ a body snippet on a body + hit). The embedded viewer may later swap in semantic search via ``mem_index``; + this keeps a zero-dependency baseline shared with the portable viewer. + """ + root = Path(root) + ql = (q or "").strip().lower() + type_l = (type_ or "").strip().lower() + tag_l = (tag or "").strip().lower() + results = [] + for rel, p in _concept_rel_paths(root): + fm, body = parse_doc(p.read_text(encoding="utf-8")) + card = _summary(rel, fm) + if type_l and card["type"].lower() != type_l: + continue + if tag_l and tag_l not in [str(t).lower() for t in card["tags"]]: + continue + meta_blob = " ".join([ + card["title"], card["description"], card["type"], + " ".join(str(t) for t in card["tags"]), + ]).lower() + snippet = "" + rank = -1 + if not ql: + rank = 0 + elif ql in meta_blob: + rank = 2 + elif ql in body.lower(): + rank = 1 + idx = body.lower().index(ql) + start = max(0, idx - 40) + snippet = ("…" if start else "") + body[start:idx + len(ql) + 40].strip().replace("\n", " ") + if rank < 0: + continue + results.append({**card, "rank": rank, "snippet": snippet}) + results.sort(key=lambda r: (-r["rank"], r["title"].lower())) + return results + + +def read_graph(root: Path) -> dict: + """Concept nodes + link edges — the "graph-shaped, not just tree-shaped" view. + + Nodes are concepts; a directed edge ``a → b`` exists when concept ``a``'s body + has a markdown link resolving to concept ``b`` (broken links are dropped — + only edges between two real concepts are emitted). Edges are deduped. + """ + root = Path(root) + nodes = [] + known = set() + for rel, p in _concept_rel_paths(root): + fm, _ = parse_doc(p.read_text(encoding="utf-8")) + nodes.append({ + "id": rel, + "title": fm.get("title") or _title_from_path(rel), + "type": fm.get("type") or "", + }) + known.add(rel) + + edges = [] + seen = set() + for rel, p in _concept_rel_paths(root): + _, body = parse_doc(p.read_text(encoding="utf-8")) + for _title, href in _LINK_RE.findall(body): + if not _is_bundle_md_link(href): + continue + tgt = _resolve_link(rel, href) + if tgt in known and tgt != rel and (rel, tgt) not in seen: + seen.add((rel, tgt)) + edges.append({"source": rel, "target": tgt}) + return {"nodes": nodes, "edges": edges} diff --git a/docs/OKF.md b/docs/OKF.md new file mode 100644 index 0000000..f1de1aa --- /dev/null +++ b/docs/OKF.md @@ -0,0 +1,287 @@ +# Design: OKF Knowledge Layer + Viewer + +**Status:** Design / not yet implemented +**Date:** 2026-06-29 +**Owner:** chela + +A design for exporting chela's accumulated fleet knowledge as a portable +[Open Knowledge Format](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf) +(OKF v0.1) bundle, and — the centerpiece — a **viewer** that lets a human +glance at, browse, search, and navigate that knowledge. + +--- + +## Motivation + +chela already *reads* a lot of fleet knowledge but only renders it +ephemerally in the dashboard: + +- transcript recaps + PR links (`chela/transcripts.py`) +- dispatcher runs (`runs` table, `chela/dispatcher.py`) +- scheduled tasks (`tasks` table, `chela/scheduler.py`) +- per-agent context usage (`~/.chela/context/.json`) +- live agent/window state (`chela/discovery.py`, `chela/agent_manager.py`) + +None of this is persisted in a portable, inspectable shape. When a session +ends, its working knowledge is locked inside a JSONL transcript. The +orchestrator relays context between sessions by hand. There's no way to ask +"what does the fleet collectively know right now?" and *see* the answer. + +OKF gives that knowledge an on-disk shape: a directory of typed markdown +files with YAML frontmatter, vendor-neutral, readable by any tool or human. +But OKF on disk is invisible — a folder of `.md` files. **The value only +materializes with a viewer**, because the two things that make OKF more than +a folder are both latent: + +- it is **graph-shaped, not just tree-shaped** (concepts cross-link via + markdown links → untyped directed edges) +- it is built for **progressive disclosure** (`index.md` per directory) + +Neither shows up when you `ls` the bundle. So this design treats the viewer +as the product and the export as the pipeline that feeds it. + +--- + +## OKF v0.1 in one screen (what we must conform to) + +The conformance bar is deliberately low: + +- **Required:** every non-reserved `.md` file has parseable YAML frontmatter + containing a non-empty **`type`** field. That's it. +- **Recommended (soft):** `title`, `description`, `resource` (a URI for the + underlying asset), `tags` (YAML list), `timestamp` (ISO 8601). +- **Extensions:** producers may add arbitrary keys; consumers MUST preserve + unknown keys and MUST tolerate broken links / unknown types / missing + optional fields. +- **Reserved files:** `index.md` (directory listing, no frontmatter, entries + as `* [Title](url) - description`) and `log.md` (date-grouped history, + newest first, `YYYY-MM-DD` headings). +- **Links:** absolute bundle-relative (`/tables/x.md`) or relative + (`./x.md`); relationship type is conveyed by prose, edges are untyped. +- **Citations:** sources under a `# Citations` heading. +- **Versioning:** declare `okf_version: "0.1"` in the bundle-root `index.md` + frontmatter (the only place frontmatter is allowed in an index). + +Practical consequence: emitting a conformant bundle is essentially "write +frontmatter with a `type` field." We can be liberal in what we emit and the +viewer must be liberal in what it accepts. + +--- + +## Producer: what chela exports + +chela's data model maps almost 1:1 onto OKF typed concepts: + +| chela source | OKF `type:` | file | +|---|---|---| +| `runs` row (`dispatcher.py`) | `Dispatch Run` | `runs/.md` — `resource:` = `pr_url`, `timestamp:` = `ended_at`, body = recap | +| `tasks` row (`scheduler.py`) | `Scheduled Task` | `schedules/.md` | +| agent window (`discovery.py`) | `Agent` | `agents/.md` — CWD, liveness, links → its runs | +| project repo | `Project` | `projects/.md` | +| transcript recap + PR (`transcripts.py`) | folded into Agent/Run body; transcript path under `# Citations` | + +### Bundle layout emitted + +``` +~/.chela/knowledge/ # default; --out to override +├── index.md # okf_version: "0.1" in frontmatter +├── log.md # fleet activity, date-grouped, newest first +├── viewer.html # self-contained portable viewer (see below) +├── agents/ +│ ├── index.md +│ └── researcher.md # links → its runs, its project +├── runs/ +│ ├── index.md +│ └── task-0042.md # resource: +├── schedules/ +│ └── index.md +└── projects/ + └── index.md +``` + +`index.md` files reuse the progressive-disclosure listing the dashboard +already renders (`* [Title](url) - desc`). `log.md` falls out of dispatcher +run history for free (date-grouped, newest first). + +### Where it plugs in + +- **New module `chela/okf.py`** — pure serializer: dataclasses → + markdown+frontmatter, using the existing `pyyaml` dep. No new daemon, no + new external process — consistent with chela's "lean on the DB / tmux" + pattern. Designed to pair with a future reader half in the same module. +- **CLI** — `cmd_knowledge` in `chela/main.py` (subparser block ~L286): + `chela knowledge export [--out DIR] [--since DATE]`. Mirrors how + `dispatch` / `schedule` are wired. +- **Auto-refresh (optional)** — a `WORKFLOW.md` `hooks.after_done` entry + re-exports on PR merge, so the bundle stays live without a cron. + +--- + +## Viewer: the centerpiece + +Two deployment shapes, same conceptual UI. Build the data model once; render +it twice. + +### A. Embedded (live) viewer — in the chela dashboard + +Auto-exports `~/.chela/knowledge/` on first view (Refresh re-exports from current +fleet state). Lives in the existing Flask + vanilla-JS SPA (`chela/dashboard/`). + +- **Flask routes** (read-only, loopback-guarded like the rest of the + dashboard) — thin `jsonify` wrappers over the `okf.read_*` reader half: + - `GET /api/knowledge/tree` — directory + index structure for browse pane + (also carries counts-by-type + `log.md` for the glance overview) + - `GET /api/knowledge/concept?path=...` — one concept: parsed frontmatter + + raw body + outbound links + computed backlinks (path-traversal guarded) + - `GET /api/knowledge/search?q=...&type=...&tag=...` — search results + - `GET /api/knowledge/graph` — nodes (concepts) + edges (links) + - `POST /api/knowledge/export` — force a re-export (the Refresh button) +- **A vanilla-JS view module** (`static/js/knowledge.js`, modeled on + `schedules.js`) — a new "Knowledge" view alongside the agent wall / kanban. + The dashboard SPA is plain classic-script modules + `render_template`, **not** + Lit/HTMX; the markdown-render + link-resolve helpers are kept self-contained so + the portable `viewer.html` can reuse them verbatim. + +### B. Portable viewer — shipped inside the bundle + +A single self-contained `viewer.html` written into the bundle on export. It +reads its sibling `.md` files (via `fetch` when served, or a small inlined +manifest for `file://`) and provides the same browse/search/graph UI with +**zero chela install**. This matches OKF's no-runtime / "just files, +shippable as a tarball" ethos: export a bundle, open `viewer.html` anywhere, +see and search the knowledge. Strong portability + portfolio story. + +> ⚠️ **The portfolio piece is the format + viewer, not a real bundle.** A +> bundle of the actual fleet's knowledge (runs, PR links, agent/project names) +> is **local data and is never published.** Sharing a portable bundle is an +> **opt-in, manual, scrubbed** export — not an exposed endpoint, not a +> committed artifact. See [Security / exposure](#security--exposure). + +> Build order: do the parsing/index/search/graph logic as plain JS that runs +> in both contexts, so A and B share code rather than diverging. + +### The four UI surfaces + +1. **Glance** — "what does the fleet know" overview: counts by `type`, + recent activity from `log.md`, freshest concepts by `timestamp`. The + one-screen answer to "what's in my memory right now." +2. **Browse** — progressive disclosure per OKF intent: root `index.md` → + drill into directories → open a concept rendered as markdown, with a + **frontmatter header card** (type badge, `resource` link, tags, + timestamp) and a **backlinks panel** (what links *to* this concept). + Backlinks are the single thing raw files can never show — highest-value, + cheapest to compute (invert the link graph). +3. **Search** — full-text over frontmatter (`title` / `description` / `tags` + / `type`) + body, filterable by type / tag / date. All local → SQLite FTS + (embedded) or a small in-memory index (portable). For semantic search in + the embedded viewer, **reuse the existing local retrieval layer rather than + reinvent it** (`mem_index.py`: `sqlite-vec` + `fastembed`/`bge-small`, + sha1-delta, no API, nothing leaves the box) — see + [Reuse the local retrieval layer](#reuse-the-local-retrieval-layer). +4. **Graph** — concepts as nodes, markdown links as edges; click a node to + open it. Where "graph-shaped, not just tree-shaped" becomes visible. + +### Consumer robustness (per spec) + +The viewer is an OKF *consumer*, so it MUST: tolerate broken links (render as +dangling, never crash), tolerate unknown `type` values (show the badge as-is), +tolerate missing optional fields (derive `title` from filename), and preserve +unknown frontmatter keys (show them in a raw-fields section). + +--- + +## Security / exposure + +**The OKF *code* is public (MIT); the OKF *bundle* is local data and never is.** +This is the load-bearing boundary — the bundle holds real fleet knowledge +(dispatch runs, PR links, agent/project names) that must not reach the outside +world. + +- **Routes inherit the dashboard's loopback posture for free.** The + `/api/knowledge/*` routes are served by the existing dashboard, which binds + `127.0.0.1` by default and is auth-free *because* it's loopback (the app + refuses to start with terminals enabled on a non-loopback host without an + explicit `TERMINALS_EXPOSE` override). Remote access is **tailnet-only** via + Tailscale (Caddy listens on the `tailscale/chela` interface, not the public + internet), so OKF adds **no new exposure surface**. Do not add a separate + listener; if OKF ever needs its own host/port, it must reuse + `config.is_loopback_host()` and the same guard. +- **The bundle is never committed.** `~/.chela/knowledge/` (and any sample + bundle carrying real data) must be git-ignored; an `--out` pointed inside the + repo is a mistake. The repo ships the serializer + viewer, never an export. +- **The portable `viewer.html` is opt-in, manual, and scrubbed.** Shipping a + bundle is a deliberate human act, not an endpoint and not a CI artifact. + +## Reuse the local retrieval layer + +The home-root memory work (2026-06-30) already built a local semantic-retrieval +layer — `mem_index.py` (`sqlite-vec` + `fastembed`/`bge-small`, 384-dim, +sha1-delta updates, fully on-box, no API). **OKF's embedded-viewer search +should ride on that primitive rather than reinvent it**, scoped to OKF's own +content (a derived, git-ignored `.db` next to the bundle — exactly how OKF +treats the markdown as source-of-truth). + +A complementary lesson from the same work: the per-prompt recall hook injects +**file pointers, not vetted facts** (a tools-denied model confabulated numbers +around them). That is precisely **why OKF earns its keep on top of raw +embeddings** — typed frontmatter + backlinks give the model (and the human) a +*verifiable* landing structure to open and check, instead of trusting a snippet. + +> **Boundary:** reuse the *pattern/primitive*, scoped to OKF content. Do **not** +> wire chela to the home-root recall server or the orchestrator's private +> corpus — cross-querying private memory from a (public-repo) viewer is exactly +> the line the [Security / exposure](#security--exposure) section forbids. + +## Beyond chela's own data (future) + +Because the viewer consumes *any* OKF bundle, it generalizes: + +- Point it at the orchestrator's `~/.claude/.../memory/` (already markdown + + frontmatter + `[[wikilinks]]`) — a near-OKF source. A thin adapter + (`[[name]]` → markdown link, `metadata.type` → `type`) would make the whole + personal memory system browsable/searchable in the same viewer. **Local-only** + per the boundary above — the adapter renders private memory in a *local* + viewer, never a published one. +- Mount external OKF bundles (e.g. Google's `ga4` / `stackoverflow` samples) + to validate the consumer against third-party producers. + +This is the interop payoff: one viewer, any OKF source. + +--- + +## Scope / phasing + +1. **This doc** ✅ — design captured. +2. **MVP export** — `chela/okf.py` serializer + `chela knowledge export` + emitting a conformant bundle from `scheduler.db`. One module, one command, + no new deps. +3. **Embedded viewer** — Flask read-only routes + Knowledge view in the SPA + (glance + browse + backlinks first; search; then graph). +4. **Portable viewer** — `viewer.html` shipped in the bundle, sharing the JS + logic from phase 3. +5. **Adapters** — `~/.claude` memory source; external bundle mount. + +## Open questions + +- Embedded viewer: read a pre-exported bundle, or regenerate live from the DB + each request? (Live = always fresh, no stale export; pre-exported = simpler, + matches the portable path.) +- Search backend: SQLite FTS (we already ship SQLite) vs. in-memory index + shared with the portable viewer. For the *embedded* (semantic) path, prefer + reusing `mem_index.py` (`sqlite-vec` + `fastembed`) over a new index — see + [Reuse the local retrieval layer](#reuse-the-local-retrieval-layer). +- Do we want typed edges eventually? OKF edges are untyped by spec; we could + encode relationship hints in link prose and parse them, staying conformant. + +## Portfolio framing + +"chela exports its fleet's working knowledge as a Google Open Knowledge +Format bundle — vendor-neutral, self-viewing, mountable by any agent or +tool." Standards-aligned, current, and it pre-positions the *agent personas* +roadmap item (a persona is just another OKF `type`). + +The portfolio artifact is the **format + serializer + viewer** (all public, +MIT) — demonstrated against scrubbed or synthetic sample bundles. A real export +of the live fleet's knowledge stays **local** (see +[Security / exposure](#security--exposure)); the story is "look what it can +produce," not a published dump of the fleet's internals. diff --git a/scripts/agent-terminals.sh b/scripts/agent-terminals.sh index e05c642..84a9430 100755 --- a/scripts/agent-terminals.sh +++ b/scripts/agent-terminals.sh @@ -46,13 +46,26 @@ MAP_FILE="${CHELA_DIR:-${HOME}/.chela}/agent_terminals.json" # black wall tiles on first run). mkdir -p "$(dirname "${MAP_FILE}")" -# xterm.js font stack: use whatever Nerd Font the viewer has installed -# (powerline / dev glyphs for lazygit, yazi, starship, eza), else plain -# monospace — safe fallback, no glyphs if no Nerd Font present. +# xterm.js font stack. CSS font matching is PER-GLYPH, so the browser walks this +# whole list for every character. Order matters: +# JetBrainsMono Nerd Font - if the viewer has it installed: Latin + powerline/ +# dev icons (lazygit, yazi, starship, eza) in one font +# JetBrains Mono - BUNDLED @font-face (see app.py _TERM_FONT_CSS): the +# Latin monospace body, guaranteed on any device. This +# MUST come before the Hebrew font, else on a viewer +# without the Nerd Fonts, Latin letters fall through to +# the Hebrew font, changing the English look (Miriam's +# plainer Nimbus Mono Latin instead of JetBrains Mono). +# Symbols Nerd Font - BUNDLED @font-face: icon-only PUA glyphs, for viewers +# without a Nerd Font installed. +# Miriam Mono CLM - BUNDLED @font-face: monospace Hebrew glyphs (no Nerd/ +# Latin font covers Hebrew). Only reached for Hebrew +# codepoints; monospace so it aligns on the grid. +# monospace - final safety net. # No inner quotes: ttyd JSON-parses a value that starts with `"`, which # would truncate the list. Unquoted multi-word family names are valid CSS # (a sequence of space-separated identifiers). -FONT_FAMILY='JetBrainsMono Nerd Font, FiraCode Nerd Font, Hack Nerd Font, Symbols Nerd Font, monospace' +FONT_FAMILY='JetBrainsMono Nerd Font, JetBrains Mono, Symbols Nerd Font, Miriam Mono CLM, monospace' FONT_FAMILY="${CHELA_TERM_FONT:-$FONT_FAMILY}" # xterm.js font size (px). Override with CHELA_TERM_FONTSIZE to fit more columns diff --git a/tests/test_okf.py b/tests/test_okf.py new file mode 100644 index 0000000..4510471 --- /dev/null +++ b/tests/test_okf.py @@ -0,0 +1,243 @@ +"""OKF serializer — lock in conformance (every concept file has a non-empty +`type`; reserved files have no frontmatter; the bundle-root index declares +okf_version) and the producer field mappings. The bundle is exercised against +synthetic state so the test needs no live tmux/DB.""" +import pytest +import yaml + +from chela import discovery, dispatcher, okf, scheduler +from chela.models import ScheduledTask + + +def _meta(md: str) -> dict: + """Parse the YAML frontmatter block of a concept file.""" + assert md.startswith("---"), "concept file must start with frontmatter" + return yaml.safe_load(md.split("---")[1]) + + +def test_run_doc_field_mapping(): + run = { + "task_id": "TODO.md:3", "workflow_path": "/home/x/proj/WORKFLOW.md", + "title": "Add dark mode", "status": "awaiting_review", "window_name": "agent-7", + "worktree_path": "/home/x/wt/dark", "branch_name": "feat/dark", + "started_at": "2026-06-29T10:00:00+00:00", "ended_at": "2026-06-29T12:30:00+00:00", + "attempt": 2, "last_error": None, "pr_url": "https://github.com/x/proj/pull/42", + "pr_state": "open", + } + md = okf._run_doc(run) + m = _meta(md) + assert m["type"] == okf.TYPE_RUN + assert m["resource"] == "https://github.com/x/proj/pull/42" # resource = pr_url + assert m["timestamp"] == "2026-06-29T12:30:00+00:00" # timestamp = ended_at + assert "[agent-7](../agents/agent-7.md)" in md # relative cross-link + assert "`/home/x/wt/dark`" in md # worktree under Citations + + +def test_schedule_doc_field_mapping(): + t = ScheduledTask( + id=5, agent_name="researcher", schedule_type="interval", schedule_value="15m", + prompt="Scan arxiv for new papers\nthen summarize", enabled=True, + last_run=None, next_run="2026-06-30T13:00:00+00:00", + ) + md = okf._schedule_doc(t) + m = _meta(md) + assert m["type"] == okf.TYPE_SCHEDULE + assert m["schedule_value"] == "15m" and m["enabled"] is True + assert "[researcher](../agents/researcher.md)" in md + assert "Scan arxiv for new papers" in md + + +def test_agent_doc_description_is_recap_and_surfaces_pr(monkeypatch): + """An agent concept should read as *what it's doing* (recap) with its project + + latest PR as frontmatter, so a glance card is insight, not boilerplate.""" + monkeypatch.setattr(discovery, "get_window_cwd", lambda name: "/home/x/nautilus") + monkeypatch.setattr(okf.transcripts, "_resolve_agent_transcript", lambda name: "/t.jsonl") + monkeypatch.setattr(okf.transcripts, "latest_recap", + lambda p: "# heading\n\nRefactored the risk engine and opened a PR.\nmore detail") + monkeypatch.setattr(okf.transcripts, "latest_pr", + lambda p: type("PR", (), {"url": "https://github.com/x/p/pull/9"})()) + md, cwd = okf._agent_doc("nautilus", "@11", {}) + m = _meta(md) + assert m["description"] == "Refactored the risk engine and opened a PR." # recap, not "agent window …" + assert m["pr_url"] == "https://github.com/x/p/pull/9" + assert m["project"] == "nautilus" + assert cwd == "/home/x/nautilus" + + +def test_agent_doc_description_falls_back_without_recap(monkeypatch): + monkeypatch.setattr(discovery, "get_window_cwd", lambda name: "/home/x/proj") + monkeypatch.setattr(okf.transcripts, "_resolve_agent_transcript", lambda name: None) + md, _ = okf._agent_doc("shell-1", "@2", {}) + m = _meta(md) + assert m["description"] == "agent window shell-1 @ proj" + assert "pr_url" not in m # empty optional keys are dropped (terse bundle) + + +def test_project_doc_description_rolls_up_counts(): + md = okf._project_doc("nautilus", "/home/x/nautilus", ["a", "b", "a"], [{"task_id": "t"}]) + m = _meta(md) + assert m["description"] == "2 agents · 1 run" # deduped agents, run count + assert m["agent_count"] == 2 and m["run_count"] == 1 + + +def test_log_md_is_date_grouped_newest_first(): + runs = [ + {"task_id": "a", "title": "A", "status": "done", "ended_at": "2026-06-28T09:00:00+00:00"}, + {"task_id": "b", "title": "B", "status": "done", "ended_at": "2026-06-29T09:00:00+00:00"}, + ] + log = okf._log_md(runs) + assert log.index("## 2026-06-29") < log.index("## 2026-06-28") # newest first + + +def test_export_bundle_is_conformant(tmp_path, monkeypatch): + """A full export against synthetic state must produce an OKF-conformant bundle.""" + run = { + "task_id": "TODO.md:1", "workflow_path": "/home/x/proj/WORKFLOW.md", "title": "T", + "status": "done", "window_name": "agent-1", "worktree_path": "", "branch_name": "", + "started_at": "2026-06-29T10:00:00+00:00", "ended_at": "2026-06-29T11:00:00+00:00", + "attempt": 1, "last_error": None, "pr_url": "", "pr_state": "", + } + task = ScheduledTask( + id=1, agent_name="agent-1", schedule_type="cron", schedule_value="0 9 * * *", + prompt="daily", enabled=True, last_run=None, next_run="2026-07-01T09:00:00+00:00", + ) + monkeypatch.setattr(dispatcher, "list_runs", lambda: [run]) + monkeypatch.setattr(scheduler, "list_tasks", lambda: [task]) + monkeypatch.setattr(discovery, "get_all_windows", lambda: {"agent-1": "@1"}) + monkeypatch.setattr(discovery, "get_window_cwd", lambda name: "/home/x/proj") + monkeypatch.setattr(okf.transcripts, "_resolve_agent_transcript", lambda name: None) + + out = tmp_path / "knowledge" + summary = okf.export_bundle(out_dir=out) + assert summary == { + "out": str(out), "okf_version": "0.1", + "runs": 1, "schedules": 1, "agents": 1, "projects": 1, "since": "", + } + + # Bundle-root index declares okf_version; reserved files carry no frontmatter; + # every other .md is a concept file with a non-empty `type`. + for p in sorted(out.rglob("*.md")): + rel = p.relative_to(out).as_posix() + text = p.read_text() + if rel == "index.md": + assert _meta(text)["okf_version"] == "0.1" + elif p.name in ("index.md", "log.md"): + assert not text.startswith("---"), f"{rel} reserved file must have no frontmatter" + else: + assert _meta(text).get("type"), f"{rel} missing non-empty type" + + +def test_within_git_repo_detects_worktree(tmp_path): + (tmp_path / ".git").mkdir() + nested = tmp_path / "a" / "b" + nested.mkdir(parents=True) + assert okf._within_git_repo(nested) == tmp_path + assert okf._within_git_repo(tmp_path.parent) is None + + +# --------------------------------------------------------------------------- +# Reader — the consumer half the viewer reads through. +# --------------------------------------------------------------------------- + +def _mini_bundle(root): + """A tiny but representative bundle: a project linked to from an agent, plus + the reserved index/log files. Mirrors the shape okf.export_bundle emits.""" + (root / "agents").mkdir(parents=True) + (root / "projects").mkdir(parents=True) + (root / "agents" / "nautilus.md").write_text( + "---\ntype: Agent\ntitle: nautilus\ntags: [agent]\n" + "timestamp: '2026-06-30T09:00:00+00:00'\n---\n\n" + "**Project:** [nautilus](../projects/nautilus.md)\n", encoding="utf-8") + (root / "projects" / "nautilus.md").write_text( + "---\ntype: Project\ntitle: nautilus\ntags: [project]\n" + "timestamp: '2026-06-30T08:00:00+00:00'\n---\n\n" + "## Agents\n\n* [nautilus](../agents/nautilus.md)\n", encoding="utf-8") + (root / "agents" / "index.md").write_text("# Agents\n\n* [nautilus](./nautilus.md)\n", encoding="utf-8") + (root / "index.md").write_text( + "---\nokf_version: '0.1'\ntitle: fleet\n---\n\n# fleet\n", encoding="utf-8") + (root / "log.md").write_text("# Activity log\n\n## 2026-06-30\n- did a thing\n", encoding="utf-8") + return root + + +def test_parse_doc_tolerates_missing_and_broken_frontmatter(): + fm, body = okf.parse_doc("no frontmatter here") + assert fm == {} and body == "no frontmatter here" + fm, body = okf.parse_doc("---\ntype: Agent\ntitle: x\n---\nhello\n") + assert fm == {"type": "Agent", "title": "x"} and body.strip() == "hello" + # a non-mapping / unparseable block is treated as no metadata, never raises + fm, _ = okf.parse_doc("---\n: : :\n---\nbody") + assert fm == {} + + +def test_read_tree_counts_and_groups(tmp_path): + root = _mini_bundle(tmp_path) + tree = okf.read_tree(root) + assert tree["exported"] is True + assert tree["okf_version"] == "0.1" + assert tree["total"] == 2 # reserved index/log excluded + assert tree["counts"] == {"Agent": 1, "Project": 1} + assert set(tree["dirs"]) == {"agents", "projects"} + assert "## 2026-06-30" in tree["log"] + + +def test_read_tree_missing_bundle_is_not_an_error(tmp_path): + tree = okf.read_tree(tmp_path / "nope") + assert tree["exported"] is False and tree["total"] == 0 and tree["dirs"] == {} + + +def test_read_concept_computes_backlinks_by_inverting_links(tmp_path): + root = _mini_bundle(tmp_path) + # The agent links OUT to the project; the project must therefore show the + # agent as a BACKLINK (the headline feature — invisible to `ls`). + proj = okf.read_concept(root, "projects/nautilus.md") + assert proj["type"] == "Project" + assert [b["path"] for b in proj["backlinks"]] == ["agents/nautilus.md"] + assert [o["path"] for o in proj["outbound"]] == ["agents/nautilus.md"] + assert proj["frontmatter"]["tags"] == ["project"] # unknown/soft keys preserved + + +def test_read_concept_marks_broken_outbound_links(tmp_path): + root = _mini_bundle(tmp_path) + (root / "agents" / "ghost.md").write_text( + "---\ntype: Agent\ntitle: ghost\n---\n\n[gone](../projects/missing.md)\n", encoding="utf-8") + c = okf.read_concept(root, "agents/ghost.md") + assert c["outbound"] == [{"title": "gone", "path": "projects/missing.md", "exists": False}] + + +def test_read_concept_rejects_path_traversal(tmp_path): + root = _mini_bundle(tmp_path) + (tmp_path.parent / "secret.md").write_text("---\ntype: X\n---\nsecret", encoding="utf-8") + for bad in ["../secret.md", "/etc/passwd", "agents/../../secret.md"]: + with pytest.raises(ValueError): + okf.read_concept(root, bad) + with pytest.raises(ValueError): # non-markdown is refused outright + okf.read_concept(root, "agents/nautilus.txt") + with pytest.raises(FileNotFoundError): + okf.read_concept(root, "agents/absent.md") + + +def test_read_search_ranks_meta_over_body_and_filters(tmp_path): + root = _mini_bundle(tmp_path) + hits = okf.read_search(root, "nautilus") + assert {h["path"] for h in hits} == {"agents/nautilus.md", "projects/nautilus.md"} + # type filter narrows to one concept + only_agents = okf.read_search(root, "", type_="Agent") + assert [h["path"] for h in only_agents] == ["agents/nautilus.md"] + # tag filter likewise + only_proj = okf.read_search(root, "", tag="project") + assert [h["path"] for h in only_proj] == ["projects/nautilus.md"] + + +def test_read_graph_emits_edges_between_real_concepts_only(tmp_path): + root = _mini_bundle(tmp_path) + (root / "agents" / "ghost.md").write_text( + "---\ntype: Agent\ntitle: ghost\n---\n\n[gone](../projects/missing.md)\n", encoding="utf-8") + g = okf.read_graph(root) + assert {n["id"] for n in g["nodes"]} == { + "agents/nautilus.md", "projects/nautilus.md", "agents/ghost.md"} + # ghost's broken link is dropped; only resolvable concept→concept edges remain + pairs = {(e["source"], e["target"]) for e in g["edges"]} + assert pairs == { + ("agents/nautilus.md", "projects/nautilus.md"), + ("projects/nautilus.md", "agents/nautilus.md"), + }