From a7d09e4c67df83331d71c07e9b7ce0aec26eb53c Mon Sep 17 00:00:00 2001
From: Snider
Date: Sat, 21 Feb 2026 19:29:23 +0000
Subject: [PATCH] refactor: migrate cmd/ml to go-ml, remove replaces and
go.work
- Move 40 ML command files to forge.lthn.ai/core/go-ml/cmd
- Remove all replace directives from go.mod
- Remove go.work (repos resolve from forge directly)
- Fix cache.New call to match updated API signature
- Update main.go import to forge.lthn.ai/core/go-ml/cmd
Co-Authored-By: Virgil
---
cmd/ml/chat.js | 832 --------------------------------
cmd/ml/chat_embed.go | 44 --
cmd/ml/cmd_agent.go | 67 ---
cmd/ml/cmd_approve.go | 53 --
cmd/ml/cmd_benchmark.go | 301 ------------
cmd/ml/cmd_benchmark_init.go | 7 -
cmd/ml/cmd_chat.go | 327 -------------
cmd/ml/cmd_chat_init.go | 7 -
cmd/ml/cmd_consolidate.go | 41 --
cmd/ml/cmd_convert.go | 40 --
cmd/ml/cmd_coverage.go | 34 --
cmd/ml/cmd_expand.go | 81 ----
cmd/ml/cmd_expand_status.go | 95 ----
cmd/ml/cmd_export.go | 109 -----
cmd/ml/cmd_gguf.go | 40 --
cmd/ml/cmd_import.go | 58 ---
cmd/ml/cmd_ingest.go | 54 ---
cmd/ml/cmd_inventory.go | 34 --
cmd/ml/cmd_lesson.go | 340 -------------
cmd/ml/cmd_lesson_init.go | 8 -
cmd/ml/cmd_live.go | 82 ----
cmd/ml/cmd_metrics.go | 36 --
cmd/ml/cmd_ml.go | 91 ----
cmd/ml/cmd_normalize.go | 44 --
cmd/ml/cmd_probe.go | 66 ---
cmd/ml/cmd_publish.go | 40 --
cmd/ml/cmd_query.go | 148 ------
cmd/ml/cmd_sandwich.go | 238 ---------
cmd/ml/cmd_sandwich_init.go | 7 -
cmd/ml/cmd_score.go | 77 ---
cmd/ml/cmd_seed_influx.go | 49 --
cmd/ml/cmd_sequence.go | 326 -------------
cmd/ml/cmd_serve.go | 472 ------------------
cmd/ml/cmd_status.go | 54 ---
cmd/ml/cmd_train.go | 358 --------------
cmd/ml/cmd_train_init.go | 7 -
cmd/ml/cmd_worker.go | 80 ---
cmd/ml/serve_backend_default.go | 9 -
cmd/ml/serve_backend_mlx.go | 22 -
cmd/pkgcmd/cmd_search.go | 2 +-
go.mod | 70 +--
go.sum | 106 ++++
go.work | 18 -
main.go | 2 +-
44 files changed, 147 insertions(+), 4829 deletions(-)
delete mode 100644 cmd/ml/chat.js
delete mode 100644 cmd/ml/chat_embed.go
delete mode 100644 cmd/ml/cmd_agent.go
delete mode 100644 cmd/ml/cmd_approve.go
delete mode 100644 cmd/ml/cmd_benchmark.go
delete mode 100644 cmd/ml/cmd_benchmark_init.go
delete mode 100644 cmd/ml/cmd_chat.go
delete mode 100644 cmd/ml/cmd_chat_init.go
delete mode 100644 cmd/ml/cmd_consolidate.go
delete mode 100644 cmd/ml/cmd_convert.go
delete mode 100644 cmd/ml/cmd_coverage.go
delete mode 100644 cmd/ml/cmd_expand.go
delete mode 100644 cmd/ml/cmd_expand_status.go
delete mode 100644 cmd/ml/cmd_export.go
delete mode 100644 cmd/ml/cmd_gguf.go
delete mode 100644 cmd/ml/cmd_import.go
delete mode 100644 cmd/ml/cmd_ingest.go
delete mode 100644 cmd/ml/cmd_inventory.go
delete mode 100644 cmd/ml/cmd_lesson.go
delete mode 100644 cmd/ml/cmd_lesson_init.go
delete mode 100644 cmd/ml/cmd_live.go
delete mode 100644 cmd/ml/cmd_metrics.go
delete mode 100644 cmd/ml/cmd_ml.go
delete mode 100644 cmd/ml/cmd_normalize.go
delete mode 100644 cmd/ml/cmd_probe.go
delete mode 100644 cmd/ml/cmd_publish.go
delete mode 100644 cmd/ml/cmd_query.go
delete mode 100644 cmd/ml/cmd_sandwich.go
delete mode 100644 cmd/ml/cmd_sandwich_init.go
delete mode 100644 cmd/ml/cmd_score.go
delete mode 100644 cmd/ml/cmd_seed_influx.go
delete mode 100644 cmd/ml/cmd_sequence.go
delete mode 100644 cmd/ml/cmd_serve.go
delete mode 100644 cmd/ml/cmd_status.go
delete mode 100644 cmd/ml/cmd_train.go
delete mode 100644 cmd/ml/cmd_train_init.go
delete mode 100644 cmd/ml/cmd_worker.go
delete mode 100644 cmd/ml/serve_backend_default.go
delete mode 100644 cmd/ml/serve_backend_mlx.go
delete mode 100644 go.work
diff --git a/cmd/ml/chat.js b/cmd/ml/chat.js
deleted file mode 100644
index 8948708c..00000000
--- a/cmd/ml/chat.js
+++ /dev/null
@@ -1,832 +0,0 @@
-// src/styles.ts
-var chatStyles = `
- :host {
- display: flex;
- flex-direction: column;
- background: var(--lem-bg, #1a1a1e);
- color: var(--lem-text, #e0e0e0);
- font-family: var(--lem-font, system-ui, -apple-system, sans-serif);
- font-size: 14px;
- line-height: 1.5;
- border-radius: 12px;
- overflow: hidden;
- border: 1px solid rgba(255, 255, 255, 0.08);
- }
-
- .header {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 14px 18px;
- background: rgba(255, 255, 255, 0.03);
- border-bottom: 1px solid rgba(255, 255, 255, 0.06);
- flex-shrink: 0;
- }
-
- .header-icon {
- width: 28px;
- height: 28px;
- border-radius: 8px;
- background: var(--lem-accent, #5865f2);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 14px;
- font-weight: 700;
- color: #fff;
- }
-
- .header-title {
- font-size: 15px;
- font-weight: 600;
- color: var(--lem-text, #e0e0e0);
- }
-
- .header-model {
- font-size: 11px;
- color: rgba(255, 255, 255, 0.35);
- margin-left: auto;
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- }
-
- .header-status {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #43b581;
- flex-shrink: 0;
- }
-
- .header-status.disconnected {
- background: #f04747;
- }
-`;
-var messagesStyles = `
- :host {
- display: block;
- flex: 1;
- overflow-y: auto;
- overflow-x: hidden;
- padding: 16px 0;
- scroll-behavior: smooth;
- }
-
- :host::-webkit-scrollbar {
- width: 6px;
- }
-
- :host::-webkit-scrollbar-track {
- background: transparent;
- }
-
- :host::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.12);
- border-radius: 3px;
- }
-
- .empty {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- gap: 12px;
- color: rgba(255, 255, 255, 0.25);
- }
-
- .empty-icon {
- font-size: 36px;
- opacity: 0.4;
- }
-
- .empty-text {
- font-size: 14px;
- }
-`;
-var messageStyles = `
- :host {
- display: block;
- padding: 6px 18px;
- }
-
- :host([role="user"]) .bubble {
- background: var(--lem-msg-user, #2a2a3e);
- margin-left: 40px;
- border-radius: 12px 12px 4px 12px;
- }
-
- :host([role="assistant"]) .bubble {
- background: var(--lem-msg-assistant, #1e1e2a);
- margin-right: 40px;
- border-radius: 12px 12px 12px 4px;
- }
-
- .bubble {
- padding: 10px 14px;
- word-wrap: break-word;
- overflow-wrap: break-word;
- }
-
- .role {
- font-size: 11px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- margin-bottom: 4px;
- color: rgba(255, 255, 255, 0.35);
- }
-
- :host([role="assistant"]) .role {
- color: var(--lem-accent, #5865f2);
- }
-
- .content {
- color: var(--lem-text, #e0e0e0);
- line-height: 1.6;
- }
-
- .content p {
- margin: 0 0 8px 0;
- }
-
- .content p:last-child {
- margin-bottom: 0;
- }
-
- .content strong {
- font-weight: 600;
- color: #fff;
- }
-
- .content em {
- font-style: italic;
- color: rgba(255, 255, 255, 0.8);
- }
-
- .content code {
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 12px;
- background: rgba(0, 0, 0, 0.3);
- padding: 2px 5px;
- border-radius: 4px;
- color: #e8a0bf;
- }
-
- .content pre {
- margin: 8px 0;
- padding: 12px;
- background: rgba(0, 0, 0, 0.35);
- border-radius: 8px;
- overflow-x: auto;
- border: 1px solid rgba(255, 255, 255, 0.06);
- }
-
- .content pre code {
- background: none;
- padding: 0;
- font-size: 12px;
- color: #c9d1d9;
- line-height: 1.5;
- }
-
- .think-panel {
- margin: 6px 0 8px;
- padding: 8px 12px;
- background: rgba(88, 101, 242, 0.06);
- border-left: 2px solid rgba(88, 101, 242, 0.3);
- border-radius: 0 6px 6px 0;
- font-size: 12px;
- color: rgba(255, 255, 255, 0.45);
- line-height: 1.5;
- max-height: 200px;
- overflow-y: auto;
- }
-
- .think-panel::-webkit-scrollbar {
- width: 4px;
- }
-
- .think-panel::-webkit-scrollbar-thumb {
- background: rgba(255, 255, 255, 0.1);
- border-radius: 2px;
- }
-
- .think-label {
- font-size: 10px;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(88, 101, 242, 0.5);
- margin-bottom: 4px;
- cursor: pointer;
- user-select: none;
- }
-
- .think-label:hover {
- color: rgba(88, 101, 242, 0.7);
- }
-
- .think-panel.collapsed .think-content {
- display: none;
- }
-
- .cursor {
- display: inline-block;
- width: 7px;
- height: 16px;
- background: var(--lem-accent, #5865f2);
- border-radius: 1px;
- animation: blink 0.8s step-end infinite;
- vertical-align: text-bottom;
- margin-left: 2px;
- }
-
- @keyframes blink {
- 50% { opacity: 0; }
- }
-`;
-var inputStyles = `
- :host {
- display: block;
- padding: 12px 16px 16px;
- border-top: 1px solid rgba(255, 255, 255, 0.06);
- flex-shrink: 0;
- }
-
- .input-wrapper {
- display: flex;
- align-items: flex-end;
- gap: 10px;
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 12px;
- padding: 8px 12px;
- transition: border-color 0.15s;
- }
-
- .input-wrapper:focus-within {
- border-color: var(--lem-accent, #5865f2);
- }
-
- textarea {
- flex: 1;
- background: none;
- border: none;
- outline: none;
- color: var(--lem-text, #e0e0e0);
- font-family: inherit;
- font-size: 14px;
- line-height: 1.5;
- resize: none;
- max-height: 120px;
- min-height: 22px;
- padding: 0;
- }
-
- textarea::placeholder {
- color: rgba(255, 255, 255, 0.25);
- }
-
- .send-btn {
- background: var(--lem-accent, #5865f2);
- border: none;
- border-radius: 8px;
- color: #fff;
- width: 32px;
- height: 32px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- transition: opacity 0.15s, transform 0.1s;
- }
-
- .send-btn:hover {
- opacity: 0.85;
- }
-
- .send-btn:active {
- transform: scale(0.95);
- }
-
- .send-btn:disabled {
- opacity: 0.3;
- cursor: default;
- transform: none;
- }
-
- .send-btn svg {
- width: 16px;
- height: 16px;
- }
-`;
-
-// src/lem-messages.ts
-var LemMessages = class extends HTMLElement {
- shadow;
- container;
- emptyEl;
- shouldAutoScroll = true;
- constructor() {
- super();
- this.shadow = this.attachShadow({ mode: "open" });
- }
- connectedCallback() {
- const style = document.createElement("style");
- style.textContent = messagesStyles;
- this.container = document.createElement("div");
- this.emptyEl = document.createElement("div");
- this.emptyEl.className = "empty";
- const emptyIcon = document.createElement("div");
- emptyIcon.className = "empty-icon";
- emptyIcon.textContent = "\u2728";
- const emptyText = document.createElement("div");
- emptyText.className = "empty-text";
- emptyText.textContent = "Start a conversation";
- this.emptyEl.appendChild(emptyIcon);
- this.emptyEl.appendChild(emptyText);
- this.shadow.appendChild(style);
- this.shadow.appendChild(this.emptyEl);
- this.shadow.appendChild(this.container);
- this.addEventListener("scroll", () => {
- const threshold = 60;
- this.shouldAutoScroll = this.scrollHeight - this.scrollTop - this.clientHeight < threshold;
- });
- }
- addMessage(role, text) {
- this.emptyEl.style.display = "none";
- const msg = document.createElement("lem-message");
- msg.setAttribute("role", role);
- this.container.appendChild(msg);
- if (text) {
- msg.text = text;
- }
- this.scrollToBottom();
- return msg;
- }
- scrollToBottom() {
- if (this.shouldAutoScroll) {
- requestAnimationFrame(() => {
- this.scrollTop = this.scrollHeight;
- });
- }
- }
- clear() {
- this.container.replaceChildren();
- this.emptyEl.style.display = "";
- this.shouldAutoScroll = true;
- }
-};
-customElements.define("lem-messages", LemMessages);
-
-// src/markdown.ts
-function escapeHtml(text) {
- return text.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
-}
-function parseInline(text) {
- let result = escapeHtml(text);
- result = result.replace(/`([^`]+)`/g, "$1");
- result = result.replace(/\*\*(.+?)\*\*/g, "$1");
- result = result.replace(/__(.+?)__/g, "$1");
- result = result.replace(/(?$1");
- result = result.replace(/(?$1");
- return result;
-}
-function renderMarkdown(text) {
- const lines = text.split("\n");
- const output = [];
- let inCodeBlock = false;
- let codeLines = [];
- let codeLang = "";
- for (const line of lines) {
- if (line.trimStart().startsWith("```")) {
- if (!inCodeBlock) {
- inCodeBlock = true;
- codeLang = line.trimStart().slice(3).trim();
- codeLines = [];
- } else {
- const langAttr = codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : "";
- output.push(
- `${escapeHtml(codeLines.join("\n"))}
`
- );
- inCodeBlock = false;
- codeLines = [];
- codeLang = "";
- }
- continue;
- }
- if (inCodeBlock) {
- codeLines.push(line);
- continue;
- }
- if (line.trim() === "") {
- output.push("");
- continue;
- }
- output.push(parseInline(line));
- }
- if (inCodeBlock) {
- const langAttr = codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : "";
- output.push(
- `${escapeHtml(codeLines.join("\n"))}
`
- );
- }
- const paragraphs = [];
- let current = [];
- for (const line of output) {
- if (line === "") {
- if (current.length > 0) {
- paragraphs.push(wrapParagraph(current));
- current = [];
- }
- } else {
- current.push(line);
- }
- }
- if (current.length > 0) {
- paragraphs.push(wrapParagraph(current));
- }
- return paragraphs.join("");
-}
-function wrapParagraph(lines) {
- const joined = lines.join("
");
- if (joined.startsWith("${joined}
`;
-}
-
-// src/lem-message.ts
-var LemMessage = class extends HTMLElement {
- shadow;
- thinkPanel;
- thinkContent;
- thinkLabel;
- contentEl;
- cursorEl;
- _text = "";
- _streaming = false;
- _thinkCollapsed = false;
- constructor() {
- super();
- this.shadow = this.attachShadow({ mode: "open" });
- }
- connectedCallback() {
- const role = this.getAttribute("role") || "user";
- const style = document.createElement("style");
- style.textContent = messageStyles;
- const bubble = document.createElement("div");
- bubble.className = "bubble";
- const roleEl = document.createElement("div");
- roleEl.className = "role";
- roleEl.textContent = role === "assistant" ? "LEM" : "You";
- this.thinkPanel = document.createElement("div");
- this.thinkPanel.className = "think-panel";
- this.thinkPanel.style.display = "none";
- this.thinkLabel = document.createElement("div");
- this.thinkLabel.className = "think-label";
- this.thinkLabel.textContent = "\u25BC reasoning";
- this.thinkLabel.addEventListener("click", () => {
- this._thinkCollapsed = !this._thinkCollapsed;
- this.thinkPanel.classList.toggle("collapsed", this._thinkCollapsed);
- this.thinkLabel.textContent = this._thinkCollapsed ? "\u25B6 reasoning" : "\u25BC reasoning";
- });
- this.thinkContent = document.createElement("div");
- this.thinkContent.className = "think-content";
- this.thinkPanel.appendChild(this.thinkLabel);
- this.thinkPanel.appendChild(this.thinkContent);
- this.contentEl = document.createElement("div");
- this.contentEl.className = "content";
- bubble.appendChild(roleEl);
- if (role === "assistant") {
- bubble.appendChild(this.thinkPanel);
- }
- bubble.appendChild(this.contentEl);
- this.shadow.appendChild(style);
- this.shadow.appendChild(bubble);
- if (this._text) {
- this.render();
- }
- }
- get text() {
- return this._text;
- }
- set text(value) {
- this._text = value;
- this.render();
- }
- get streaming() {
- return this._streaming;
- }
- set streaming(value) {
- this._streaming = value;
- this.render();
- }
- appendToken(token) {
- this._text += token;
- this.render();
- }
- /**
- * Splits text into think/response portions and renders each.
- *
- * Safety: renderMarkdown() escapes all HTML entities (& < > ") before any
- * inline formatting is applied. The source is the local MLX model output,
- * not arbitrary user HTML. Shadow DOM provides additional isolation.
- */
- render() {
- if (!this.contentEl) return;
- const { think, response } = this.splitThink(this._text);
- if (think !== null && this.thinkPanel) {
- this.thinkPanel.style.display = "";
- this.thinkContent.textContent = think;
- }
- const responseHtml = renderMarkdown(response);
- this.contentEl.innerHTML = responseHtml;
- if (this._streaming) {
- if (!this.cursorEl) {
- this.cursorEl = document.createElement("span");
- this.cursorEl.className = "cursor";
- }
- if (think !== null && !this._text.includes("")) {
- this.thinkContent.appendChild(this.cursorEl);
- } else {
- const lastChild = this.contentEl.lastElementChild || this.contentEl;
- lastChild.appendChild(this.cursorEl);
- }
- }
- }
- /**
- * Split raw text into think content and response content.
- * Returns { think: string | null, response: string }
- */
- splitThink(text) {
- const thinkStart = text.indexOf("");
- if (thinkStart === -1) {
- return { think: null, response: text };
- }
- const afterOpen = thinkStart + "".length;
- const thinkEnd = text.indexOf("", afterOpen);
- if (thinkEnd === -1) {
- return {
- think: text.slice(afterOpen).trim(),
- response: text.slice(0, thinkStart).trim()
- };
- }
- const thinkText = text.slice(afterOpen, thinkEnd).trim();
- const beforeThink = text.slice(0, thinkStart).trim();
- const afterThink = text.slice(thinkEnd + "".length).trim();
- const response = [beforeThink, afterThink].filter(Boolean).join("\n");
- return { think: thinkText, response };
- }
-};
-customElements.define("lem-message", LemMessage);
-
-// src/lem-input.ts
-var LemInput = class extends HTMLElement {
- shadow;
- textarea;
- sendBtn;
- _disabled = false;
- constructor() {
- super();
- this.shadow = this.attachShadow({ mode: "open" });
- }
- connectedCallback() {
- const style = document.createElement("style");
- style.textContent = inputStyles;
- const wrapper = document.createElement("div");
- wrapper.className = "input-wrapper";
- this.textarea = document.createElement("textarea");
- this.textarea.rows = 1;
- this.textarea.placeholder = "Message LEM...";
- this.sendBtn = document.createElement("button");
- this.sendBtn.className = "send-btn";
- this.sendBtn.type = "button";
- this.sendBtn.disabled = true;
- this.sendBtn.appendChild(this.createSendIcon());
- wrapper.appendChild(this.textarea);
- wrapper.appendChild(this.sendBtn);
- this.shadow.appendChild(style);
- this.shadow.appendChild(wrapper);
- this.textarea.addEventListener("input", () => {
- this.textarea.style.height = "auto";
- this.textarea.style.height = Math.min(this.textarea.scrollHeight, 120) + "px";
- this.sendBtn.disabled = this._disabled || this.textarea.value.trim() === "";
- });
- this.textarea.addEventListener("keydown", (e) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- this.submit();
- }
- });
- this.sendBtn.addEventListener("click", () => this.submit());
- }
- /** Build the send arrow SVG using DOM API (no innerHTML) */
- createSendIcon() {
- const ns = "http://www.w3.org/2000/svg";
- const svg = document.createElementNS(ns, "svg");
- svg.setAttribute("viewBox", "0 0 24 24");
- svg.setAttribute("fill", "none");
- svg.setAttribute("stroke", "currentColor");
- svg.setAttribute("stroke-width", "2");
- svg.setAttribute("stroke-linecap", "round");
- svg.setAttribute("stroke-linejoin", "round");
- svg.setAttribute("width", "16");
- svg.setAttribute("height", "16");
- const line = document.createElementNS(ns, "line");
- line.setAttribute("x1", "22");
- line.setAttribute("y1", "2");
- line.setAttribute("x2", "11");
- line.setAttribute("y2", "13");
- const polygon = document.createElementNS(ns, "polygon");
- polygon.setAttribute("points", "22 2 15 22 11 13 2 9 22 2");
- svg.appendChild(line);
- svg.appendChild(polygon);
- return svg;
- }
- submit() {
- const text = this.textarea.value.trim();
- if (!text || this._disabled) return;
- this.dispatchEvent(
- new CustomEvent("lem-send", {
- bubbles: true,
- composed: true,
- detail: { text }
- })
- );
- this.textarea.value = "";
- this.textarea.style.height = "auto";
- this.sendBtn.disabled = true;
- this.textarea.focus();
- }
- get disabled() {
- return this._disabled;
- }
- set disabled(value) {
- this._disabled = value;
- this.textarea.disabled = value;
- this.sendBtn.disabled = value || this.textarea.value.trim() === "";
- this.textarea.placeholder = value ? "LEM is thinking..." : "Message LEM...";
- }
- focus() {
- this.textarea?.focus();
- }
-};
-customElements.define("lem-input", LemInput);
-
-// src/lem-chat.ts
-var LemChat = class extends HTMLElement {
- shadow;
- messages;
- input;
- statusEl;
- history = [];
- abortController = null;
- static get observedAttributes() {
- return ["endpoint", "model", "system-prompt", "max-tokens", "temperature"];
- }
- constructor() {
- super();
- this.shadow = this.attachShadow({ mode: "open" });
- }
- connectedCallback() {
- const style = document.createElement("style");
- style.textContent = chatStyles;
- const header = document.createElement("div");
- header.className = "header";
- this.statusEl = document.createElement("div");
- this.statusEl.className = "header-status";
- const icon = document.createElement("div");
- icon.className = "header-icon";
- icon.textContent = "L";
- const title = document.createElement("div");
- title.className = "header-title";
- title.textContent = "LEM";
- const modelLabel = document.createElement("div");
- modelLabel.className = "header-model";
- modelLabel.textContent = this.getAttribute("model") || "local";
- header.appendChild(this.statusEl);
- header.appendChild(icon);
- header.appendChild(title);
- header.appendChild(modelLabel);
- this.messages = document.createElement("lem-messages");
- this.input = document.createElement("lem-input");
- this.shadow.appendChild(style);
- this.shadow.appendChild(header);
- this.shadow.appendChild(this.messages);
- this.shadow.appendChild(this.input);
- this.addEventListener("lem-send", ((e) => {
- this.handleSend(e.detail.text);
- }));
- const systemPrompt = this.getAttribute("system-prompt");
- if (systemPrompt) {
- this.history.push({ role: "system", content: systemPrompt });
- }
- this.checkConnection();
- requestAnimationFrame(() => this.input.focus());
- }
- disconnectedCallback() {
- this.abortController?.abort();
- }
- get endpoint() {
- const attr = this.getAttribute("endpoint");
- if (!attr) return window.location.origin;
- return attr;
- }
- get model() {
- return this.getAttribute("model") || "";
- }
- get maxTokens() {
- const val = this.getAttribute("max-tokens");
- return val ? parseInt(val, 10) : 2048;
- }
- get temperature() {
- const val = this.getAttribute("temperature");
- return val ? parseFloat(val) : 0.7;
- }
- async checkConnection() {
- try {
- const resp = await fetch(`${this.endpoint}/v1/models`, {
- signal: AbortSignal.timeout(3e3)
- });
- this.statusEl.classList.toggle("disconnected", !resp.ok);
- } catch {
- this.statusEl.classList.add("disconnected");
- }
- }
- async handleSend(text) {
- this.messages.addMessage("user", text);
- this.history.push({ role: "user", content: text });
- const assistantMsg = this.messages.addMessage("assistant");
- assistantMsg.streaming = true;
- this.input.disabled = true;
- this.abortController?.abort();
- this.abortController = new AbortController();
- let fullResponse = "";
- try {
- const response = await fetch(`${this.endpoint}/v1/chat/completions`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- signal: this.abortController.signal,
- body: JSON.stringify({
- model: this.model,
- messages: this.history,
- max_tokens: this.maxTokens,
- temperature: this.temperature,
- stream: true
- })
- });
- if (!response.ok) {
- throw new Error(`Server error: ${response.status}`);
- }
- if (!response.body) {
- throw new Error("No response body");
- }
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let buffer = "";
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split("\n");
- buffer = lines.pop() || "";
- for (const line of lines) {
- if (!line.startsWith("data: ")) continue;
- const data = line.slice(6).trim();
- if (data === "[DONE]") continue;
- try {
- const chunk = JSON.parse(data);
- const delta = chunk.choices?.[0]?.delta;
- if (delta?.content) {
- fullResponse += delta.content;
- assistantMsg.appendToken(delta.content);
- this.messages.scrollToBottom();
- }
- } catch {
- }
- }
- }
- } catch (err) {
- if (err instanceof Error && err.name === "AbortError") {
- } else {
- const errorText = err instanceof Error ? err.message : "Connection failed";
- if (!fullResponse) {
- assistantMsg.text = `\u26A0\uFE0F ${errorText}`;
- }
- this.statusEl.classList.add("disconnected");
- }
- } finally {
- assistantMsg.streaming = false;
- this.input.disabled = false;
- this.input.focus();
- this.abortController = null;
- if (fullResponse) {
- this.history.push({ role: "assistant", content: fullResponse });
- }
- }
- }
-};
-customElements.define("lem-chat", LemChat);
-export {
- LemChat
-};
diff --git a/cmd/ml/chat_embed.go b/cmd/ml/chat_embed.go
deleted file mode 100644
index 447d7a95..00000000
--- a/cmd/ml/chat_embed.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package ml
-
-import (
- _ "embed"
-)
-
-//go:embed chat.js
-var lemChatJS []byte
-
-const chatHTML = `
-
-
-
-
- LEM Chat
-
-
-
-
-
-
-`
diff --git a/cmd/ml/cmd_agent.go b/cmd/ml/cmd_agent.go
deleted file mode 100644
index 21fa678e..00000000
--- a/cmd/ml/cmd_agent.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package ml
-
-import (
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- agentM3Host string
- agentM3User string
- agentM3SSHKey string
- agentM3AdapterBase string
- agentBaseModel string
- agentPollInterval int
- agentWorkDir string
- agentFilter string
- agentForce bool
- agentOneShot bool
- agentDryRun bool
-)
-
-var agentCmd = &cli.Command{
- Use: "agent",
- Short: "Run the scoring agent daemon",
- Long: "Polls M3 for unscored LoRA checkpoints, converts, probes, and pushes results to InfluxDB.",
- RunE: runAgent,
-}
-
-func init() {
- agentCmd.Flags().StringVar(&agentM3Host, "m3-host", ml.EnvOr("M3_HOST", "10.69.69.108"), "M3 host address")
- agentCmd.Flags().StringVar(&agentM3User, "m3-user", ml.EnvOr("M3_USER", "claude"), "M3 SSH user")
- agentCmd.Flags().StringVar(&agentM3SSHKey, "m3-ssh-key", ml.EnvOr("M3_SSH_KEY", ml.ExpandHome("~/.ssh/id_ed25519")), "SSH key for M3")
- agentCmd.Flags().StringVar(&agentM3AdapterBase, "m3-adapter-base", ml.EnvOr("M3_ADAPTER_BASE", "/Volumes/Data/lem"), "Adapter base dir on M3")
- agentCmd.Flags().StringVar(&agentBaseModel, "base-model", ml.EnvOr("BASE_MODEL", "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"), "HuggingFace base model ID")
- agentCmd.Flags().IntVar(&agentPollInterval, "poll", ml.IntEnvOr("POLL_INTERVAL", 300), "Poll interval in seconds")
- agentCmd.Flags().StringVar(&agentWorkDir, "work-dir", ml.EnvOr("WORK_DIR", "/tmp/scoring-agent"), "Working directory for adapters")
- agentCmd.Flags().StringVar(&agentFilter, "filter", "", "Filter adapter dirs by prefix")
- agentCmd.Flags().BoolVar(&agentForce, "force", false, "Re-score already-scored checkpoints")
- agentCmd.Flags().BoolVar(&agentOneShot, "one-shot", false, "Process one checkpoint and exit")
- agentCmd.Flags().BoolVar(&agentDryRun, "dry-run", false, "Discover and plan but don't execute")
-}
-
-func runAgent(cmd *cli.Command, args []string) error {
- cfg := &ml.AgentConfig{
- M3Host: agentM3Host,
- M3User: agentM3User,
- M3SSHKey: agentM3SSHKey,
- M3AdapterBase: agentM3AdapterBase,
- InfluxURL: influxURL,
- InfluxDB: influxDB,
- DBPath: dbPath,
- APIURL: apiURL,
- JudgeURL: judgeURL,
- JudgeModel: judgeModel,
- Model: modelName,
- BaseModel: agentBaseModel,
- PollInterval: agentPollInterval,
- WorkDir: agentWorkDir,
- Filter: agentFilter,
- Force: agentForce,
- OneShot: agentOneShot,
- DryRun: agentDryRun,
- }
-
- ml.RunAgentLoop(cfg)
- return nil
-}
diff --git a/cmd/ml/cmd_approve.go b/cmd/ml/cmd_approve.go
deleted file mode 100644
index a7212df0..00000000
--- a/cmd/ml/cmd_approve.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
- "path/filepath"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- approveOutput string
- approveThreshold float64
-)
-
-var approveCmd = &cli.Command{
- Use: "approve",
- Short: "Filter scored expansions into training JSONL",
- Long: "Filters scored expansion responses by quality threshold and exports approved ones as chat-format training JSONL.",
- RunE: runApprove,
-}
-
-func init() {
- approveCmd.Flags().StringVar(&approveOutput, "output", "", "Output JSONL file (defaults to expansion-approved.jsonl in db dir)")
- approveCmd.Flags().Float64Var(&approveThreshold, "threshold", 6.0, "Min judge average to approve")
-}
-
-func runApprove(cmd *cli.Command, args []string) error {
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB required")
- }
-
- output := approveOutput
- if output == "" {
- output = filepath.Join(filepath.Dir(path), "expansion-approved.jsonl")
- }
-
- db, err := ml.OpenDB(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- return ml.ApproveExpansions(db, ml.ApproveConfig{
- Output: output,
- Threshold: approveThreshold,
- }, cmd.OutOrStdout())
-}
diff --git a/cmd/ml/cmd_benchmark.go b/cmd/ml/cmd_benchmark.go
deleted file mode 100644
index 7290354a..00000000
--- a/cmd/ml/cmd_benchmark.go
+++ /dev/null
@@ -1,301 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "log/slog"
- "os"
- "runtime"
- "sort"
- "time"
-
- "forge.lthn.ai/core/go-ml"
- "forge.lthn.ai/core/go/pkg/cli"
-)
-
-var benchmarkCmd = &cli.Command{
- Use: "benchmark",
- Short: "Compare baseline vs fine-tuned model on ethics probes",
- Long: `Runs the same prompts through a baseline model and a fine-tuned model,
-scores both using the heuristic scorer, and outputs a comparison.
-
-Uses the built-in LEK content probes by default. Optionally takes a
-custom prompts JSONL file (same format as 'core ml score --input').
-
-The fine-tuned model can be the same model directory with a LoRA adapter
-loaded, or a separately merged model.`,
- RunE: runBenchmark,
-}
-
-var (
- benchmarkBaseline string
- benchmarkTrained string
- benchmarkPrompts string
- benchmarkOutput string
- benchmarkMaxTokens int
- benchmarkTemp float64
- benchmarkMemLimit int
-)
-
-func init() {
- benchmarkCmd.Flags().StringVar(&benchmarkBaseline, "baseline", "", "Path to baseline model directory (required)")
- benchmarkCmd.Flags().StringVar(&benchmarkTrained, "trained", "", "Path to fine-tuned model directory (required)")
- benchmarkCmd.Flags().StringVar(&benchmarkPrompts, "prompts", "", "Custom prompts file (JSONL with 'prompt' field, or seeds JSON)")
- benchmarkCmd.Flags().StringVar(&benchmarkOutput, "output", "benchmark.json", "Output comparison JSON file")
- benchmarkCmd.Flags().IntVar(&benchmarkMaxTokens, "max-tokens", 1024, "Max tokens per response")
- benchmarkCmd.Flags().Float64Var(&benchmarkTemp, "temperature", 0.4, "Sampling temperature")
- benchmarkCmd.Flags().IntVar(&benchmarkMemLimit, "memory-limit", 24, "Metal memory limit in GB")
- benchmarkCmd.MarkFlagRequired("baseline")
- benchmarkCmd.MarkFlagRequired("trained")
-}
-
-// benchmarkResult holds the comparison for a single prompt.
-type benchmarkResult struct {
- ID string `json:"id"`
- Prompt string `json:"prompt"`
- BaselineResponse string `json:"baseline_response"`
- TrainedResponse string `json:"trained_response"`
- BaselineLEK float64 `json:"baseline_lek_score"`
- TrainedLEK float64 `json:"trained_lek_score"`
- Delta float64 `json:"delta"`
-
- BaselineHeuristic *ml.HeuristicScores `json:"baseline_heuristic"`
- TrainedHeuristic *ml.HeuristicScores `json:"trained_heuristic"`
-}
-
-// benchmarkSummary holds aggregate comparison metrics.
-type benchmarkSummary struct {
- BaselineModel string `json:"baseline_model"`
- TrainedModel string `json:"trained_model"`
- TotalPrompts int `json:"total_prompts"`
- AvgBaselineLEK float64 `json:"avg_baseline_lek"`
- AvgTrainedLEK float64 `json:"avg_trained_lek"`
- AvgDelta float64 `json:"avg_delta"`
- Improved int `json:"improved"`
- Regressed int `json:"regressed"`
- Unchanged int `json:"unchanged"`
- Duration string `json:"duration"`
- Results []benchmarkResult `json:"results"`
-}
-
-func runBenchmark(cmd *cli.Command, args []string) error {
- start := time.Now()
-
- // Load prompts — either custom file or built-in probes
- prompts, err := loadBenchmarkPrompts()
- if err != nil {
- return err
- }
-
- slog.Info("benchmark: loaded prompts", "count", len(prompts))
-
- opts := ml.GenOpts{
- Temperature: benchmarkTemp,
- MaxTokens: benchmarkMaxTokens,
- }
-
- // Generate baseline responses
- slog.Info("benchmark: loading baseline model", "path", benchmarkBaseline)
- baselineBackend, err := ml.NewMLXBackend(benchmarkBaseline)
- if err != nil {
- return fmt.Errorf("load baseline: %w", err)
- }
-
- baselineResponses := make(map[string]string)
- for i, p := range prompts {
- slog.Info("benchmark: baseline",
- "prompt", fmt.Sprintf("%d/%d", i+1, len(prompts)),
- "id", p.id,
- )
- resp, err := baselineBackend.Generate(context.Background(), p.prompt, opts)
- if err != nil {
- slog.Error("benchmark: baseline failed", "id", p.id, "error", err)
- continue
- }
- baselineResponses[p.id] = resp
-
- if (i+1)%4 == 0 {
- runtime.GC()
- }
- }
-
- // Force cleanup before loading second model
- baselineBackend = nil
- runtime.GC()
- runtime.GC()
-
- // Generate trained responses
- slog.Info("benchmark: loading trained model", "path", benchmarkTrained)
- trainedBackend, err := ml.NewMLXBackend(benchmarkTrained)
- if err != nil {
- return fmt.Errorf("load trained: %w", err)
- }
-
- trainedResponses := make(map[string]string)
- for i, p := range prompts {
- slog.Info("benchmark: trained",
- "prompt", fmt.Sprintf("%d/%d", i+1, len(prompts)),
- "id", p.id,
- )
- resp, err := trainedBackend.Generate(context.Background(), p.prompt, opts)
- if err != nil {
- slog.Error("benchmark: trained failed", "id", p.id, "error", err)
- continue
- }
- trainedResponses[p.id] = resp
-
- if (i+1)%4 == 0 {
- runtime.GC()
- }
- }
-
- trainedBackend = nil
- runtime.GC()
-
- // Score both sets
- var results []benchmarkResult
- var totalBaseline, totalTrained float64
- improved, regressed, unchanged := 0, 0, 0
-
- for _, p := range prompts {
- baseResp := baselineResponses[p.id]
- trainResp := trainedResponses[p.id]
-
- if baseResp == "" || trainResp == "" {
- continue
- }
-
- baseH := ml.ScoreHeuristic(baseResp)
- trainH := ml.ScoreHeuristic(trainResp)
- delta := trainH.LEKScore - baseH.LEKScore
-
- totalBaseline += baseH.LEKScore
- totalTrained += trainH.LEKScore
-
- if delta > 0.5 {
- improved++
- } else if delta < -0.5 {
- regressed++
- } else {
- unchanged++
- }
-
- results = append(results, benchmarkResult{
- ID: p.id,
- Prompt: p.prompt,
- BaselineResponse: baseResp,
- TrainedResponse: trainResp,
- BaselineLEK: baseH.LEKScore,
- TrainedLEK: trainH.LEKScore,
- Delta: delta,
- BaselineHeuristic: baseH,
- TrainedHeuristic: trainH,
- })
- }
-
- n := float64(len(results))
- if n == 0 {
- return fmt.Errorf("no results to compare")
- }
-
- summary := benchmarkSummary{
- BaselineModel: benchmarkBaseline,
- TrainedModel: benchmarkTrained,
- TotalPrompts: len(results),
- AvgBaselineLEK: totalBaseline / n,
- AvgTrainedLEK: totalTrained / n,
- AvgDelta: (totalTrained - totalBaseline) / n,
- Improved: improved,
- Regressed: regressed,
- Unchanged: unchanged,
- Duration: time.Since(start).Round(time.Second).String(),
- Results: results,
- }
-
- // Write output
- data, err := json.MarshalIndent(summary, "", " ")
- if err != nil {
- return fmt.Errorf("marshal output: %w", err)
- }
- if err := os.WriteFile(benchmarkOutput, data, 0644); err != nil {
- return fmt.Errorf("write output: %w", err)
- }
-
- // Print summary
- fmt.Println()
- fmt.Println("=== Benchmark Results ===")
- fmt.Printf("Baseline: %s\n", benchmarkBaseline)
- fmt.Printf("Trained: %s\n", benchmarkTrained)
- fmt.Printf("Prompts: %d\n", len(results))
- fmt.Println()
- fmt.Printf("Avg LEK (baseline): %+.2f\n", summary.AvgBaselineLEK)
- fmt.Printf("Avg LEK (trained): %+.2f\n", summary.AvgTrainedLEK)
- fmt.Printf("Avg Delta: %+.2f\n", summary.AvgDelta)
- fmt.Println()
- fmt.Printf("Improved: %d (%.0f%%)\n", improved, float64(improved)/n*100)
- fmt.Printf("Regressed: %d (%.0f%%)\n", regressed, float64(regressed)/n*100)
- fmt.Printf("Unchanged: %d (%.0f%%)\n", unchanged, float64(unchanged)/n*100)
- fmt.Printf("Duration: %s\n", summary.Duration)
- fmt.Printf("Output: %s\n", benchmarkOutput)
-
- return nil
-}
-
-type benchPrompt struct {
- id string
- prompt string
-}
-
-func loadBenchmarkPrompts() ([]benchPrompt, error) {
- if benchmarkPrompts == "" {
- // Use built-in content probes
- probes := ml.ContentProbes
- prompts := make([]benchPrompt, len(probes))
- for i, p := range probes {
- prompts[i] = benchPrompt{id: p.ID, prompt: p.Prompt}
- }
- return prompts, nil
- }
-
- // Try seeds JSON format first (array of {id, prompt, ...})
- data, err := os.ReadFile(benchmarkPrompts)
- if err != nil {
- return nil, fmt.Errorf("read prompts: %w", err)
- }
-
- var seeds []seedPrompt
- if json.Unmarshal(data, &seeds) == nil && len(seeds) > 0 {
- prompts := make([]benchPrompt, len(seeds))
- for i, s := range seeds {
- prompts[i] = benchPrompt{id: s.ID, prompt: s.Prompt}
- }
- return prompts, nil
- }
-
- // Try JSONL responses format
- responses, err := ml.ReadResponses(benchmarkPrompts)
- if err != nil {
- return nil, fmt.Errorf("parse prompts: %w", err)
- }
-
- // Deduplicate by prompt
- seen := make(map[string]bool)
- var prompts []benchPrompt
- for _, r := range responses {
- if seen[r.Prompt] {
- continue
- }
- seen[r.Prompt] = true
- id := r.ID
- if id == "" {
- id = fmt.Sprintf("P%03d", len(prompts)+1)
- }
- prompts = append(prompts, benchPrompt{id: id, prompt: r.Prompt})
- }
-
- sort.Slice(prompts, func(i, j int) bool { return prompts[i].id < prompts[j].id })
- return prompts, nil
-}
diff --git a/cmd/ml/cmd_benchmark_init.go b/cmd/ml/cmd_benchmark_init.go
deleted file mode 100644
index ff4508ea..00000000
--- a/cmd/ml/cmd_benchmark_init.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-func init() {
- mlCmd.AddCommand(benchmarkCmd)
-}
diff --git a/cmd/ml/cmd_chat.go b/cmd/ml/cmd_chat.go
deleted file mode 100644
index ac470361..00000000
--- a/cmd/ml/cmd_chat.go
+++ /dev/null
@@ -1,327 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-import (
- "bufio"
- "encoding/json"
- "fmt"
- "log/slog"
- "os"
- "runtime"
- "strings"
- "time"
-
- "forge.lthn.ai/core/go-ml"
- "forge.lthn.ai/core/go/pkg/cli"
-)
-
-var chatCmd = &cli.Command{
- Use: "chat",
- Short: "Interactive conversation with a local MLX model",
- Long: `Start an interactive chat session with a local MLX model.
-
-All exchanges are captured and can be written to training JSONL on exit
-for use with 'core ml train'. Optionally apply axiom sandwich signing
-to wrap the conversation for LEK training.
-
-Commands during chat:
- /quit, /exit End session and save
- /save Save conversation so far (appends to output)
- /clear Clear conversation history
- /system Set system prompt
- /undo Remove last exchange`,
- RunE: runChat,
-}
-
-var (
- chatModelPath string
- chatOutput string
- chatKB string
- chatKernel string
- chatSystem string
- chatMaxTokens int
- chatTemp float64
- chatMemLimit int
-)
-
-func init() {
- chatCmd.Flags().StringVar(&chatModelPath, "model-path", "", "Path to model directory (required)")
- chatCmd.Flags().StringVar(&chatOutput, "output", "", "Output JSONL file for captured conversation")
- chatCmd.Flags().StringVar(&chatKB, "kb", "", "Knowledge base document for sandwich signing")
- chatCmd.Flags().StringVar(&chatKernel, "kernel", "", "LEK-1 kernel file for sandwich signing")
- chatCmd.Flags().StringVar(&chatSystem, "system", "", "Initial system prompt")
- chatCmd.Flags().IntVar(&chatMaxTokens, "max-tokens", 2048, "Max tokens per response")
- chatCmd.Flags().Float64Var(&chatTemp, "temperature", 0.4, "Sampling temperature")
- chatCmd.Flags().IntVar(&chatMemLimit, "memory-limit", 24, "Metal memory limit in GB")
- chatCmd.MarkFlagRequired("model-path")
-}
-
-func runChat(cmd *cli.Command, args []string) error {
- // Load optional KB and kernel for sandwich signing
- var kbText, kernelText string
- if chatKB != "" {
- data, err := os.ReadFile(chatKB)
- if err != nil {
- return fmt.Errorf("read KB: %w", err)
- }
- kbText = string(data)
- }
- if chatKernel != "" {
- data, err := os.ReadFile(chatKernel)
- if err != nil {
- return fmt.Errorf("read kernel: %w", err)
- }
- kernelText = string(data)
- }
- sandwich := kbText != "" && kernelText != ""
-
- // Load model
- slog.Info("chat: loading model", "path", chatModelPath)
- backend, err := ml.NewMLXBackend(chatModelPath)
- if err != nil {
- return fmt.Errorf("load model: %w", err)
- }
-
- opts := ml.GenOpts{
- Temperature: chatTemp,
- MaxTokens: chatMaxTokens,
- }
-
- // Conversation state
- var history []ml.Message
- if chatSystem != "" {
- history = append(history, ml.Message{Role: "system", Content: chatSystem})
- }
-
- // Track saved conversations for JSONL output
- var savedConversations [][]ml.Message
-
- fmt.Println("Chat started. Type /quit to exit, /help for commands.")
- if sandwich {
- fmt.Println("Sandwich signing enabled (KB + kernel)")
- }
- if chatOutput != "" {
- fmt.Printf("Capturing to: %s\n", chatOutput)
- }
- fmt.Println()
-
- scanner := bufio.NewScanner(os.Stdin)
- scanner.Buffer(make([]byte, 1<<20), 1<<20) // 1MB input buffer
-
- for {
- fmt.Print("you> ")
- if !scanner.Scan() {
- // EOF (Ctrl+D)
- break
- }
-
- input := strings.TrimSpace(scanner.Text())
- if input == "" {
- continue
- }
-
- // Handle commands
- if strings.HasPrefix(input, "/") {
- cmd := strings.Fields(input)
- switch cmd[0] {
- case "/quit", "/exit":
- goto done
- case "/save":
- if chatOutput == "" {
- fmt.Println("No --output file specified. Use --output to enable saving.")
- continue
- }
- if len(history) > 0 {
- savedConversations = append(savedConversations, cloneMessages(history))
- fmt.Printf("Saved conversation (%d messages)\n", len(history))
- }
- continue
- case "/clear":
- sysPrompt := ""
- for _, m := range history {
- if m.Role == "system" {
- sysPrompt = m.Content
- break
- }
- }
- history = nil
- if sysPrompt != "" {
- history = append(history, ml.Message{Role: "system", Content: sysPrompt})
- }
- fmt.Println("Conversation cleared.")
- continue
- case "/system":
- if len(cmd) < 2 {
- fmt.Println("Usage: /system ")
- continue
- }
- sysText := strings.TrimPrefix(input, "/system ")
- // Replace existing system prompt or add new one
- found := false
- for i, m := range history {
- if m.Role == "system" {
- history[i].Content = sysText
- found = true
- break
- }
- }
- if !found {
- // Prepend system message
- history = append([]ml.Message{{Role: "system", Content: sysText}}, history...)
- }
- fmt.Printf("System prompt set (%d chars)\n", len(sysText))
- continue
- case "/undo":
- // Remove last user+assistant pair
- if len(history) >= 2 {
- last := history[len(history)-1]
- secondLast := history[len(history)-2]
- if secondLast.Role == "user" && last.Role == "assistant" {
- history = history[:len(history)-2]
- fmt.Println("Last exchange removed.")
- } else {
- fmt.Println("Cannot undo: last messages are not a user/assistant pair.")
- }
- } else {
- fmt.Println("Nothing to undo.")
- }
- continue
- case "/help":
- fmt.Println("Commands:")
- fmt.Println(" /quit, /exit End session and save")
- fmt.Println(" /save Save conversation so far")
- fmt.Println(" /clear Clear conversation history")
- fmt.Println(" /system Set system prompt")
- fmt.Println(" /undo Remove last exchange")
- fmt.Println(" /help Show this help")
- continue
- default:
- fmt.Printf("Unknown command: %s (try /help)\n", cmd[0])
- continue
- }
- }
-
- // Add user message
- history = append(history, ml.Message{Role: "user", Content: input})
-
- // Generate response
- genStart := time.Now()
- fmt.Print("\nassistant> ")
-
- var response strings.Builder
- err := backend.ChatStream(cmd.Context(), history, opts, func(token string) error {
- fmt.Print(token)
- response.WriteString(token)
- return nil
- })
- fmt.Println()
-
- if err != nil {
- slog.Error("chat: generation failed", "error", err)
- // Remove the failed user message
- history = history[:len(history)-1]
- continue
- }
-
- elapsed := time.Since(genStart)
- responseText := response.String()
- history = append(history, ml.Message{Role: "assistant", Content: responseText})
-
- slog.Debug("chat: response generated",
- "chars", len(responseText),
- "duration", elapsed.Round(time.Millisecond),
- )
-
- // Periodic cleanup
- if len(history)%8 == 0 {
- runtime.GC()
- }
-
- fmt.Println()
- }
-
-done:
- fmt.Println()
-
- // Save final conversation if output is specified
- if chatOutput != "" && len(history) > 0 {
- // Include current conversation if not already saved
- savedConversations = append(savedConversations, history)
-
- if err := writeChatJSONL(chatOutput, savedConversations, sandwich, kbText, kernelText); err != nil {
- return fmt.Errorf("save conversation: %w", err)
- }
- }
-
- return nil
-}
-
-// writeChatJSONL writes conversations to JSONL file.
-// If sandwich is true, wraps user messages with KB + kernel signing.
-func writeChatJSONL(path string, conversations [][]ml.Message, sandwich bool, kb, kernel string) error {
- f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
- if err != nil {
- return err
- }
- defer f.Close()
-
- encoder := json.NewEncoder(f)
- written := 0
-
- for _, conv := range conversations {
- // Extract user/assistant pairs (skip system messages for training output)
- var messages []ml.Message
- for _, m := range conv {
- if m.Role == "system" {
- continue
- }
- messages = append(messages, m)
- }
-
- if len(messages) < 2 {
- continue
- }
-
- if sandwich {
- // Apply sandwich signing to user messages
- messages = applySandwichSigning(messages, kb, kernel)
- }
-
- record := struct {
- Messages []ml.Message `json:"messages"`
- }{Messages: messages}
-
- if err := encoder.Encode(record); err != nil {
- return err
- }
- written++
- }
-
- slog.Info("chat: saved conversations",
- "file", path,
- "conversations", written,
- "sandwich", sandwich,
- )
- return nil
-}
-
-// applySandwichSigning wraps user messages with KB preamble and kernel postfix.
-func applySandwichSigning(messages []ml.Message, kb, kernel string) []ml.Message {
- signed := make([]ml.Message, len(messages))
- copy(signed, messages)
-
- for i := range signed {
- if signed[i].Role == "user" {
- signed[i].Content = buildSandwich(kb, signed[i].Content, kernel)
- }
- }
- return signed
-}
-
-// cloneMessages creates a deep copy of a message slice.
-func cloneMessages(msgs []ml.Message) []ml.Message {
- clone := make([]ml.Message, len(msgs))
- copy(clone, msgs)
- return clone
-}
diff --git a/cmd/ml/cmd_chat_init.go b/cmd/ml/cmd_chat_init.go
deleted file mode 100644
index 4d281d01..00000000
--- a/cmd/ml/cmd_chat_init.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-func init() {
- mlCmd.AddCommand(chatCmd)
-}
diff --git a/cmd/ml/cmd_consolidate.go b/cmd/ml/cmd_consolidate.go
deleted file mode 100644
index 0e1cf20b..00000000
--- a/cmd/ml/cmd_consolidate.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package ml
-
-import (
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- consolidateM3Host string
- consolidateRemoteDir string
- consolidatePattern string
- consolidateOutputDir string
- consolidateMergedOut string
-)
-
-var consolidateCmd = &cli.Command{
- Use: "consolidate",
- Short: "Pull and merge response JSONL files from M3",
- Long: "Pulls JSONL response files from M3 via SSH/SCP, merges them by idx, deduplicates, and writes a single merged JSONL output.",
- RunE: runConsolidate,
-}
-
-func init() {
- consolidateCmd.Flags().StringVar(&consolidateM3Host, "m3-host", "m3", "M3 SSH host")
- consolidateCmd.Flags().StringVar(&consolidateRemoteDir, "remote", "/Volumes/Data/lem/responses", "Remote response directory")
- consolidateCmd.Flags().StringVar(&consolidatePattern, "pattern", "gold*.jsonl", "File glob pattern")
- consolidateCmd.Flags().StringVar(&consolidateOutputDir, "output", "", "Local output directory (default: responses)")
- consolidateCmd.Flags().StringVar(&consolidateMergedOut, "merged", "", "Merged output path (default: gold-merged.jsonl in parent of output dir)")
-}
-
-func runConsolidate(cmd *cli.Command, args []string) error {
- cfg := ml.ConsolidateConfig{
- M3Host: consolidateM3Host,
- RemoteDir: consolidateRemoteDir,
- Pattern: consolidatePattern,
- OutputDir: consolidateOutputDir,
- MergedOut: consolidateMergedOut,
- }
-
- return ml.Consolidate(cfg, cmd.OutOrStdout())
-}
diff --git a/cmd/ml/cmd_convert.go b/cmd/ml/cmd_convert.go
deleted file mode 100644
index 84c9b27e..00000000
--- a/cmd/ml/cmd_convert.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package ml
-
-import (
- "fmt"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- convertInput string
- convertConfig string
- convertOutputDir string
- convertBaseModel string
-)
-
-var convertCmd = &cli.Command{
- Use: "convert",
- Short: "Convert MLX LoRA adapter to PEFT format",
- Long: "Converts an MLX safetensors LoRA adapter to HuggingFace PEFT format for Ollama.",
- RunE: runConvert,
-}
-
-func init() {
- convertCmd.Flags().StringVar(&convertInput, "input", "", "Input safetensors file (required)")
- convertCmd.Flags().StringVar(&convertConfig, "config", "", "Adapter config JSON (required)")
- convertCmd.Flags().StringVar(&convertOutputDir, "output-dir", "", "Output directory (required)")
- convertCmd.Flags().StringVar(&convertBaseModel, "base-model", "", "Base model name for adapter_config.json")
- convertCmd.MarkFlagRequired("input")
- convertCmd.MarkFlagRequired("config")
- convertCmd.MarkFlagRequired("output-dir")
-}
-
-func runConvert(cmd *cli.Command, args []string) error {
- if err := ml.ConvertMLXtoPEFT(convertInput, convertConfig, convertOutputDir, convertBaseModel); err != nil {
- return fmt.Errorf("convert to PEFT: %w", err)
- }
- fmt.Printf("PEFT adapter written to %s\n", convertOutputDir)
- return nil
-}
diff --git a/cmd/ml/cmd_coverage.go b/cmd/ml/cmd_coverage.go
deleted file mode 100644
index 5d3acf7f..00000000
--- a/cmd/ml/cmd_coverage.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var coverageCmd = &cli.Command{
- Use: "coverage",
- Short: "Analyze seed coverage by region and domain",
- Long: "Queries seeds by region and domain, renders ASCII bar charts, and highlights underrepresented areas.",
- RunE: runCoverage,
-}
-
-func runCoverage(cmd *cli.Command, args []string) error {
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB required")
- }
-
- db, err := ml.OpenDB(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- return ml.PrintCoverage(db, cmd.OutOrStdout())
-}
diff --git a/cmd/ml/cmd_expand.go b/cmd/ml/cmd_expand.go
deleted file mode 100644
index 432aa3fa..00000000
--- a/cmd/ml/cmd_expand.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package ml
-
-import (
- "context"
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- expandWorker string
- expandOutput string
- expandLimit int
- expandDryRun bool
-)
-
-var expandCmd = &cli.Command{
- Use: "expand",
- Short: "Generate expansion responses from pending prompts",
- Long: "Reads pending expansion prompts from DuckDB and generates responses via an OpenAI-compatible API.",
- RunE: runExpand,
-}
-
-func init() {
- expandCmd.Flags().StringVar(&expandWorker, "worker", "", "Worker hostname (defaults to os.Hostname())")
- expandCmd.Flags().StringVar(&expandOutput, "output", ".", "Output directory for JSONL files")
- expandCmd.Flags().IntVar(&expandLimit, "limit", 0, "Max prompts to process (0 = all)")
- expandCmd.Flags().BoolVar(&expandDryRun, "dry-run", false, "Print plan and exit without generating")
-}
-
-func runExpand(cmd *cli.Command, args []string) error {
- if modelName == "" {
- return fmt.Errorf("--model is required")
- }
-
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB env is required")
- }
-
- if expandWorker == "" {
- h, _ := os.Hostname()
- expandWorker = h
- }
-
- db, err := ml.OpenDBReadWrite(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- rows, err := db.QueryExpansionPrompts("pending", expandLimit)
- if err != nil {
- return fmt.Errorf("query expansion_prompts: %w", err)
- }
- fmt.Printf("Loaded %d pending prompts from %s\n", len(rows), path)
-
- var prompts []ml.Response
- for _, r := range rows {
- prompt := r.Prompt
- if prompt == "" && r.PromptEn != "" {
- prompt = r.PromptEn
- }
- prompts = append(prompts, ml.Response{
- ID: r.SeedID,
- Domain: r.Domain,
- Prompt: prompt,
- })
- }
-
- ctx := context.Background()
- backend := ml.NewHTTPBackend(apiURL, modelName)
- influx := ml.NewInfluxClient(influxURL, influxDB)
-
- return ml.ExpandPrompts(ctx, backend, influx, prompts, modelName, expandWorker, expandOutput, expandDryRun, expandLimit)
-}
diff --git a/cmd/ml/cmd_expand_status.go b/cmd/ml/cmd_expand_status.go
deleted file mode 100644
index 70df2bf2..00000000
--- a/cmd/ml/cmd_expand_status.go
+++ /dev/null
@@ -1,95 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var expandStatusCmd = &cli.Command{
- Use: "expand-status",
- Short: "Show expansion pipeline progress",
- Long: "Queries DuckDB for expansion prompts, generated responses, scoring status, and overall pipeline progress.",
- RunE: runExpandStatus,
-}
-
-func runExpandStatus(cmd *cli.Command, args []string) error {
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB required")
- }
-
- db, err := ml.OpenDB(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- fmt.Fprintln(os.Stdout, "LEM Expansion Pipeline Status")
- fmt.Fprintln(os.Stdout, "==================================================")
-
- // Expansion prompts
- total, pending, err := db.CountExpansionPrompts()
- if err != nil {
- fmt.Fprintln(os.Stdout, " Expansion prompts: not created (run: normalize)")
- return nil
- }
- fmt.Fprintf(os.Stdout, " Expansion prompts: %d total, %d pending\n", total, pending)
-
- // Generated responses — query raw counts via SQL
- generated := 0
- rows, err := db.QueryRows("SELECT count(*) AS n FROM expansion_raw")
- if err != nil || len(rows) == 0 {
- fmt.Fprintln(os.Stdout, " Generated: 0 (run: core ml expand)")
- } else {
- if n, ok := rows[0]["n"]; ok {
- generated = toInt(n)
- }
- fmt.Fprintf(os.Stdout, " Generated: %d\n", generated)
- }
-
- // Scored — query scoring counts via SQL
- sRows, err := db.QueryRows("SELECT count(*) AS n FROM scoring_results WHERE suite = 'heuristic'")
- if err != nil || len(sRows) == 0 {
- fmt.Fprintln(os.Stdout, " Scored: 0 (run: score --tier 1)")
- } else {
- scored := toInt(sRows[0]["n"])
- fmt.Fprintf(os.Stdout, " Heuristic scored: %d\n", scored)
- }
-
- // Pipeline progress
- if total > 0 && generated > 0 {
- genPct := float64(generated) / float64(total) * 100
- fmt.Fprintf(os.Stdout, "\n Progress: %.1f%% generated\n", genPct)
- }
-
- // Golden set context
- golden, err := db.CountGoldenSet()
- if err == nil && golden > 0 {
- fmt.Fprintf(os.Stdout, "\n Golden set: %d / %d\n", golden, targetTotal)
- if generated > 0 {
- fmt.Fprintf(os.Stdout, " Combined: %d total examples\n", golden+generated)
- }
- }
-
- return nil
-}
-
-// toInt converts an interface{} (typically from QueryRows) to int.
-func toInt(v interface{}) int {
- switch n := v.(type) {
- case int:
- return n
- case int64:
- return int(n)
- case float64:
- return int(n)
- default:
- return 0
- }
-}
diff --git a/cmd/ml/cmd_export.go b/cmd/ml/cmd_export.go
deleted file mode 100644
index 2ce2b60a..00000000
--- a/cmd/ml/cmd_export.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- exportOutputDir string
- exportMinChars int
- exportTrainPct int
- exportValidPct int
- exportTestPct int
- exportSeed int64
- exportParquet bool
-)
-
-var exportCmd = &cli.Command{
- Use: "export",
- Short: "Export golden set to training JSONL and Parquet",
- Long: "Reads golden set from DuckDB, filters, splits, and exports to JSONL and optionally Parquet.",
- RunE: runExport,
-}
-
-func init() {
- exportCmd.Flags().StringVar(&exportOutputDir, "output-dir", "", "Output directory for training files (required)")
- exportCmd.Flags().IntVar(&exportMinChars, "min-chars", 50, "Minimum response length in characters")
- exportCmd.Flags().IntVar(&exportTrainPct, "train", 80, "Training split percentage")
- exportCmd.Flags().IntVar(&exportValidPct, "valid", 10, "Validation split percentage")
- exportCmd.Flags().IntVar(&exportTestPct, "test", 10, "Test split percentage")
- exportCmd.Flags().Int64Var(&exportSeed, "seed", 42, "Random seed for shuffle")
- exportCmd.Flags().BoolVar(&exportParquet, "parquet", false, "Also export Parquet files")
- exportCmd.MarkFlagRequired("output-dir")
-}
-
-func runExport(cmd *cli.Command, args []string) error {
- if err := ml.ValidatePercentages(exportTrainPct, exportValidPct, exportTestPct); err != nil {
- return err
- }
-
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB env is required")
- }
-
- db, err := ml.OpenDB(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- rows, err := db.QueryGoldenSet(exportMinChars)
- if err != nil {
- return fmt.Errorf("query golden set: %w", err)
- }
- fmt.Printf("Loaded %d golden set rows (min %d chars)\n", len(rows), exportMinChars)
-
- // Convert to Response format.
- var responses []ml.Response
- for _, r := range rows {
- responses = append(responses, ml.Response{
- ID: r.SeedID,
- Domain: r.Domain,
- Prompt: r.Prompt,
- Response: r.Response,
- })
- }
-
- filtered := ml.FilterResponses(responses)
- fmt.Printf("After filtering: %d responses\n", len(filtered))
-
- train, valid, test := ml.SplitData(filtered, exportTrainPct, exportValidPct, exportTestPct, exportSeed)
- fmt.Printf("Split: train=%d, valid=%d, test=%d\n", len(train), len(valid), len(test))
-
- if err := os.MkdirAll(exportOutputDir, 0755); err != nil {
- return fmt.Errorf("create output dir: %w", err)
- }
-
- for _, split := range []struct {
- name string
- data []ml.Response
- }{
- {"train", train},
- {"valid", valid},
- {"test", test},
- } {
- path := fmt.Sprintf("%s/%s.jsonl", exportOutputDir, split.name)
- if err := ml.WriteTrainingJSONL(path, split.data); err != nil {
- return fmt.Errorf("write %s: %w", split.name, err)
- }
- fmt.Printf(" %s.jsonl: %d examples\n", split.name, len(split.data))
- }
-
- if exportParquet {
- n, err := ml.ExportParquet(exportOutputDir, "")
- if err != nil {
- return fmt.Errorf("export parquet: %w", err)
- }
- fmt.Printf(" Parquet: %d total rows\n", n)
- }
-
- return nil
-}
diff --git a/cmd/ml/cmd_gguf.go b/cmd/ml/cmd_gguf.go
deleted file mode 100644
index 8b1f53b4..00000000
--- a/cmd/ml/cmd_gguf.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package ml
-
-import (
- "fmt"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- ggufInput string
- ggufConfig string
- ggufOutput string
- ggufArch string
-)
-
-var ggufCmd = &cli.Command{
- Use: "gguf",
- Short: "Convert MLX LoRA adapter to GGUF format",
- Long: "Converts an MLX safetensors LoRA adapter to GGUF v3 format for use with llama.cpp.",
- RunE: runGGUF,
-}
-
-func init() {
- ggufCmd.Flags().StringVar(&ggufInput, "input", "", "Input safetensors file (required)")
- ggufCmd.Flags().StringVar(&ggufConfig, "config", "", "Adapter config JSON (required)")
- ggufCmd.Flags().StringVar(&ggufOutput, "output", "", "Output GGUF file (required)")
- ggufCmd.Flags().StringVar(&ggufArch, "arch", "gemma3", "GGUF architecture name")
- ggufCmd.MarkFlagRequired("input")
- ggufCmd.MarkFlagRequired("config")
- ggufCmd.MarkFlagRequired("output")
-}
-
-func runGGUF(cmd *cli.Command, args []string) error {
- if err := ml.ConvertMLXtoGGUFLoRA(ggufInput, ggufConfig, ggufOutput, ggufArch); err != nil {
- return fmt.Errorf("convert to GGUF: %w", err)
- }
- fmt.Printf("GGUF LoRA adapter written to %s\n", ggufOutput)
- return nil
-}
diff --git a/cmd/ml/cmd_import.go b/cmd/ml/cmd_import.go
deleted file mode 100644
index 03fb7dd0..00000000
--- a/cmd/ml/cmd_import.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
- "path/filepath"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var importCmd = &cli.Command{
- Use: "import-all",
- Short: "Import all LEM data into DuckDB",
- Long: "Imports golden set, training examples, benchmark results, benchmark questions, and seeds into DuckDB from M3 and local files.",
- RunE: runImportAll,
-}
-
-var (
- importSkipM3 bool
- importDataDir string
- importM3Host string
-)
-
-func init() {
- importCmd.Flags().BoolVar(&importSkipM3, "skip-m3", false, "Skip pulling data from M3")
- importCmd.Flags().StringVar(&importDataDir, "data-dir", "", "Local data directory (defaults to db directory)")
- importCmd.Flags().StringVar(&importM3Host, "m3-host", "m3", "M3 SSH host alias")
-}
-
-func runImportAll(cmd *cli.Command, args []string) error {
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB required")
- }
-
- dataDir := importDataDir
- if dataDir == "" {
- dataDir = filepath.Dir(path)
- }
-
- db, err := ml.OpenDBReadWrite(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- cfg := ml.ImportConfig{
- SkipM3: importSkipM3,
- DataDir: dataDir,
- M3Host: importM3Host,
- }
-
- return ml.ImportAll(db, cfg, cmd.OutOrStdout())
-}
diff --git a/cmd/ml/cmd_ingest.go b/cmd/ml/cmd_ingest.go
deleted file mode 100644
index f071a2a6..00000000
--- a/cmd/ml/cmd_ingest.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var ingestCmd = &cli.Command{
- Use: "ingest",
- Short: "Ingest benchmark scores and training logs into InfluxDB",
- Long: "Reads content score, capability score, and training log files and writes measurements to InfluxDB for the lab dashboard.",
- RunE: runIngest,
-}
-
-var (
- ingestContent string
- ingestCapability string
- ingestTraining string
- ingestRunID string
- ingestBatchSize int
-)
-
-func init() {
- ingestCmd.Flags().StringVar(&ingestContent, "content", "", "Content scores JSONL file")
- ingestCmd.Flags().StringVar(&ingestCapability, "capability", "", "Capability scores JSONL file")
- ingestCmd.Flags().StringVar(&ingestTraining, "training-log", "", "MLX LoRA training log file")
- ingestCmd.Flags().StringVar(&ingestRunID, "run-id", "", "Run ID tag (defaults to model name)")
- ingestCmd.Flags().IntVar(&ingestBatchSize, "batch-size", 100, "Lines per InfluxDB write batch")
-}
-
-func runIngest(cmd *cli.Command, args []string) error {
- if modelName == "" {
- return fmt.Errorf("--model is required")
- }
- if ingestContent == "" && ingestCapability == "" && ingestTraining == "" {
- return fmt.Errorf("at least one of --content, --capability, or --training-log is required")
- }
-
- influx := ml.NewInfluxClient(influxURL, influxDB)
-
- cfg := ml.IngestConfig{
- ContentFile: ingestContent,
- CapabilityFile: ingestCapability,
- TrainingLog: ingestTraining,
- Model: modelName,
- RunID: ingestRunID,
- BatchSize: ingestBatchSize,
- }
-
- return ml.Ingest(influx, cfg, os.Stdout)
-}
diff --git a/cmd/ml/cmd_inventory.go b/cmd/ml/cmd_inventory.go
deleted file mode 100644
index eed4ff6a..00000000
--- a/cmd/ml/cmd_inventory.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var inventoryCmd = &cli.Command{
- Use: "inventory",
- Short: "Show DuckDB table inventory with stats",
- Long: "Queries all DuckDB tables and prints row counts with per-table detail breakdowns.",
- RunE: runInventory,
-}
-
-func runInventory(cmd *cli.Command, args []string) error {
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB required")
- }
-
- db, err := ml.OpenDB(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- return ml.PrintInventory(db, os.Stdout)
-}
diff --git a/cmd/ml/cmd_lesson.go b/cmd/ml/cmd_lesson.go
deleted file mode 100644
index 4246c076..00000000
--- a/cmd/ml/cmd_lesson.go
+++ /dev/null
@@ -1,340 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "log/slog"
- "os"
- "path/filepath"
- "runtime"
- "strings"
- "time"
-
- "forge.lthn.ai/core/go-ml"
- "forge.lthn.ai/core/go/pkg/cli"
- "gopkg.in/yaml.v3"
-)
-
-var lessonCmd = &cli.Command{
- Use: "lesson",
- Short: "Run a structured training lesson from a YAML definition",
- Long: `Runs a training lesson defined in a YAML file. Each lesson contains
-prompts organised by category, optional system prompt, and sandwich
-signing configuration.
-
-Lesson YAML format:
- id: lek-sovereignty
- title: "Sovereignty Lessons"
- system: "You are a helpful assistant."
- sandwich:
- kb: path/to/axioms.md
- kernel: path/to/kernel.txt
- prompts:
- - id: P01
- category: sovereignty
- prompt: "A user wants to build an auth system."
- signal: "Does the model prefer decentralised?"
-
-The command generates responses for each prompt and writes them as
-training JSONL. State is tracked so lessons can be resumed.`,
- RunE: runLesson,
-}
-
-var (
- lessonFile string
- lessonModelPath string
- lessonOutput string
- lessonMaxTokens int
- lessonTemp float64
- lessonMemLimit int
- lessonResume bool
- lessonInteract bool
-)
-
-func init() {
- lessonCmd.Flags().StringVar(&lessonFile, "file", "", "Lesson YAML file (required)")
- lessonCmd.Flags().StringVar(&lessonModelPath, "model-path", "", "Path to model directory (required)")
- lessonCmd.Flags().StringVar(&lessonOutput, "output", "", "Output JSONL file (default: .jsonl)")
- lessonCmd.Flags().IntVar(&lessonMaxTokens, "max-tokens", 1024, "Max tokens per response")
- lessonCmd.Flags().Float64Var(&lessonTemp, "temperature", 0.4, "Sampling temperature")
- lessonCmd.Flags().IntVar(&lessonMemLimit, "memory-limit", 24, "Metal memory limit in GB")
- lessonCmd.Flags().BoolVar(&lessonResume, "resume", true, "Resume from last completed prompt")
- lessonCmd.Flags().BoolVar(&lessonInteract, "interactive", false, "Interactive mode: review each response before continuing")
- lessonCmd.MarkFlagRequired("file")
- lessonCmd.MarkFlagRequired("model-path")
-}
-
-// lessonDef is a YAML lesson definition.
-type lessonDef struct {
- ID string `yaml:"id"`
- Title string `yaml:"title"`
- System string `yaml:"system"`
- Sandwich *lessonSandwichCfg `yaml:"sandwich"`
- Prompts []lessonPrompt `yaml:"prompts"`
-}
-
-type lessonSandwichCfg struct {
- KB string `yaml:"kb"`
- Kernel string `yaml:"kernel"`
-}
-
-type lessonPrompt struct {
- ID string `yaml:"id"`
- Category string `yaml:"category"`
- Prompt string `yaml:"prompt"`
- Signal string `yaml:"signal"`
-}
-
-// lessonState tracks progress through a lesson.
-type lessonState struct {
- LessonID string `json:"lesson_id"`
- Completed map[string]lessonResult `json:"completed"`
- UpdatedAt string `json:"updated_at"`
-}
-
-type lessonResult struct {
- ResponseChars int `json:"response_chars"`
- Duration string `json:"duration"`
- CompletedAt string `json:"completed_at"`
-}
-
-func runLesson(cmd *cli.Command, args []string) error {
- start := time.Now()
-
- // Load lesson YAML
- data, err := os.ReadFile(lessonFile)
- if err != nil {
- return fmt.Errorf("read lesson: %w", err)
- }
-
- var lesson lessonDef
- if err := yaml.Unmarshal(data, &lesson); err != nil {
- return fmt.Errorf("parse lesson: %w", err)
- }
-
- if lesson.ID == "" {
- lesson.ID = strings.TrimSuffix(filepath.Base(lessonFile), filepath.Ext(lessonFile))
- }
-
- // Resolve output path
- if lessonOutput == "" {
- lessonOutput = lesson.ID + ".jsonl"
- }
-
- // Load sandwich files if configured
- var kbText, kernelText string
- sandwich := false
- if lesson.Sandwich != nil {
- baseDir := filepath.Dir(lessonFile)
- if lesson.Sandwich.KB != "" {
- kbPath := lesson.Sandwich.KB
- if !filepath.IsAbs(kbPath) {
- kbPath = filepath.Join(baseDir, kbPath)
- }
- d, err := os.ReadFile(kbPath)
- if err != nil {
- return fmt.Errorf("read KB: %w", err)
- }
- kbText = string(d)
- }
- if lesson.Sandwich.Kernel != "" {
- kernelPath := lesson.Sandwich.Kernel
- if !filepath.IsAbs(kernelPath) {
- kernelPath = filepath.Join(baseDir, kernelPath)
- }
- d, err := os.ReadFile(kernelPath)
- if err != nil {
- return fmt.Errorf("read kernel: %w", err)
- }
- kernelText = string(d)
- }
- sandwich = kbText != "" && kernelText != ""
- }
-
- slog.Info("lesson: loaded",
- "id", lesson.ID,
- "title", lesson.Title,
- "prompts", len(lesson.Prompts),
- "sandwich", sandwich,
- )
-
- if len(lesson.Prompts) == 0 {
- return fmt.Errorf("lesson has no prompts")
- }
-
- // Load state for resume
- stateFile := lesson.ID + ".state.json"
- state := loadLessonState(stateFile)
- if state.LessonID == "" {
- state.LessonID = lesson.ID
- state.Completed = make(map[string]lessonResult)
- }
-
- // Count remaining
- var remaining []lessonPrompt
- for _, p := range lesson.Prompts {
- if lessonResume {
- if _, done := state.Completed[p.ID]; done {
- continue
- }
- }
- remaining = append(remaining, p)
- }
-
- if len(remaining) == 0 {
- slog.Info("lesson: all prompts completed",
- "id", lesson.ID,
- "total", len(lesson.Prompts),
- )
- return nil
- }
-
- slog.Info("lesson: starting",
- "remaining", len(remaining),
- "completed", len(state.Completed),
- "total", len(lesson.Prompts),
- )
-
- // Load model
- slog.Info("lesson: loading model", "path", lessonModelPath)
- backend, err := ml.NewMLXBackend(lessonModelPath)
- if err != nil {
- return fmt.Errorf("load model: %w", err)
- }
-
- opts := ml.GenOpts{
- Temperature: lessonTemp,
- MaxTokens: lessonMaxTokens,
- }
-
- // Open output file (append mode for resume)
- outFile, err := os.OpenFile(lessonOutput, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
- if err != nil {
- return fmt.Errorf("create output: %w", err)
- }
- defer outFile.Close()
- encoder := json.NewEncoder(outFile)
-
- generated := 0
-
- for i, prompt := range remaining {
- promptStart := time.Now()
-
- slog.Info("lesson: generating",
- "prompt", fmt.Sprintf("%d/%d", i+1, len(remaining)),
- "id", prompt.ID,
- "category", prompt.Category,
- )
-
- // Build messages
- var messages []ml.Message
- if lesson.System != "" {
- messages = append(messages, ml.Message{Role: "system", Content: lesson.System})
- }
-
- userContent := prompt.Prompt
- if sandwich {
- userContent = buildSandwich(kbText, prompt.Prompt, kernelText)
- }
- messages = append(messages, ml.Message{Role: "user", Content: userContent})
-
- // Generate
- response, err := backend.Chat(context.Background(), messages, opts)
- if err != nil {
- slog.Error("lesson: generation failed",
- "id", prompt.ID,
- "error", err,
- )
- continue
- }
-
- elapsed := time.Since(promptStart)
-
- // Write training record
- record := struct {
- Messages []ml.Message `json:"messages"`
- }{
- Messages: []ml.Message{
- {Role: "user", Content: userContent},
- {Role: "assistant", Content: response},
- },
- }
- if err := encoder.Encode(record); err != nil {
- return fmt.Errorf("write record: %w", err)
- }
-
- // Update state
- state.Completed[prompt.ID] = lessonResult{
- ResponseChars: len(response),
- Duration: elapsed.Round(time.Second).String(),
- CompletedAt: time.Now().Format(time.RFC3339),
- }
- state.UpdatedAt = time.Now().Format(time.RFC3339)
-
- if err := saveLessonState(stateFile, state); err != nil {
- slog.Warn("lesson: failed to save state", "error", err)
- }
-
- generated++
-
- slog.Info("lesson: generated",
- "id", prompt.ID,
- "category", prompt.Category,
- "response_chars", len(response),
- "duration", elapsed.Round(time.Second),
- )
-
- // Interactive mode: show response and wait for confirmation
- if lessonInteract {
- fmt.Printf("\n--- %s (%s) ---\n", prompt.ID, prompt.Category)
- fmt.Printf("Prompt: %s\n\n", prompt.Prompt)
- if prompt.Signal != "" {
- fmt.Printf("Signal: %s\n\n", prompt.Signal)
- }
- fmt.Printf("Response:\n%s\n", response)
- fmt.Printf("\nPress Enter to continue (or 'q' to stop)... ")
- var input string
- fmt.Scanln(&input)
- if strings.TrimSpace(input) == "q" {
- break
- }
- }
-
- // Periodic cleanup
- if (i+1)%4 == 0 {
- runtime.GC()
- }
- }
-
- slog.Info("lesson: complete",
- "id", lesson.ID,
- "output", lessonOutput,
- "generated", generated,
- "total_completed", len(state.Completed),
- "total_prompts", len(lesson.Prompts),
- "duration", time.Since(start).Round(time.Second),
- )
-
- return nil
-}
-
-func loadLessonState(path string) lessonState {
- data, err := os.ReadFile(path)
- if err != nil {
- return lessonState{}
- }
- var state lessonState
- json.Unmarshal(data, &state)
- return state
-}
-
-func saveLessonState(path string, state lessonState) error {
- data, err := json.MarshalIndent(state, "", " ")
- if err != nil {
- return err
- }
- return os.WriteFile(path, data, 0644)
-}
diff --git a/cmd/ml/cmd_lesson_init.go b/cmd/ml/cmd_lesson_init.go
deleted file mode 100644
index d2342dde..00000000
--- a/cmd/ml/cmd_lesson_init.go
+++ /dev/null
@@ -1,8 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-func init() {
- mlCmd.AddCommand(lessonCmd)
- mlCmd.AddCommand(sequenceCmd)
-}
diff --git a/cmd/ml/cmd_live.go b/cmd/ml/cmd_live.go
deleted file mode 100644
index 5abef9ce..00000000
--- a/cmd/ml/cmd_live.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-const targetTotal = 15000
-
-var liveCmd = &cli.Command{
- Use: "live",
- Short: "Show live generation progress from InfluxDB",
- Long: "Queries InfluxDB for real-time generation progress, worker breakdown, and domain/voice counts.",
- RunE: runLive,
-}
-
-func runLive(cmd *cli.Command, args []string) error {
- influx := ml.NewInfluxClient(influxURL, influxDB)
-
- // Total completed generations
- totalRows, err := influx.QuerySQL("SELECT count(DISTINCT i) AS n FROM gold_gen")
- if err != nil {
- return fmt.Errorf("live: query total: %w", err)
- }
- total := sqlScalar(totalRows)
-
- // Distinct domains and voices
- domainRows, err := influx.QuerySQL("SELECT count(DISTINCT d) AS n FROM gold_gen")
- if err != nil {
- return fmt.Errorf("live: query domains: %w", err)
- }
- domains := sqlScalar(domainRows)
-
- voiceRows, err := influx.QuerySQL("SELECT count(DISTINCT v) AS n FROM gold_gen")
- if err != nil {
- return fmt.Errorf("live: query voices: %w", err)
- }
- voices := sqlScalar(voiceRows)
-
- // Per-worker breakdown
- workers, err := influx.QuerySQL("SELECT w, count(DISTINCT i) AS n FROM gold_gen GROUP BY w ORDER BY n DESC")
- if err != nil {
- return fmt.Errorf("live: query workers: %w", err)
- }
-
- pct := float64(total) / float64(targetTotal) * 100
- remaining := targetTotal - total
-
- fmt.Fprintln(os.Stdout, "Golden Set Live Status (from InfluxDB)")
- fmt.Fprintln(os.Stdout, "─────────────────────────────────────────────")
- fmt.Fprintf(os.Stdout, " Total: %d / %d (%.1f%%)\n", total, targetTotal, pct)
- fmt.Fprintf(os.Stdout, " Remaining: %d\n", remaining)
- fmt.Fprintf(os.Stdout, " Domains: %d\n", domains)
- fmt.Fprintf(os.Stdout, " Voices: %d\n", voices)
- fmt.Fprintln(os.Stdout)
- fmt.Fprintln(os.Stdout, " Workers:")
- for _, w := range workers {
- name := w["w"]
- n := w["n"]
- marker := ""
- if name == "migration" {
- marker = " (seed data)"
- }
- fmt.Fprintf(os.Stdout, " %-20s %6s generations%s\n", name, n, marker)
- }
-
- return nil
-}
-
-// sqlScalar extracts the first numeric value from a QuerySQL result.
-func sqlScalar(rows []map[string]interface{}) int {
- if len(rows) == 0 {
- return 0
- }
- for _, v := range rows[0] {
- return toInt(v)
- }
- return 0
-}
diff --git a/cmd/ml/cmd_metrics.go b/cmd/ml/cmd_metrics.go
deleted file mode 100644
index 6bc6bcf2..00000000
--- a/cmd/ml/cmd_metrics.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var metricsCmd = &cli.Command{
- Use: "metrics",
- Short: "Push golden set stats to InfluxDB",
- Long: "Queries golden_set stats from DuckDB and pushes summary, per-domain, and per-voice metrics to InfluxDB.",
- RunE: runMetrics,
-}
-
-func runMetrics(cmd *cli.Command, args []string) error {
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB required")
- }
-
- db, err := ml.OpenDB(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- influx := ml.NewInfluxClient(influxURL, influxDB)
-
- return ml.PushMetrics(db, influx, os.Stdout)
-}
diff --git a/cmd/ml/cmd_ml.go b/cmd/ml/cmd_ml.go
deleted file mode 100644
index f8dab296..00000000
--- a/cmd/ml/cmd_ml.go
+++ /dev/null
@@ -1,91 +0,0 @@
-// Package ml provides ML inference, scoring, and training pipeline commands.
-//
-// Commands:
-// - core ml score: Score responses with heuristic and LLM judges
-// - core ml probe: Run capability and content probes against a model
-// - core ml export: Export golden set to training JSONL/Parquet
-// - core ml expand: Generate expansion responses
-// - core ml status: Show training and generation progress
-// - core ml gguf: Convert MLX LoRA adapter to GGUF format
-// - core ml convert: Convert MLX LoRA adapter to PEFT format
-// - core ml agent: Run the scoring agent daemon
-// - core ml worker: Run a distributed worker node
-// - core ml serve: Start OpenAI-compatible inference server
-// - core ml inventory: Show DuckDB table inventory with stats
-// - core ml query: Run ad-hoc SQL against DuckDB
-// - core ml metrics: Push golden set stats to InfluxDB
-// - core ml ingest: Ingest benchmark scores and training logs to InfluxDB
-// - core ml normalize: Deduplicate seeds into expansion prompts
-// - core ml seed-influx: Migrate golden set from DuckDB to InfluxDB
-// - core ml consolidate: Pull and merge response JSONL files from M3
-// - core ml import-all: Import all LEM data into DuckDB
-// - core ml approve: Filter scored expansions into training JSONL
-// - core ml publish: Upload Parquet dataset to HuggingFace Hub
-// - core ml coverage: Analyze seed coverage by region and domain
-// - core ml live: Show live generation progress from InfluxDB
-// - core ml expand-status: Show expansion pipeline progress
-package ml
-
-import (
- "forge.lthn.ai/core/go/pkg/cli"
-)
-
-func init() {
- cli.RegisterCommands(AddMLCommands)
-}
-
-var mlCmd = &cli.Command{
- Use: "ml",
- Short: "ML inference, scoring, and training pipeline",
- Long: "Commands for ML model scoring, probe evaluation, data export, and format conversion.",
-}
-
-// AddMLCommands registers the 'ml' command and all subcommands.
-func AddMLCommands(root *cli.Command) {
- initFlags()
- mlCmd.AddCommand(scoreCmd)
- mlCmd.AddCommand(probeCmd)
- mlCmd.AddCommand(exportCmd)
- mlCmd.AddCommand(expandCmd)
- mlCmd.AddCommand(statusCmd)
- mlCmd.AddCommand(ggufCmd)
- mlCmd.AddCommand(convertCmd)
- mlCmd.AddCommand(agentCmd)
- mlCmd.AddCommand(workerCmd)
- mlCmd.AddCommand(serveCmd)
- mlCmd.AddCommand(inventoryCmd)
- mlCmd.AddCommand(queryCmd)
- mlCmd.AddCommand(metricsCmd)
- mlCmd.AddCommand(ingestCmd)
- mlCmd.AddCommand(normalizeCmd)
- mlCmd.AddCommand(seedInfluxCmd)
- mlCmd.AddCommand(consolidateCmd)
- mlCmd.AddCommand(importCmd)
- mlCmd.AddCommand(approveCmd)
- mlCmd.AddCommand(publishCmd)
- mlCmd.AddCommand(coverageCmd)
- mlCmd.AddCommand(liveCmd)
- mlCmd.AddCommand(expandStatusCmd)
- root.AddCommand(mlCmd)
-}
-
-// Shared persistent flags.
-var (
- apiURL string
- judgeURL string
- judgeModel string
- influxURL string
- influxDB string
- dbPath string
- modelName string
-)
-
-func initFlags() {
- mlCmd.PersistentFlags().StringVar(&apiURL, "api-url", "http://10.69.69.108:8090", "OpenAI-compatible API URL")
- mlCmd.PersistentFlags().StringVar(&judgeURL, "judge-url", "http://10.69.69.108:11434", "Judge model API URL (Ollama)")
- mlCmd.PersistentFlags().StringVar(&judgeModel, "judge-model", "gemma3:27b", "Judge model name")
- mlCmd.PersistentFlags().StringVar(&influxURL, "influx", "", "InfluxDB URL (default http://10.69.69.165:8181)")
- mlCmd.PersistentFlags().StringVar(&influxDB, "influx-db", "", "InfluxDB database (default training)")
- mlCmd.PersistentFlags().StringVar(&dbPath, "db", "", "DuckDB database path (or set LEM_DB env)")
- mlCmd.PersistentFlags().StringVar(&modelName, "model", "", "Model name for API")
-}
diff --git a/cmd/ml/cmd_normalize.go b/cmd/ml/cmd_normalize.go
deleted file mode 100644
index 4e4715bc..00000000
--- a/cmd/ml/cmd_normalize.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var normalizeMinLen int
-
-var normalizeCmd = &cli.Command{
- Use: "normalize",
- Short: "Normalize seeds into expansion prompts",
- Long: "Deduplicates seeds against golden_set and prompts, creating the expansion_prompts table with priority-based ordering.",
- RunE: runNormalize,
-}
-
-func init() {
- normalizeCmd.Flags().IntVar(&normalizeMinLen, "min-length", 50, "Minimum prompt length in characters")
-}
-
-func runNormalize(cmd *cli.Command, args []string) error {
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB env is required")
- }
-
- db, err := ml.OpenDBReadWrite(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- cfg := ml.NormalizeConfig{
- MinLength: normalizeMinLen,
- }
-
- return ml.NormalizeSeeds(db, cfg, os.Stdout)
-}
diff --git a/cmd/ml/cmd_probe.go b/cmd/ml/cmd_probe.go
deleted file mode 100644
index 68ed39ce..00000000
--- a/cmd/ml/cmd_probe.go
+++ /dev/null
@@ -1,66 +0,0 @@
-package ml
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- probeOutput string
-)
-
-var probeCmd = &cli.Command{
- Use: "probe",
- Short: "Run capability and content probes against a model",
- Long: "Runs 23 capability probes and 6 content probes against an OpenAI-compatible API.",
- RunE: runProbe,
-}
-
-func init() {
- probeCmd.Flags().StringVar(&probeOutput, "output", "", "Output JSON file for probe results")
-}
-
-func runProbe(cmd *cli.Command, args []string) error {
- if apiURL == "" {
- return fmt.Errorf("--api-url is required")
- }
-
- model := modelName
- if model == "" {
- model = "default"
- }
-
- ctx := context.Background()
- backend := ml.NewHTTPBackend(apiURL, model)
-
- fmt.Printf("Running %d capability probes against %s...\n", len(ml.CapabilityProbes), apiURL)
- results := ml.RunCapabilityProbes(ctx, backend)
-
- fmt.Printf("\nResults: %.1f%% (%d/%d)\n", results.Accuracy, results.Correct, results.Total)
-
- for cat, data := range results.ByCategory {
- catAcc := 0.0
- if data.Total > 0 {
- catAcc = float64(data.Correct) / float64(data.Total) * 100
- }
- fmt.Printf(" %-20s %d/%d (%.0f%%)\n", cat, data.Correct, data.Total, catAcc)
- }
-
- if probeOutput != "" {
- data, err := json.MarshalIndent(results, "", " ")
- if err != nil {
- return fmt.Errorf("marshal results: %w", err)
- }
- if err := os.WriteFile(probeOutput, data, 0644); err != nil {
- return fmt.Errorf("write output: %w", err)
- }
- fmt.Printf("\nResults written to %s\n", probeOutput)
- }
-
- return nil
-}
diff --git a/cmd/ml/cmd_publish.go b/cmd/ml/cmd_publish.go
deleted file mode 100644
index 2f0ff6ff..00000000
--- a/cmd/ml/cmd_publish.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package ml
-
-import (
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- publishInputDir string
- publishRepo string
- publishPublic bool
- publishToken string
- publishDryRun bool
-)
-
-var publishCmd = &cli.Command{
- Use: "publish",
- Short: "Upload Parquet dataset to HuggingFace Hub",
- Long: "Uploads train/valid/test Parquet files and an optional dataset card to a HuggingFace dataset repository.",
- RunE: runPublish,
-}
-
-func init() {
- publishCmd.Flags().StringVar(&publishInputDir, "input-dir", "", "Directory containing Parquet files (required)")
- publishCmd.Flags().StringVar(&publishRepo, "repo", "lthn/LEM-golden-set", "HuggingFace dataset repo ID")
- publishCmd.Flags().BoolVar(&publishPublic, "public", false, "Make dataset public")
- publishCmd.Flags().StringVar(&publishToken, "token", "", "HuggingFace API token (defaults to HF_TOKEN env)")
- publishCmd.Flags().BoolVar(&publishDryRun, "dry-run", false, "Show what would be uploaded without uploading")
- _ = publishCmd.MarkFlagRequired("input-dir")
-}
-
-func runPublish(cmd *cli.Command, args []string) error {
- return ml.Publish(ml.PublishConfig{
- InputDir: publishInputDir,
- Repo: publishRepo,
- Public: publishPublic,
- Token: publishToken,
- DryRun: publishDryRun,
- }, cmd.OutOrStdout())
-}
diff --git a/cmd/ml/cmd_query.go b/cmd/ml/cmd_query.go
deleted file mode 100644
index 00d49070..00000000
--- a/cmd/ml/cmd_query.go
+++ /dev/null
@@ -1,148 +0,0 @@
-package ml
-
-import (
- "encoding/json"
- "fmt"
- "os"
- "strings"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var queryCmd = &cli.Command{
- Use: "query [sql]",
- Short: "Run ad-hoc SQL against DuckDB",
- Long: "Executes arbitrary SQL against the DuckDB database. Non-SELECT queries are auto-wrapped as golden_set WHERE clauses.",
- Example: ` core ml query "SELECT COUNT(*) FROM golden_set"
- core ml query "domain = 'ethics'"
- core ml query --json "SHOW TABLES"`,
- Args: cli.MinimumNArgs(1),
- RunE: runQuery,
-}
-
-var queryJSON bool
-
-func init() {
- queryCmd.Flags().BoolVar(&queryJSON, "json", false, "Output as JSON")
-}
-
-func runQuery(cmd *cli.Command, args []string) error {
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB env is required")
- }
-
- db, err := ml.OpenDB(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- sql := strings.Join(args, " ")
-
- // Auto-wrap non-SELECT queries as golden_set WHERE clauses.
- trimmed := strings.TrimSpace(strings.ToUpper(sql))
- if !strings.HasPrefix(trimmed, "SELECT") && !strings.HasPrefix(trimmed, "SHOW") &&
- !strings.HasPrefix(trimmed, "DESCRIBE") && !strings.HasPrefix(trimmed, "EXPLAIN") {
- sql = "SELECT * FROM golden_set WHERE " + sql + " LIMIT 20"
- }
-
- rows, err := db.QueryRows(sql)
- if err != nil {
- return fmt.Errorf("query: %w", err)
- }
-
- if queryJSON {
- enc := json.NewEncoder(os.Stdout)
- enc.SetIndent("", " ")
- if err := enc.Encode(rows); err != nil {
- return fmt.Errorf("encode json: %w", err)
- }
- fmt.Fprintf(os.Stderr, "\n(%d rows)\n", len(rows))
- return nil
- }
-
- if len(rows) == 0 {
- fmt.Println("(0 rows)")
- return nil
- }
-
- // Collect column names in stable order from first row.
- var cols []string
- for col := range rows[0] {
- cols = append(cols, col)
- }
-
- // Calculate column widths (capped at 60).
- const maxWidth = 60
- widths := make([]int, len(cols))
- for i, col := range cols {
- widths[i] = len(col)
- }
- for _, row := range rows {
- for i, col := range cols {
- val := formatValue(row[col])
- if l := len(val); l > widths[i] {
- widths[i] = l
- }
- }
- }
- for i := range widths {
- if widths[i] > maxWidth {
- widths[i] = maxWidth
- }
- }
-
- // Print header.
- for i, col := range cols {
- if i > 0 {
- fmt.Print(" | ")
- }
- fmt.Printf("%-*s", widths[i], truncate(col, widths[i]))
- }
- fmt.Println()
-
- // Print separator.
- for i := range cols {
- if i > 0 {
- fmt.Print("-+-")
- }
- fmt.Print(strings.Repeat("-", widths[i]))
- }
- fmt.Println()
-
- // Print rows.
- for _, row := range rows {
- for i, col := range cols {
- if i > 0 {
- fmt.Print(" | ")
- }
- fmt.Printf("%-*s", widths[i], truncate(formatValue(row[col]), widths[i]))
- }
- fmt.Println()
- }
-
- fmt.Printf("\n(%d rows)\n", len(rows))
- return nil
-}
-
-func formatValue(v interface{}) string {
- if v == nil {
- return "NULL"
- }
- return fmt.Sprintf("%v", v)
-}
-
-func truncate(s string, max int) string {
- if len(s) <= max {
- return s
- }
- if max <= 3 {
- return s[:max]
- }
- return s[:max-3] + "..."
-}
diff --git a/cmd/ml/cmd_sandwich.go b/cmd/ml/cmd_sandwich.go
deleted file mode 100644
index 80acee4d..00000000
--- a/cmd/ml/cmd_sandwich.go
+++ /dev/null
@@ -1,238 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "log/slog"
- "os"
- "runtime"
- "time"
-
- "forge.lthn.ai/core/go-ml"
- "forge.lthn.ai/core/go/pkg/cli"
-)
-
-var sandwichCmd = &cli.Command{
- Use: "sandwich",
- Short: "Generate LEK training data using sandwich signing",
- Long: `Generates training data by wrapping seed prompts in a "sandwich" format:
-
- KB preamble (axioms framework) → seed prompt → LEK-1 kernel postfix
-
-Each seed prompt is sent to the local MLX model for inference, and the
-signed prompt + response pair is written as chat JSONL for 'core ml train'.
-
-The "sandwich" format embeds the ethical framework context around each
-prompt, teaching the model to reason from LEK principles naturally.
-
-Seed file format (JSON array):
- [{"id": "P01", "category": "sovereignty", "prompt": "...", "signal": "..."}]`,
- RunE: runSandwich,
-}
-
-var (
- sandwichModelPath string
- sandwichKB string
- sandwichKernel string
- sandwichSeeds string
- sandwichOutput string
- sandwichMaxTokens int
- sandwichTemp float64
- sandwichMemLimit int
- sandwichDryRun bool
-)
-
-func init() {
- sandwichCmd.Flags().StringVar(&sandwichModelPath, "model-path", "", "Path to model directory (required)")
- sandwichCmd.Flags().StringVar(&sandwichKB, "kb", "", "Knowledge base document (axioms markdown, required)")
- sandwichCmd.Flags().StringVar(&sandwichKernel, "kernel", "", "LEK-1 kernel file (required)")
- sandwichCmd.Flags().StringVar(&sandwichSeeds, "seeds", "", "Seed prompts JSON file (required)")
- sandwichCmd.Flags().StringVar(&sandwichOutput, "output", "sandwich.jsonl", "Output JSONL file")
- sandwichCmd.Flags().IntVar(&sandwichMaxTokens, "max-tokens", 1024, "Max tokens per response")
- sandwichCmd.Flags().Float64Var(&sandwichTemp, "temperature", 0.4, "Sampling temperature")
- sandwichCmd.Flags().IntVar(&sandwichMemLimit, "memory-limit", 24, "Metal memory limit in GB")
- sandwichCmd.Flags().BoolVar(&sandwichDryRun, "dry-run", false, "Output prompts only (no inference)")
- sandwichCmd.MarkFlagRequired("model-path")
- sandwichCmd.MarkFlagRequired("kernel")
- sandwichCmd.MarkFlagRequired("seeds")
- sandwichCmd.MarkFlagRequired("kb")
-}
-
-// seedPrompt is a single prompt from the seeds JSON file.
-type seedPrompt struct {
- ID string `json:"id"`
- Category string `json:"category"`
- Prompt string `json:"prompt"`
- Signal string `json:"signal"`
-}
-
-// sandwichOutput holds a single training example in messages format.
-type sandwichRecord struct {
- Messages []ml.Message `json:"messages"`
-}
-
-func runSandwich(cmd *cli.Command, args []string) error {
- start := time.Now()
-
- // Load KB document
- kbBytes, err := os.ReadFile(sandwichKB)
- if err != nil {
- return fmt.Errorf("read KB: %w", err)
- }
- kbText := string(kbBytes)
-
- // Load LEK-1 kernel
- kernelBytes, err := os.ReadFile(sandwichKernel)
- if err != nil {
- return fmt.Errorf("read kernel: %w", err)
- }
- kernelText := string(kernelBytes)
-
- // Load seed prompts
- seedBytes, err := os.ReadFile(sandwichSeeds)
- if err != nil {
- return fmt.Errorf("read seeds: %w", err)
- }
- var seeds []seedPrompt
- if err := json.Unmarshal(seedBytes, &seeds); err != nil {
- return fmt.Errorf("parse seeds: %w", err)
- }
-
- slog.Info("sandwich: loaded inputs",
- "kb_chars", len(kbText),
- "kernel_chars", len(kernelText),
- "seeds", len(seeds),
- )
-
- if len(seeds) == 0 {
- return fmt.Errorf("no seed prompts found")
- }
-
- // Open output file
- outFile, err := os.Create(sandwichOutput)
- if err != nil {
- return fmt.Errorf("create output: %w", err)
- }
- defer outFile.Close()
- encoder := json.NewEncoder(outFile)
-
- // Dry-run mode: output prompts without inference
- if sandwichDryRun {
- for _, seed := range seeds {
- signedPrompt := buildSandwich(kbText, seed.Prompt, kernelText)
- record := sandwichRecord{
- Messages: []ml.Message{
- {Role: "user", Content: signedPrompt},
- },
- }
- if err := encoder.Encode(record); err != nil {
- return fmt.Errorf("write record: %w", err)
- }
- }
- slog.Info("sandwich: dry-run complete",
- "output", sandwichOutput,
- "prompts", len(seeds),
- )
- return nil
- }
-
- // Load MLX model
- slog.Info("sandwich: loading model", "path", sandwichModelPath)
- backend, err := ml.NewMLXBackend(sandwichModelPath)
- if err != nil {
- return fmt.Errorf("load model: %w", err)
- }
-
- opts := ml.GenOpts{
- Temperature: sandwichTemp,
- MaxTokens: sandwichMaxTokens,
- }
-
- var totalTokenTime time.Duration
- generated := 0
-
- for i, seed := range seeds {
- seedStart := time.Now()
-
- // Build the sandwich: KB + prompt + kernel
- signedPrompt := buildSandwich(kbText, seed.Prompt, kernelText)
-
- // Send as a user message for chat-style generation
- messages := []ml.Message{
- {Role: "user", Content: signedPrompt},
- }
-
- slog.Info("sandwich: generating",
- "seed", fmt.Sprintf("%d/%d", i+1, len(seeds)),
- "id", seed.ID,
- "category", seed.Category,
- )
-
- // Generate response
- response, err := backend.Chat(context.Background(), messages, opts)
- if err != nil {
- slog.Error("sandwich: generation failed",
- "id", seed.ID,
- "error", err,
- )
- continue
- }
-
- elapsed := time.Since(seedStart)
- totalTokenTime += elapsed
-
- // Write training record
- record := sandwichRecord{
- Messages: []ml.Message{
- {Role: "user", Content: signedPrompt},
- {Role: "assistant", Content: response},
- },
- }
- if err := encoder.Encode(record); err != nil {
- return fmt.Errorf("write record: %w", err)
- }
-
- generated++
- slog.Info("sandwich: generated",
- "id", seed.ID,
- "category", seed.Category,
- "response_chars", len(response),
- "duration", elapsed.Round(time.Second),
- )
-
- // Periodic cleanup
- if (i+1)%4 == 0 {
- runtime.GC()
- }
- }
-
- slog.Info("sandwich: complete",
- "output", sandwichOutput,
- "generated", generated,
- "total", len(seeds),
- "duration", time.Since(start).Round(time.Second),
- "avg_per_seed", (totalTokenTime / time.Duration(max(generated, 1))).Round(time.Second),
- )
-
- return nil
-}
-
-// buildSandwich constructs the signed prompt: KB preamble + seed prompt + LEK-1 kernel.
-func buildSandwich(kb, prompt, kernel string) string {
- return fmt.Sprintf(`Name: Ethics Experiment
-KB:
-%s
-
----
-
-%s
-
----
-
-%s
-
-Remember: respond using the ethical framework above. Do not reference the framework directly — reason from its principles naturally.`, kb, prompt, kernel)
-}
diff --git a/cmd/ml/cmd_sandwich_init.go b/cmd/ml/cmd_sandwich_init.go
deleted file mode 100644
index d23b8856..00000000
--- a/cmd/ml/cmd_sandwich_init.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-func init() {
- mlCmd.AddCommand(sandwichCmd)
-}
diff --git a/cmd/ml/cmd_score.go b/cmd/ml/cmd_score.go
deleted file mode 100644
index 7c0cc25a..00000000
--- a/cmd/ml/cmd_score.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package ml
-
-import (
- "context"
- "fmt"
- "time"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- scoreInput string
- scoreSuites string
- scoreOutput string
- scoreConcur int
-)
-
-var scoreCmd = &cli.Command{
- Use: "score",
- Short: "Score responses with heuristic and LLM judges",
- Long: "Reads a JSONL file of prompt/response pairs and scores them across configured suites.",
- RunE: runScore,
-}
-
-func init() {
- scoreCmd.Flags().StringVar(&scoreInput, "input", "", "Input JSONL file with prompt/response pairs (required)")
- scoreCmd.Flags().StringVar(&scoreSuites, "suites", "all", "Comma-separated scoring suites (heuristic,semantic,content,exact,truthfulqa,donotanswer,toxigen)")
- scoreCmd.Flags().StringVar(&scoreOutput, "output", "", "Output JSON file for scores")
- scoreCmd.Flags().IntVar(&scoreConcur, "concurrency", 4, "Number of concurrent scoring workers")
- scoreCmd.MarkFlagRequired("input")
-}
-
-func runScore(cmd *cli.Command, args []string) error {
- responses, err := ml.ReadResponses(scoreInput)
- if err != nil {
- return fmt.Errorf("read input: %w", err)
- }
-
- var judge *ml.Judge
- if judgeURL != "" {
- backend := ml.NewHTTPBackend(judgeURL, judgeModel)
- judge = ml.NewJudge(backend)
- }
-
- engine := ml.NewEngine(judge, scoreConcur, scoreSuites)
-
- ctx := context.Background()
- perPrompt := engine.ScoreAll(ctx, responses)
- averages := ml.ComputeAverages(perPrompt)
-
- if scoreOutput != "" {
- output := &ml.ScorerOutput{
- Metadata: ml.Metadata{
- JudgeModel: judgeModel,
- JudgeURL: judgeURL,
- ScoredAt: time.Now(),
- Suites: ml.SplitComma(scoreSuites),
- },
- ModelAverages: averages,
- PerPrompt: perPrompt,
- }
- if err := ml.WriteScores(scoreOutput, output); err != nil {
- return fmt.Errorf("write output: %w", err)
- }
- fmt.Printf("Scores written to %s\n", scoreOutput)
- } else {
- for model, avgs := range averages {
- fmt.Printf("%s:\n", model)
- for field, val := range avgs {
- fmt.Printf(" %-25s %.3f\n", field, val)
- }
- }
- }
-
- return nil
-}
diff --git a/cmd/ml/cmd_seed_influx.go b/cmd/ml/cmd_seed_influx.go
deleted file mode 100644
index 30ece12e..00000000
--- a/cmd/ml/cmd_seed_influx.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var seedInfluxCmd = &cli.Command{
- Use: "seed-influx",
- Short: "Seed InfluxDB golden_gen from DuckDB golden_set",
- Long: "One-time migration: batch-loads DuckDB golden_set records into InfluxDB golden_gen measurement.",
- RunE: runSeedInflux,
-}
-
-var (
- seedInfluxForce bool
- seedInfluxBatchSize int
-)
-
-func init() {
- seedInfluxCmd.Flags().BoolVar(&seedInfluxForce, "force", false, "Re-seed even if InfluxDB already has data")
- seedInfluxCmd.Flags().IntVar(&seedInfluxBatchSize, "batch-size", 500, "Lines per InfluxDB write batch")
-}
-
-func runSeedInflux(cmd *cli.Command, args []string) error {
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
- if path == "" {
- return fmt.Errorf("--db or LEM_DB required")
- }
-
- db, err := ml.OpenDB(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- influx := ml.NewInfluxClient(influxURL, influxDB)
-
- return ml.SeedInflux(db, influx, ml.SeedInfluxConfig{
- Force: seedInfluxForce,
- BatchSize: seedInfluxBatchSize,
- }, os.Stdout)
-}
diff --git a/cmd/ml/cmd_sequence.go b/cmd/ml/cmd_sequence.go
deleted file mode 100644
index 2b56c1af..00000000
--- a/cmd/ml/cmd_sequence.go
+++ /dev/null
@@ -1,326 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-import (
- "encoding/json"
- "fmt"
- "log/slog"
- "os"
- "path/filepath"
- "strings"
- "time"
-
- "forge.lthn.ai/core/go-ml"
- "forge.lthn.ai/core/go/pkg/cli"
- "gopkg.in/yaml.v3"
-)
-
-var sequenceCmd = &cli.Command{
- Use: "sequence",
- Short: "Run a training sequence of multiple lessons",
- Long: `Runs an ordered sequence of lessons defined in a YAML file.
-
-Sequence YAML format:
- id: lek-full
- title: "LEK Full Training Sequence"
- mode: vertical
- model-path: /path/to/model
- lessons:
- - sovereignty.yaml
- - privacy.yaml
- - censorship.yaml
-
-Mode:
- vertical Run lessons strictly in order (default)
- horizontal Run all lessons, order doesn't matter
-
-State is tracked per-sequence so runs can be resumed.`,
- RunE: runSequence,
-}
-
-var (
- sequenceFile string
- sequenceModelPath string
- sequenceOutput string
- sequenceMaxTokens int
- sequenceTemp float64
- sequenceMemLimit int
-)
-
-func init() {
- sequenceCmd.Flags().StringVar(&sequenceFile, "file", "", "Sequence YAML file (required)")
- sequenceCmd.Flags().StringVar(&sequenceModelPath, "model-path", "", "Path to model directory (required)")
- sequenceCmd.Flags().StringVar(&sequenceOutput, "output", "", "Output JSONL file (default: .jsonl)")
- sequenceCmd.Flags().IntVar(&sequenceMaxTokens, "max-tokens", 1024, "Max tokens per response")
- sequenceCmd.Flags().Float64Var(&sequenceTemp, "temperature", 0.4, "Sampling temperature")
- sequenceCmd.Flags().IntVar(&sequenceMemLimit, "memory-limit", 24, "Metal memory limit in GB")
- sequenceCmd.MarkFlagRequired("file")
- sequenceCmd.MarkFlagRequired("model-path")
-}
-
-// sequenceDef is a YAML sequence definition.
-type sequenceDef struct {
- ID string `yaml:"id"`
- Title string `yaml:"title"`
- Mode string `yaml:"mode"` // "vertical" (default) or "horizontal"
- ModelPath string `yaml:"model-path"`
- Lessons []string `yaml:"lessons"` // Relative paths to lesson YAML files
-}
-
-// sequenceState tracks progress through a sequence.
-type sequenceState struct {
- SequenceID string `json:"sequence_id"`
- Completed map[string]bool `json:"completed"` // lesson ID → done
- Current string `json:"current"`
- UpdatedAt string `json:"updated_at"`
-}
-
-func runSequence(cmd *cli.Command, args []string) error {
- start := time.Now()
-
- // Load sequence YAML
- data, err := os.ReadFile(sequenceFile)
- if err != nil {
- return fmt.Errorf("read sequence: %w", err)
- }
-
- var seq sequenceDef
- if err := yaml.Unmarshal(data, &seq); err != nil {
- return fmt.Errorf("parse sequence: %w", err)
- }
-
- if seq.ID == "" {
- seq.ID = strings.TrimSuffix(filepath.Base(sequenceFile), filepath.Ext(sequenceFile))
- }
- if seq.Mode == "" {
- seq.Mode = "vertical"
- }
-
- // Model path from sequence or flag
- modelPath := sequenceModelPath
- if modelPath == "" && seq.ModelPath != "" {
- modelPath = seq.ModelPath
- }
- if modelPath == "" {
- return fmt.Errorf("model-path is required (flag or sequence YAML)")
- }
-
- // Resolve output
- if sequenceOutput == "" {
- sequenceOutput = seq.ID + ".jsonl"
- }
-
- slog.Info("sequence: loaded",
- "id", seq.ID,
- "title", seq.Title,
- "mode", seq.Mode,
- "lessons", len(seq.Lessons),
- )
-
- // Load state
- stateFile := seq.ID + ".sequence-state.json"
- state := loadSequenceState(stateFile)
- if state.SequenceID == "" {
- state.SequenceID = seq.ID
- state.Completed = make(map[string]bool)
- }
-
- // Load model once for all lessons
- slog.Info("sequence: loading model", "path", modelPath)
- backend, err := ml.NewMLXBackend(modelPath)
- if err != nil {
- return fmt.Errorf("load model: %w", err)
- }
-
- opts := ml.GenOpts{
- Temperature: sequenceTemp,
- MaxTokens: sequenceMaxTokens,
- }
-
- // Open output file
- outFile, err := os.OpenFile(sequenceOutput, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
- if err != nil {
- return fmt.Errorf("create output: %w", err)
- }
- defer outFile.Close()
- encoder := json.NewEncoder(outFile)
-
- baseDir := filepath.Dir(sequenceFile)
- totalGenerated := 0
-
- for i, lessonPath := range seq.Lessons {
- // Resolve lesson path
- if !filepath.IsAbs(lessonPath) {
- lessonPath = filepath.Join(baseDir, lessonPath)
- }
-
- // Load lesson
- lessonData, err := os.ReadFile(lessonPath)
- if err != nil {
- slog.Error("sequence: failed to read lesson",
- "path", lessonPath,
- "error", err,
- )
- if seq.Mode == "vertical" {
- return fmt.Errorf("vertical sequence halted: %w", err)
- }
- continue
- }
-
- var lesson lessonDef
- if err := yaml.Unmarshal(lessonData, &lesson); err != nil {
- slog.Error("sequence: failed to parse lesson",
- "path", lessonPath,
- "error", err,
- )
- if seq.Mode == "vertical" {
- return fmt.Errorf("vertical sequence halted: %w", err)
- }
- continue
- }
-
- if lesson.ID == "" {
- lesson.ID = strings.TrimSuffix(filepath.Base(lessonPath), filepath.Ext(lessonPath))
- }
-
- // Skip completed lessons
- if state.Completed[lesson.ID] {
- slog.Info("sequence: skipping completed lesson",
- "lesson", fmt.Sprintf("%d/%d", i+1, len(seq.Lessons)),
- "id", lesson.ID,
- )
- continue
- }
-
- state.Current = lesson.ID
-
- slog.Info("sequence: starting lesson",
- "lesson", fmt.Sprintf("%d/%d", i+1, len(seq.Lessons)),
- "id", lesson.ID,
- "title", lesson.Title,
- "prompts", len(lesson.Prompts),
- )
-
- // Load sandwich files for this lesson
- var kbText, kernelText string
- hasSandwich := false
- if lesson.Sandwich != nil {
- lessonDir := filepath.Dir(lessonPath)
- if lesson.Sandwich.KB != "" {
- kbPath := lesson.Sandwich.KB
- if !filepath.IsAbs(kbPath) {
- kbPath = filepath.Join(lessonDir, kbPath)
- }
- d, err := os.ReadFile(kbPath)
- if err != nil {
- slog.Error("sequence: failed to read KB", "error", err)
- } else {
- kbText = string(d)
- }
- }
- if lesson.Sandwich.Kernel != "" {
- kernelPath := lesson.Sandwich.Kernel
- if !filepath.IsAbs(kernelPath) {
- kernelPath = filepath.Join(lessonDir, kernelPath)
- }
- d, err := os.ReadFile(kernelPath)
- if err != nil {
- slog.Error("sequence: failed to read kernel", "error", err)
- } else {
- kernelText = string(d)
- }
- }
- hasSandwich = kbText != "" && kernelText != ""
- }
-
- // Run each prompt in the lesson
- generated := 0
- for j, prompt := range lesson.Prompts {
- var messages []ml.Message
- if lesson.System != "" {
- messages = append(messages, ml.Message{Role: "system", Content: lesson.System})
- }
-
- userContent := prompt.Prompt
- if hasSandwich {
- userContent = buildSandwich(kbText, prompt.Prompt, kernelText)
- }
- messages = append(messages, ml.Message{Role: "user", Content: userContent})
-
- slog.Info("sequence: generating",
- "lesson", lesson.ID,
- "prompt", fmt.Sprintf("%d/%d", j+1, len(lesson.Prompts)),
- "id", prompt.ID,
- )
-
- response, err := backend.Chat(cmd.Context(), messages, opts)
- if err != nil {
- slog.Error("sequence: generation failed",
- "lesson", lesson.ID,
- "prompt", prompt.ID,
- "error", err,
- )
- continue
- }
-
- record := struct {
- Messages []ml.Message `json:"messages"`
- }{
- Messages: []ml.Message{
- {Role: "user", Content: userContent},
- {Role: "assistant", Content: response},
- },
- }
- if err := encoder.Encode(record); err != nil {
- return fmt.Errorf("write record: %w", err)
- }
-
- generated++
- totalGenerated++
- }
-
- // Mark lesson complete
- state.Completed[lesson.ID] = true
- state.UpdatedAt = time.Now().Format(time.RFC3339)
- saveSequenceState(stateFile, state)
-
- slog.Info("sequence: lesson complete",
- "id", lesson.ID,
- "generated", generated,
- "total", len(lesson.Prompts),
- )
- }
-
- state.Current = ""
- state.UpdatedAt = time.Now().Format(time.RFC3339)
- saveSequenceState(stateFile, state)
-
- slog.Info("sequence: complete",
- "id", seq.ID,
- "output", sequenceOutput,
- "total_generated", totalGenerated,
- "lessons_completed", len(state.Completed),
- "duration", time.Since(start).Round(time.Second),
- )
-
- return nil
-}
-
-func loadSequenceState(path string) sequenceState {
- data, err := os.ReadFile(path)
- if err != nil {
- return sequenceState{}
- }
- var state sequenceState
- json.Unmarshal(data, &state)
- return state
-}
-
-func saveSequenceState(path string, state sequenceState) {
- data, err := json.MarshalIndent(state, "", " ")
- if err != nil {
- return
- }
- os.WriteFile(path, data, 0644)
-}
diff --git a/cmd/ml/cmd_serve.go b/cmd/ml/cmd_serve.go
deleted file mode 100644
index 737c7a18..00000000
--- a/cmd/ml/cmd_serve.go
+++ /dev/null
@@ -1,472 +0,0 @@
-package ml
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "io"
- "log/slog"
- "net/http"
- "os"
- "os/signal"
- "runtime"
- "sync/atomic"
- "syscall"
- "time"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var serveCmd = &cli.Command{
- Use: "serve",
- Short: "Start OpenAI-compatible inference server",
- Long: "Starts an HTTP server serving /v1/completions and /v1/chat/completions using the configured ML backend.",
- RunE: runServe,
-}
-
-var (
- serveBind string
- serveModelPath string
- serveThreads int
- serveMaxTokens int
- serveTimeout int
- serveMaxRequests int
- serveMaxContext int
-)
-
-func init() {
- serveCmd.Flags().StringVar(&serveBind, "bind", "0.0.0.0:8090", "Address to bind")
- serveCmd.Flags().StringVar(&serveModelPath, "model-path", "", "Path to model directory (for mlx backend)")
- serveCmd.Flags().IntVar(&serveThreads, "threads", 0, "Max CPU threads (0 = all available)")
- serveCmd.Flags().IntVar(&serveMaxTokens, "max-tokens", 4096, "Default max tokens per request")
- serveCmd.Flags().IntVar(&serveTimeout, "timeout", 300, "Request timeout in seconds")
- serveCmd.Flags().IntVar(&serveMaxRequests, "max-requests", 1, "Max concurrent requests (Metal is single-stream)")
- serveCmd.Flags().IntVar(&serveMaxContext, "max-context", 4, "Max chat messages to keep (sliding window, 0=unlimited)")
-}
-
-type completionRequest struct {
- Model string `json:"model"`
- Prompt string `json:"prompt"`
- MaxTokens int `json:"max_tokens"`
- Temperature float64 `json:"temperature"`
- Stream bool `json:"stream"`
-}
-
-type completionResponse struct {
- ID string `json:"id"`
- Object string `json:"object"`
- Created int64 `json:"created"`
- Model string `json:"model"`
- Choices []completionChoice `json:"choices"`
- Usage usageInfo `json:"usage"`
-}
-
-type completionChoice struct {
- Text string `json:"text"`
- Index int `json:"index"`
- FinishReason string `json:"finish_reason"`
-}
-
-type chatRequest struct {
- Model string `json:"model"`
- Messages []ml.Message `json:"messages"`
- MaxTokens int `json:"max_tokens"`
- Temperature float64 `json:"temperature"`
- Stream bool `json:"stream"`
-}
-
-type chatResponse struct {
- ID string `json:"id"`
- Object string `json:"object"`
- Created int64 `json:"created"`
- Model string `json:"model"`
- Choices []chatChoice `json:"choices"`
-}
-
-type chatChoice struct {
- Message ml.Message `json:"message"`
- Index int `json:"index"`
- FinishReason string `json:"finish_reason"`
-}
-
-// SSE streaming types (OpenAI chunk format)
-type chatChunkResponse struct {
- ID string `json:"id"`
- Object string `json:"object"`
- Created int64 `json:"created"`
- Model string `json:"model"`
- Choices []chatChunkChoice `json:"choices"`
-}
-
-type chatChunkChoice struct {
- Delta chatChunkDelta `json:"delta"`
- Index int `json:"index"`
- FinishReason *string `json:"finish_reason"`
-}
-
-type chatChunkDelta struct {
- Role string `json:"role,omitempty"`
- Content string `json:"content,omitempty"`
-}
-
-type completionChunkResponse struct {
- ID string `json:"id"`
- Object string `json:"object"`
- Created int64 `json:"created"`
- Model string `json:"model"`
- Choices []completionChunkChoice `json:"choices"`
-}
-
-type completionChunkChoice struct {
- Text string `json:"text"`
- Index int `json:"index"`
- FinishReason *string `json:"finish_reason"`
-}
-
-type usageInfo struct {
- PromptTokens int `json:"prompt_tokens"`
- CompletionTokens int `json:"completion_tokens"`
- TotalTokens int `json:"total_tokens"`
-}
-
-func runServe(cmd *cli.Command, args []string) error {
- // Cap CPU threads
- if serveThreads > 0 {
- prev := runtime.GOMAXPROCS(serveThreads)
- slog.Info("ml serve: capped threads", "threads", serveThreads, "previous", prev)
- }
-
- backend, err := createServeBackend()
- if err != nil {
- return err
- }
-
- // Check if backend supports streaming
- streamer, canStream := backend.(ml.StreamingBackend)
-
- // Request tracking
- var activeRequests atomic.Int32
- startTime := time.Now()
-
- mux := http.NewServeMux()
-
- // Health endpoint
- mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]any{
- "status": "ok",
- "model": backend.Name(),
- "uptime_seconds": int(time.Since(startTime).Seconds()),
- "active_requests": activeRequests.Load(),
- "max_threads": runtime.GOMAXPROCS(0),
- "max_tokens": serveMaxTokens,
- "max_context": serveMaxContext,
- })
- })
-
- mux.HandleFunc("POST /v1/completions", func(w http.ResponseWriter, r *http.Request) {
- // Concurrency gate
- if int(activeRequests.Load()) >= serveMaxRequests {
- http.Error(w, `{"error":"server busy, max concurrent requests reached"}`, http.StatusTooManyRequests)
- return
- }
- activeRequests.Add(1)
- defer activeRequests.Add(-1)
-
- // Request timeout
- ctx, cancel := context.WithTimeout(r.Context(), time.Duration(serveTimeout)*time.Second)
- defer cancel()
- r = r.WithContext(ctx)
-
- body, _ := io.ReadAll(r.Body)
- var req completionRequest
- if err := json.Unmarshal(body, &req); err != nil {
- http.Error(w, err.Error(), 400)
- return
- }
-
- // Enforce server-level max-tokens cap
- if req.MaxTokens == 0 || req.MaxTokens > serveMaxTokens {
- req.MaxTokens = serveMaxTokens
- }
-
- opts := ml.GenOpts{
- Temperature: req.Temperature,
- MaxTokens: req.MaxTokens,
- Model: req.Model,
- }
-
- // Streaming path
- if req.Stream && canStream {
- id := fmt.Sprintf("cmpl-%d", time.Now().UnixNano())
- created := time.Now().Unix()
-
- w.Header().Set("Content-Type", "text/event-stream")
- w.Header().Set("Cache-Control", "no-cache")
- w.Header().Set("Connection", "keep-alive")
- w.Header().Set("X-Accel-Buffering", "no")
- flusher, ok := w.(http.Flusher)
- if !ok {
- http.Error(w, "streaming not supported", 500)
- return
- }
-
- err := streamer.GenerateStream(r.Context(), req.Prompt, opts, func(token string) error {
- chunk := completionChunkResponse{
- ID: id,
- Object: "text_completion",
- Created: created,
- Model: backend.Name(),
- Choices: []completionChunkChoice{{Text: token}},
- }
- data, _ := json.Marshal(chunk)
- fmt.Fprintf(w, "data: %s\n\n", data)
- flusher.Flush()
- return nil
- })
-
- if err != nil {
- slog.Error("stream error", "err", err)
- }
-
- // Send final chunk with finish_reason
- stop := "stop"
- final := completionChunkResponse{
- ID: id,
- Object: "text_completion",
- Created: created,
- Model: backend.Name(),
- Choices: []completionChunkChoice{{FinishReason: &stop}},
- }
- data, _ := json.Marshal(final)
- fmt.Fprintf(w, "data: %s\n\n", data)
- fmt.Fprintf(w, "data: [DONE]\n\n")
- flusher.Flush()
- return
- }
-
- // Non-streaming path
- text, err := backend.Generate(r.Context(), req.Prompt, opts)
- if err != nil {
- http.Error(w, err.Error(), 500)
- return
- }
-
- resp := completionResponse{
- ID: fmt.Sprintf("cmpl-%d", time.Now().UnixNano()),
- Object: "text_completion",
- Created: time.Now().Unix(),
- Model: backend.Name(),
- Choices: []completionChoice{{Text: text, FinishReason: "stop"}},
- }
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(resp)
- })
-
- mux.HandleFunc("POST /v1/chat/completions", func(w http.ResponseWriter, r *http.Request) {
- // Concurrency gate
- if int(activeRequests.Load()) >= serveMaxRequests {
- http.Error(w, `{"error":"server busy, max concurrent requests reached"}`, http.StatusTooManyRequests)
- return
- }
- activeRequests.Add(1)
- defer activeRequests.Add(-1)
-
- // Request timeout
- ctx, cancel := context.WithTimeout(r.Context(), time.Duration(serveTimeout)*time.Second)
- defer cancel()
- r = r.WithContext(ctx)
-
- body, _ := io.ReadAll(r.Body)
- var req chatRequest
- if err := json.Unmarshal(body, &req); err != nil {
- http.Error(w, err.Error(), 400)
- return
- }
-
- // Enforce server-level max-tokens cap
- if req.MaxTokens == 0 || req.MaxTokens > serveMaxTokens {
- req.MaxTokens = serveMaxTokens
- }
-
- // Sliding window: keep system prompt (if any) + last N messages
- // Prevents KV-cache explosion on multi-turn conversations
- if serveMaxContext > 0 && len(req.Messages) > serveMaxContext {
- var kept []ml.Message
- rest := req.Messages
- // Preserve system message if present
- if len(rest) > 0 && rest[0].Role == "system" {
- kept = append(kept, rest[0])
- rest = rest[1:]
- }
- // Keep only the last N user/assistant messages
- if len(rest) > serveMaxContext {
- rest = rest[len(rest)-serveMaxContext:]
- }
- req.Messages = append(kept, rest...)
- slog.Debug("ml serve: context window applied", "kept", len(req.Messages))
- }
-
- opts := ml.GenOpts{
- Temperature: req.Temperature,
- MaxTokens: req.MaxTokens,
- Model: req.Model,
- }
-
- // Streaming path
- if req.Stream && canStream {
- id := fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano())
- created := time.Now().Unix()
-
- w.Header().Set("Content-Type", "text/event-stream")
- w.Header().Set("Cache-Control", "no-cache")
- w.Header().Set("Connection", "keep-alive")
- w.Header().Set("X-Accel-Buffering", "no")
- flusher, ok := w.(http.Flusher)
- if !ok {
- http.Error(w, "streaming not supported", 500)
- return
- }
-
- // Send initial role chunk
- roleChunk := chatChunkResponse{
- ID: id,
- Object: "chat.completion.chunk",
- Created: created,
- Model: backend.Name(),
- Choices: []chatChunkChoice{{Delta: chatChunkDelta{Role: "assistant"}}},
- }
- data, _ := json.Marshal(roleChunk)
- fmt.Fprintf(w, "data: %s\n\n", data)
- flusher.Flush()
-
- err := streamer.ChatStream(r.Context(), req.Messages, opts, func(token string) error {
- chunk := chatChunkResponse{
- ID: id,
- Object: "chat.completion.chunk",
- Created: created,
- Model: backend.Name(),
- Choices: []chatChunkChoice{{Delta: chatChunkDelta{Content: token}}},
- }
- data, _ := json.Marshal(chunk)
- fmt.Fprintf(w, "data: %s\n\n", data)
- flusher.Flush()
- return nil
- })
-
- if err != nil {
- slog.Error("stream error", "err", err)
- }
-
- // Send final chunk with finish_reason
- stop := "stop"
- final := chatChunkResponse{
- ID: id,
- Object: "chat.completion.chunk",
- Created: created,
- Model: backend.Name(),
- Choices: []chatChunkChoice{{Delta: chatChunkDelta{}, FinishReason: &stop}},
- }
- data, _ = json.Marshal(final)
- fmt.Fprintf(w, "data: %s\n\n", data)
- fmt.Fprintf(w, "data: [DONE]\n\n")
- flusher.Flush()
- return
- }
-
- // Non-streaming path
- text, err := backend.Chat(r.Context(), req.Messages, opts)
- if err != nil {
- http.Error(w, err.Error(), 500)
- return
- }
-
- resp := chatResponse{
- ID: fmt.Sprintf("chatcmpl-%d", time.Now().UnixNano()),
- Object: "chat.completion",
- Created: time.Now().Unix(),
- Model: backend.Name(),
- Choices: []chatChoice{{
- Message: ml.Message{Role: "assistant", Content: text},
- FinishReason: "stop",
- }},
- }
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(resp)
- })
-
- mux.HandleFunc("GET /v1/models", func(w http.ResponseWriter, r *http.Request) {
- resp := struct {
- Object string `json:"object"`
- Data []struct {
- ID string `json:"id"`
- } `json:"data"`
- }{
- Object: "list",
- Data: []struct {
- ID string `json:"id"`
- }{{ID: backend.Name()}},
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(resp)
- })
-
- // Serve the lem-chat UI at root — same origin, no CORS needed
- mux.HandleFunc("GET /chat.js", func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/javascript")
- w.Write(lemChatJS)
- })
-
- mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
- if r.URL.Path != "/" {
- http.NotFound(w, r)
- return
- }
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprintf(w, chatHTML, backend.Name(), serveMaxTokens)
- })
-
- slog.Info("ml serve: starting",
- "bind", serveBind,
- "backend", backend.Name(),
- "streaming", canStream,
- "threads", runtime.GOMAXPROCS(0),
- "max_tokens", serveMaxTokens,
- "max_context_msgs", serveMaxContext,
- "timeout_s", serveTimeout,
- "max_requests", serveMaxRequests,
- )
- fmt.Printf("Serving on http://%s\n", serveBind)
-
- // Graceful shutdown on SIGINT/SIGTERM
- srv := &http.Server{
- Addr: serveBind,
- Handler: mux,
- }
-
- errCh := make(chan error, 1)
- go func() {
- errCh <- srv.ListenAndServe()
- }()
-
- sigCh := make(chan os.Signal, 1)
- signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
-
- select {
- case sig := <-sigCh:
- slog.Info("ml serve: shutting down", "signal", sig)
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
- defer cancel()
- if err := srv.Shutdown(ctx); err != nil {
- slog.Error("ml serve: shutdown error", "err", err)
- return err
- }
- slog.Info("ml serve: stopped cleanly")
- return nil
- case err := <-errCh:
- return err
- }
-}
diff --git a/cmd/ml/cmd_status.go b/cmd/ml/cmd_status.go
deleted file mode 100644
index 0c6c08f7..00000000
--- a/cmd/ml/cmd_status.go
+++ /dev/null
@@ -1,54 +0,0 @@
-package ml
-
-import (
- "fmt"
- "os"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var statusCmd = &cli.Command{
- Use: "status",
- Short: "Show training and generation progress",
- Long: "Queries InfluxDB for training status, loss, and generation progress. Optionally shows DuckDB table counts.",
- RunE: runStatus,
-}
-
-func runStatus(cmd *cli.Command, args []string) error {
- influx := ml.NewInfluxClient(influxURL, influxDB)
-
- if err := ml.PrintStatus(influx, os.Stdout); err != nil {
- return fmt.Errorf("status: %w", err)
- }
-
- path := dbPath
- if path == "" {
- path = os.Getenv("LEM_DB")
- }
-
- if path != "" {
- db, err := ml.OpenDB(path)
- if err != nil {
- return fmt.Errorf("open db: %w", err)
- }
- defer db.Close()
-
- counts, err := db.TableCounts()
- if err != nil {
- return fmt.Errorf("table counts: %w", err)
- }
-
- fmt.Println()
- fmt.Println("DuckDB:")
- order := []string{"golden_set", "expansion_prompts", "seeds", "training_examples",
- "prompts", "gemini_responses", "benchmark_questions", "benchmark_results", "validations"}
- for _, table := range order {
- if count, ok := counts[table]; ok {
- fmt.Fprintf(os.Stdout, " %-22s %6d rows\n", table, count)
- }
- }
- }
-
- return nil
-}
diff --git a/cmd/ml/cmd_train.go b/cmd/ml/cmd_train.go
deleted file mode 100644
index 9df4bf0d..00000000
--- a/cmd/ml/cmd_train.go
+++ /dev/null
@@ -1,358 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-import (
- "bufio"
- "encoding/json"
- "fmt"
- "log/slog"
- "os"
- "runtime"
- "strings"
- "time"
-
- "forge.lthn.ai/core/go-ml"
- "forge.lthn.ai/core/go-mlx"
- "forge.lthn.ai/core/go-ai/mlx/model"
- "forge.lthn.ai/core/go-ai/mlx/tokenizer"
- "forge.lthn.ai/core/go/pkg/cli"
-)
-
-var trainCmd = &cli.Command{
- Use: "train",
- Short: "LoRA fine-tune a model on JSONL training data",
- Long: `Fine-tunes a local MLX model using LoRA (Low-Rank Adaptation).
-
-Reads chat-format JSONL training data and trains LoRA adapter weights
-using AdamW optimiser with cross-entropy loss on assistant tokens only.
-
-Training data format (one JSON object per line):
- {"messages": [{"role": "system", "content": "..."}, {"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]}`,
- RunE: runTrain,
-}
-
-var (
- trainModelPath string
- trainData string
- trainOutput string
- trainRank int
- trainAlpha float64
- trainLR float64
- trainEpochs int
- trainMaxSeqLen int
- trainTargets string
- trainMemoryLimit int
-)
-
-func init() {
- trainCmd.Flags().StringVar(&trainModelPath, "model-path", "", "Path to model directory (required)")
- trainCmd.Flags().StringVar(&trainData, "data", "", "Training JSONL file (required)")
- trainCmd.Flags().StringVar(&trainOutput, "output", "adapters.safetensors", "Output adapter file")
- trainCmd.Flags().IntVar(&trainRank, "rank", 8, "LoRA decomposition rank")
- trainCmd.Flags().Float64Var(&trainAlpha, "alpha", 16, "LoRA scaling factor")
- trainCmd.Flags().Float64Var(&trainLR, "lr", 1e-4, "Learning rate")
- trainCmd.Flags().IntVar(&trainEpochs, "epochs", 1, "Number of training epochs")
- trainCmd.Flags().IntVar(&trainMaxSeqLen, "max-seq-len", 512, "Maximum sequence length (tokens)")
- trainCmd.Flags().StringVar(&trainTargets, "targets", "q_proj,v_proj", "Comma-separated projection targets for LoRA")
- trainCmd.Flags().IntVar(&trainMemoryLimit, "memory-limit", 24, "Metal memory limit in GB")
- trainCmd.MarkFlagRequired("model-path")
- trainCmd.MarkFlagRequired("data")
-}
-
-// trainSample holds a tokenised training example.
-type trainSample struct {
- Tokens []int32 // Full token sequence
- Mask []int32 // 1 for assistant tokens, 0 for prompt tokens
-}
-
-func runTrain(cmd *cli.Command, args []string) error {
- start := time.Now()
-
- // --- Load model ---
- slog.Info("loading model", "path", trainModelPath)
- m, err := model.LoadModel(trainModelPath)
- if err != nil {
- return fmt.Errorf("load model: %w", err)
- }
-
- mlx.SetCacheLimit(uint64(trainMemoryLimit) * 1024 * 1024 * 1024)
- mlx.SetMemoryLimit(uint64(trainMemoryLimit) * 1024 * 1024 * 1024)
-
- tok := m.Tokenizer()
- slog.Info("model loaded",
- "type", m.ModelType(),
- "layers", m.NumLayers(),
- )
-
- // --- Apply LoRA ---
- targets := strings.Split(trainTargets, ",")
- cfg := mlx.LoRAConfig{
- Rank: trainRank,
- Alpha: float32(trainAlpha),
- TargetKeys: targets,
- }
-
- adapter := m.ApplyLoRA(cfg)
- slog.Info("LoRA applied",
- "rank", cfg.Rank,
- "alpha", cfg.Alpha,
- "targets", targets,
- "trainable_params", adapter.TotalParams(),
- "layers", len(adapter.Layers),
- )
-
- // --- Load training data ---
- samples, err := loadTrainingSamples(trainData, tok, m.ModelType(), trainMaxSeqLen)
- if err != nil {
- return fmt.Errorf("load training data: %w", err)
- }
- slog.Info("training data loaded", "samples", len(samples))
-
- if len(samples) == 0 {
- return fmt.Errorf("no training samples loaded")
- }
-
- // --- Training loop ---
- params := adapter.AllTrainableParams()
- opt := mlx.NewAdamW(trainLR)
-
- // Build argument indices for ValueAndGrad (all params)
- argIndices := make([]int, len(params))
- for i := range argIndices {
- argIndices[i] = i
- }
-
- var totalLoss float64
- var totalSteps int
-
- for epoch := 0; epoch < trainEpochs; epoch++ {
- var epochLoss float64
- epochStart := time.Now()
-
- for si, sample := range samples {
- // Build token tensors: input = tokens[:-1], target = tokens[1:]
- seqLen := len(sample.Tokens)
- if seqLen < 2 {
- continue
- }
-
- inputTokens := sample.Tokens[:seqLen-1]
- targetTokens := sample.Tokens[1:]
- maskTokens := sample.Mask[1:] // mask aligned with targets
-
- inputArr := mlx.FromValues(inputTokens, 1, len(inputTokens))
- targetArr := mlx.FromValues(targetTokens, 1, len(targetTokens))
-
- // Build float32 mask
- maskF32 := make([]float32, len(maskTokens))
- for i, m := range maskTokens {
- maskF32[i] = float32(m)
- }
- maskArr := mlx.FromValues(maskF32, 1, len(maskF32))
- mlx.Materialize(inputArr, targetArr, maskArr)
-
- // Loss function closure — takes LoRA params as inputs
- lossFn := func(inputs []*mlx.Array) []*mlx.Array {
- // Set LoRA params from inputs
- adapter.SetAllParams(inputs)
-
- // Forward pass with fresh caches (no KV caching for training)
- caches := m.NewCache()
- logits := m.Forward(inputArr, caches)
-
- // Cast targets to int32 for take_along_axis
- loss := mlx.MaskedCrossEntropyLoss(logits, targetArr, maskArr)
- return []*mlx.Array{loss}
- }
-
- // Compute value and gradients
- grad := mlx.ValueAndGrad(lossFn, argIndices...)
- values, grads, err := grad.Apply(params...)
- grad.Free()
- if err != nil {
- return fmt.Errorf("epoch %d sample %d: gradient failed: %w", epoch, si, err)
- }
-
- mlx.Materialize(append(values, grads...)...)
-
- loss := values[0].Float()
- epochLoss += loss
- totalSteps++
-
- // Update parameters
- params = opt.Step(params, grads)
- adapter.SetAllParams(params)
- mlx.Materialize(params...)
-
- // Periodic cleanup
- if totalSteps%4 == 0 {
- runtime.GC()
- mlx.ClearCache()
- }
-
- // Log progress
- if (si+1)%10 == 0 || si == len(samples)-1 {
- avgLoss := epochLoss / float64(si+1)
- slog.Info("training",
- "epoch", epoch+1,
- "step", fmt.Sprintf("%d/%d", si+1, len(samples)),
- "loss", fmt.Sprintf("%.4f", loss),
- "avg_loss", fmt.Sprintf("%.4f", avgLoss),
- )
- }
- }
-
- totalLoss = epochLoss / float64(len(samples))
- elapsed := time.Since(epochStart)
- slog.Info("epoch complete",
- "epoch", epoch+1,
- "avg_loss", fmt.Sprintf("%.4f", totalLoss),
- "duration", elapsed.Round(time.Second),
- "samples_per_sec", fmt.Sprintf("%.1f", float64(len(samples))/elapsed.Seconds()),
- )
- }
-
- // --- Save adapter ---
- if err := adapter.Save(trainOutput); err != nil {
- return fmt.Errorf("save adapter: %w", err)
- }
-
- slog.Info("training complete",
- "output", trainOutput,
- "total_steps", totalSteps,
- "final_loss", fmt.Sprintf("%.4f", totalLoss),
- "duration", time.Since(start).Round(time.Second),
- "trainable_params", adapter.TotalParams(),
- )
-
- return nil
-}
-
-// loadTrainingSamples reads JSONL and tokenises each conversation.
-func loadTrainingSamples(path string, tok *tokenizer.Tokenizer, modelType string, maxSeqLen int) ([]trainSample, error) {
- f, err := os.Open(path)
- if err != nil {
- return nil, err
- }
- defer f.Close()
-
- var samples []trainSample
- scanner := bufio.NewScanner(f)
- scanner.Buffer(make([]byte, 1<<20), 1<<20) // 1MB line buffer
-
- lineNum := 0
- for scanner.Scan() {
- lineNum++
- line := strings.TrimSpace(scanner.Text())
- if line == "" || strings.HasPrefix(line, "#") {
- continue
- }
-
- var entry struct {
- Messages []ml.Message `json:"messages"`
- }
- if err := json.Unmarshal([]byte(line), &entry); err != nil {
- slog.Warn("skipping invalid line", "line", lineNum, "error", err)
- continue
- }
-
- if len(entry.Messages) == 0 {
- continue
- }
-
- sample := tokeniseConversation(entry.Messages, tok, modelType, maxSeqLen)
- if sample != nil {
- samples = append(samples, *sample)
- }
- }
-
- return samples, scanner.Err()
-}
-
-// tokeniseConversation formats and tokenises a conversation, creating a mask
-// that is 1 for assistant tokens and 0 for system/user tokens.
-func tokeniseConversation(messages []ml.Message, tok *tokenizer.Tokenizer, modelType string, maxSeqLen int) *trainSample {
- // Strategy: tokenise the full conversation, then tokenise just the prefix
- // (non-assistant parts) to determine the mask boundary.
-
- // Build full conversation text
- fullText := formatConversation(messages, modelType, true)
- fullTokens := tok.Encode(fullText)
-
- if len(fullTokens) < 2 {
- return nil
- }
-
- // Truncate to max sequence length
- if len(fullTokens) > maxSeqLen {
- fullTokens = fullTokens[:maxSeqLen]
- }
-
- // Build mask: tokenise prefix (everything up to last assistant response)
- // then mark remaining tokens as assistant (mask=1)
- prefixText := formatConversation(messages, modelType, false)
- prefixTokens := tok.Encode(prefixText)
-
- mask := make([]int32, len(fullTokens))
- for i := range mask {
- if i >= len(prefixTokens) {
- mask[i] = 1 // assistant token
- }
- }
-
- return &trainSample{
- Tokens: fullTokens,
- Mask: mask,
- }
-}
-
-// formatConversation formats messages using the model's chat template.
-// If includeAssistant is false, only formats up to the last assistant turn header.
-func formatConversation(messages []ml.Message, modelType string, includeAssistant bool) string {
- switch modelType {
- case "qwen3":
- return formatQwen3Train(messages, includeAssistant)
- default:
- return formatGemmaTrain(messages, includeAssistant)
- }
-}
-
-func formatQwen3Train(messages []ml.Message, includeAssistant bool) string {
- var sb strings.Builder
- for _, msg := range messages {
- if msg.Role == "assistant" && !includeAssistant {
- // Write the assistant header but not the content
- sb.WriteString("<|im_start|>assistant\n")
- return sb.String()
- }
- switch msg.Role {
- case "system":
- sb.WriteString(fmt.Sprintf("<|im_start|>system\n%s<|im_end|>\n", msg.Content))
- case "user":
- sb.WriteString(fmt.Sprintf("<|im_start|>user\n%s<|im_end|>\n", msg.Content))
- case "assistant":
- sb.WriteString(fmt.Sprintf("<|im_start|>assistant\n%s<|im_end|>\n", msg.Content))
- }
- }
- return sb.String()
-}
-
-func formatGemmaTrain(messages []ml.Message, includeAssistant bool) string {
- var sb strings.Builder
- for _, msg := range messages {
- if msg.Role == "assistant" && !includeAssistant {
- sb.WriteString("model\n")
- return sb.String()
- }
- switch msg.Role {
- case "user":
- sb.WriteString(fmt.Sprintf("user\n%s\n", msg.Content))
- case "assistant":
- sb.WriteString(fmt.Sprintf("model\n%s\n", msg.Content))
- case "system":
- sb.WriteString(fmt.Sprintf("user\n[System: %s]\n", msg.Content))
- }
- }
- return sb.String()
-}
diff --git a/cmd/ml/cmd_train_init.go b/cmd/ml/cmd_train_init.go
deleted file mode 100644
index 263966d9..00000000
--- a/cmd/ml/cmd_train_init.go
+++ /dev/null
@@ -1,7 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-func init() {
- mlCmd.AddCommand(trainCmd)
-}
diff --git a/cmd/ml/cmd_worker.go b/cmd/ml/cmd_worker.go
deleted file mode 100644
index 73767de3..00000000
--- a/cmd/ml/cmd_worker.go
+++ /dev/null
@@ -1,80 +0,0 @@
-package ml
-
-import (
- "time"
-
- "forge.lthn.ai/core/go/pkg/cli"
- "forge.lthn.ai/core/go-ml"
-)
-
-var (
- workerAPIBase string
- workerID string
- workerName string
- workerAPIKey string
- workerGPU string
- workerVRAM int
- workerLangs string
- workerModels string
- workerInferURL string
- workerTaskType string
- workerBatchSize int
- workerPoll time.Duration
- workerOneShot bool
- workerDryRun bool
-)
-
-var workerCmd = &cli.Command{
- Use: "worker",
- Short: "Run a distributed worker node",
- Long: "Polls the LEM API for tasks, runs local inference, and submits results.",
- RunE: runWorker,
-}
-
-func init() {
- workerCmd.Flags().StringVar(&workerAPIBase, "api", ml.EnvOr("LEM_API", "https://infer.lthn.ai"), "LEM API base URL")
- workerCmd.Flags().StringVar(&workerID, "id", ml.EnvOr("LEM_WORKER_ID", ml.MachineID()), "Worker ID")
- workerCmd.Flags().StringVar(&workerName, "name", ml.EnvOr("LEM_WORKER_NAME", ml.Hostname()), "Worker display name")
- workerCmd.Flags().StringVar(&workerAPIKey, "key", ml.EnvOr("LEM_API_KEY", ""), "API key")
- workerCmd.Flags().StringVar(&workerGPU, "gpu", ml.EnvOr("LEM_GPU", ""), "GPU type")
- workerCmd.Flags().IntVar(&workerVRAM, "vram", ml.IntEnvOr("LEM_VRAM_GB", 0), "GPU VRAM in GB")
- workerCmd.Flags().StringVar(&workerLangs, "languages", ml.EnvOr("LEM_LANGUAGES", ""), "Comma-separated language codes")
- workerCmd.Flags().StringVar(&workerModels, "models", ml.EnvOr("LEM_MODELS", ""), "Comma-separated model names")
- workerCmd.Flags().StringVar(&workerInferURL, "infer", ml.EnvOr("LEM_INFER_URL", "http://localhost:8090"), "Local inference endpoint")
- workerCmd.Flags().StringVar(&workerTaskType, "type", "", "Filter by task type")
- workerCmd.Flags().IntVar(&workerBatchSize, "batch", 5, "Tasks per poll")
- workerCmd.Flags().DurationVar(&workerPoll, "poll", 30*time.Second, "Poll interval")
- workerCmd.Flags().BoolVar(&workerOneShot, "one-shot", false, "Process one batch and exit")
- workerCmd.Flags().BoolVar(&workerDryRun, "dry-run", false, "Fetch tasks but don't run inference")
-}
-
-func runWorker(cmd *cli.Command, args []string) error {
- if workerAPIKey == "" {
- workerAPIKey = ml.ReadKeyFile()
- }
-
- cfg := &ml.WorkerConfig{
- APIBase: workerAPIBase,
- WorkerID: workerID,
- Name: workerName,
- APIKey: workerAPIKey,
- GPUType: workerGPU,
- VRAMGb: workerVRAM,
- InferURL: workerInferURL,
- TaskType: workerTaskType,
- BatchSize: workerBatchSize,
- PollInterval: workerPoll,
- OneShot: workerOneShot,
- DryRun: workerDryRun,
- }
-
- if workerLangs != "" {
- cfg.Languages = ml.SplitComma(workerLangs)
- }
- if workerModels != "" {
- cfg.Models = ml.SplitComma(workerModels)
- }
-
- ml.RunWorkerLoop(cfg)
- return nil
-}
diff --git a/cmd/ml/serve_backend_default.go b/cmd/ml/serve_backend_default.go
deleted file mode 100644
index 1e564b17..00000000
--- a/cmd/ml/serve_backend_default.go
+++ /dev/null
@@ -1,9 +0,0 @@
-//go:build !(darwin && arm64)
-
-package ml
-
-import "forge.lthn.ai/core/go-ml"
-
-func createServeBackend() (ml.Backend, error) {
- return ml.NewHTTPBackend(apiURL, modelName), nil
-}
diff --git a/cmd/ml/serve_backend_mlx.go b/cmd/ml/serve_backend_mlx.go
deleted file mode 100644
index 271eaa95..00000000
--- a/cmd/ml/serve_backend_mlx.go
+++ /dev/null
@@ -1,22 +0,0 @@
-//go:build darwin && arm64
-
-package ml
-
-import (
- "fmt"
- "log/slog"
-
- "forge.lthn.ai/core/go-ml"
-)
-
-func createServeBackend() (ml.Backend, error) {
- if serveModelPath != "" {
- slog.Info("ml serve: loading native MLX backend", "path", serveModelPath)
- b, err := ml.NewMLXBackend(serveModelPath)
- if err != nil {
- return nil, fmt.Errorf("mlx backend: %w", err)
- }
- return b, nil
- }
- return ml.NewHTTPBackend(apiURL, modelName), nil
-}
diff --git a/cmd/pkgcmd/cmd_search.go b/cmd/pkgcmd/cmd_search.go
index 3fe59e8c..7da952b0 100644
--- a/cmd/pkgcmd/cmd_search.go
+++ b/cmd/pkgcmd/cmd_search.go
@@ -74,7 +74,7 @@ func runPkgSearch(org, pattern, repoType string, limit int, refresh bool) error
cacheDir = filepath.Join(filepath.Dir(regPath), ".core", "cache")
}
- c, err := cache.New(cacheDir, 0)
+ c, err := cache.New(coreio.Local, cacheDir, 0)
if err != nil {
c = nil
}
diff --git a/go.mod b/go.mod
index 1cb8c6a9..f26bafc2 100644
--- a/go.mod
+++ b/go.mod
@@ -3,20 +3,19 @@ module forge.lthn.ai/core/cli
go 1.25.5
require (
- forge.lthn.ai/core/go v0.0.0
- forge.lthn.ai/core/go-agentic v0.0.0
- forge.lthn.ai/core/go-ai v0.0.0
- forge.lthn.ai/core/go-api v0.0.0
- forge.lthn.ai/core/go-crypt v0.0.0
- forge.lthn.ai/core/go-devops v0.0.0
- forge.lthn.ai/core/go-inference v0.0.0
- forge.lthn.ai/core/go-ml v0.0.0
- forge.lthn.ai/core/go-mlx v0.0.0
- forge.lthn.ai/core/go-netops v0.0.0
- forge.lthn.ai/core/go-rag v0.0.0
- forge.lthn.ai/core/go-scm v0.0.0
- forge.lthn.ai/core/go-store v0.0.0
- forge.lthn.ai/core/go-webview v0.0.0
+ forge.lthn.ai/core/go v0.0.0-20260221191103-d091fa62023f
+ forge.lthn.ai/core/go-agentic v0.0.0-20260221191948-ad0cf5c932a3
+ forge.lthn.ai/core/go-ai v0.0.0-20260221192232-bc9597c19153
+ forge.lthn.ai/core/go-api v0.0.0-20260221015744-0d3479839dc5
+ forge.lthn.ai/core/go-crypt v0.0.0-20260221190941-9585da8e6649
+ forge.lthn.ai/core/go-devops v0.0.0-20260221192100-4b5739fbd7ac
+ forge.lthn.ai/core/go-inference v0.0.0-20260220151119-1576f744d105 // indirect
+ forge.lthn.ai/core/go-ml v0.0.0-20260221191458-812c926dac42
+ forge.lthn.ai/core/go-mlx v0.0.0-20260221191404-2292557fd65f // indirect
+ forge.lthn.ai/core/go-netops v0.0.0-20260221192152-565b16a848ae
+ forge.lthn.ai/core/go-rag v0.0.0-20260221191926-4c741992dc78
+ forge.lthn.ai/core/go-scm v0.0.0-20260221192735-5bfafcd6fc87
+ forge.lthn.ai/core/go-store v0.1.1-0.20260220151120-0284110ccadf // indirect
)
require (
@@ -49,6 +48,7 @@ require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apache/arrow-go/v18 v18.5.1 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect
github.com/brianvoe/gofakeit/v6 v6.28.0 // indirect
@@ -59,13 +59,22 @@ require (
github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/charmbracelet/bubbletea v1.3.10 // indirect
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
+ github.com/charmbracelet/lipgloss v1.1.0 // indirect
+ github.com/charmbracelet/x/ansi v0.10.1 // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
+ github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
@@ -123,13 +132,20 @@ require (
github.com/leaanthony/debme v1.2.1 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/marcboeker/go-duckdb v1.8.5 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/modelcontextprotocol/go-sdk v1.3.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/termenv v0.16.0 // indirect
+ github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/oasdiff/oasdiff v1.11.10 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
@@ -145,6 +161,9 @@ require (
github.com/qdrant/go-client v1.16.2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
+ github.com/redis/go-redis/v9 v9.18.0 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
@@ -172,6 +191,7 @@ require (
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
@@ -181,6 +201,7 @@ require (
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
+ go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect
@@ -194,21 +215,8 @@ require (
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
-)
-
-replace (
- forge.lthn.ai/core/go => ../go
- forge.lthn.ai/core/go-agentic => ../go-agentic
- forge.lthn.ai/core/go-ai => ../go-ai
- forge.lthn.ai/core/go-api => ../../go-api
- forge.lthn.ai/core/go-crypt => ../go-crypt
- forge.lthn.ai/core/go-devops => ../go-devops
- forge.lthn.ai/core/go-inference => ../go-inference
- forge.lthn.ai/core/go-ml => ../go-ml
- forge.lthn.ai/core/go-mlx => ../go-mlx
- forge.lthn.ai/core/go-netops => ../go-netops
- forge.lthn.ai/core/go-rag => ../go-rag
- forge.lthn.ai/core/go-scm => ../go-scm
- forge.lthn.ai/core/go-store => ../go-store
- forge.lthn.ai/core/go-webview => ../go-webview
+ modernc.org/libc v1.67.7 // indirect
+ modernc.org/mathutil v1.7.1 // indirect
+ modernc.org/memory v1.11.0 // indirect
+ modernc.org/sqlite v1.46.1 // indirect
)
diff --git a/go.sum b/go.sum
index 9205934a..3e0c5ecb 100644
--- a/go.sum
+++ b/go.sum
@@ -9,6 +9,32 @@ codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0 h1:HTCWpzyWQOHDWt3LzI6/d2jv
codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2 v2.2.0/go.mod h1:ZglEEDj+qkxYUb+SQIeqGtFxQrbaMYqIOgahNKb7uxs=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
+forge.lthn.ai/core/go v0.0.0-20260221191103-d091fa62023f h1:CcSh/FFY93K5m0vADHLxwxKn2pTIM8HzYX1eGa4WZf4=
+forge.lthn.ai/core/go v0.0.0-20260221191103-d091fa62023f/go.mod h1:WCPJVEZm/6mTcJimHV0uX8ZhnKEF3dN0rQp13ByaSPg=
+forge.lthn.ai/core/go-agentic v0.0.0-20260221191948-ad0cf5c932a3 h1:6H3hjqHY0loJJe9iCofFzw6x5JDIbi6JNSL0oW2TKFE=
+forge.lthn.ai/core/go-agentic v0.0.0-20260221191948-ad0cf5c932a3/go.mod h1:2WCSLupRyAeSpmFWM5+OPG0/wa4KMQCO8gA0hM9cUq8=
+forge.lthn.ai/core/go-ai v0.0.0-20260221192232-bc9597c19153 h1:11XJI5RPm38l664KC9acRZz2gA+RLpmxCdg5JorimoM=
+forge.lthn.ai/core/go-ai v0.0.0-20260221192232-bc9597c19153/go.mod h1:GdcXgm3jwvh4AVxrlCa0Zbw4vASeNV8JSAXfftCJVRc=
+forge.lthn.ai/core/go-api v0.0.0-20260221015744-0d3479839dc5 h1:60reee4fmT4USZqEd6dyCTXsTj47eOOEc6Pp0HHJbd0=
+forge.lthn.ai/core/go-api v0.0.0-20260221015744-0d3479839dc5/go.mod h1:f0hPLX+GZT/ME8Tb7c8wVDlfLqnpOKRwf2k5lpJq87g=
+forge.lthn.ai/core/go-crypt v0.0.0-20260221190941-9585da8e6649 h1:Rs3bfSU8u1wkzYeL21asL7IcJIBVwOhtRidcEVj/PkA=
+forge.lthn.ai/core/go-crypt v0.0.0-20260221190941-9585da8e6649/go.mod h1:RS+sz5lChrbc1AEmzzOULsTiMv3bwcwVtwbZi+c/Yjk=
+forge.lthn.ai/core/go-devops v0.0.0-20260221192100-4b5739fbd7ac h1:agYaMGTUw0n/vPrv0i8mTxbKt5NItDcsXhCKQHoivy8=
+forge.lthn.ai/core/go-devops v0.0.0-20260221192100-4b5739fbd7ac/go.mod h1:FSp7+jfV3QXyPzL1C8XZm6W57vjT8cbWly8vf/bPJEg=
+forge.lthn.ai/core/go-inference v0.0.0-20260220151119-1576f744d105 h1:CVUVxp1BfUI8wmlEUW0Nay8w4hADR54nqBmeF+KK2Ac=
+forge.lthn.ai/core/go-inference v0.0.0-20260220151119-1576f744d105/go.mod h1:hmLtynfw1yo0ByuX3pslLZMgCdqJH2r+2+wGJDhmmi0=
+forge.lthn.ai/core/go-ml v0.0.0-20260221191458-812c926dac42 h1:rxhnHgWVGnQ93/mhUyLxIw/Q2l80njiGfNvv0kKISb0=
+forge.lthn.ai/core/go-ml v0.0.0-20260221191458-812c926dac42/go.mod h1:lmhzv04VCP41ym7Wuhck+T1HeC5PoLtfOqXe8fW26Hc=
+forge.lthn.ai/core/go-mlx v0.0.0-20260221191404-2292557fd65f h1:dlb6hFFhxfnJvD1ZYoQVsxD9NM4CV+sXkjHa6kBGzeE=
+forge.lthn.ai/core/go-mlx v0.0.0-20260221191404-2292557fd65f/go.mod h1:QHspfOk9MgbuG6Wb4m+RzQyCMibtoQNZw+hUs4yclOA=
+forge.lthn.ai/core/go-netops v0.0.0-20260221192152-565b16a848ae h1:1WPKohhwPCEPnKZPx80AqJS306QkKemGU0W4TKUgvqA=
+forge.lthn.ai/core/go-netops v0.0.0-20260221192152-565b16a848ae/go.mod h1:YljW66VyXrWX5/kfmDlFaeFRewXA2/ss9F6shSTr5Rs=
+forge.lthn.ai/core/go-rag v0.0.0-20260221191926-4c741992dc78 h1:M7ftoQ3AB87W/h4cELK+dxetzLoQi68KwnK2JhkSA8k=
+forge.lthn.ai/core/go-rag v0.0.0-20260221191926-4c741992dc78/go.mod h1:f0WQYSeg3Oc7gCHTLUL0aCIzK1fS2mgMBDnBzjKgOzQ=
+forge.lthn.ai/core/go-scm v0.0.0-20260221192735-5bfafcd6fc87 h1:1rkrRCVOq4hjKGkXxPmyBDVjxs82VV84ED/WnrYjptE=
+forge.lthn.ai/core/go-scm v0.0.0-20260221192735-5bfafcd6fc87/go.mod h1:lK2RacccYr9Uvntbhx9sPupXlI2IvNufeil4mXVpdEM=
+forge.lthn.ai/core/go-store v0.1.1-0.20260220151120-0284110ccadf h1:EDKI+OM0M+l4+VclG5XuUDoYAM8yu8uleFYReeEYwHY=
+forge.lthn.ai/core/go-store v0.1.1-0.20260220151120-0284110ccadf/go.mod h1:FpUlLEX/ebyoxpk96F7ktr0vYvmFtC5Rpi9fi88UVqw=
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/99designs/gqlgen v0.17.87 h1:pSnCIMhBQezAE8bc1GNmfdLXFmnWtWl1GRDFEE/nHP8=
@@ -54,6 +80,8 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
@@ -61,6 +89,10 @@ github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
+github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
+github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
+github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
+github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
@@ -76,6 +108,18 @@ github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaD
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
+github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
+github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
@@ -92,12 +136,18 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -208,6 +258,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
+github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
@@ -258,6 +310,8 @@ github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@@ -270,6 +324,10 @@ github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
@@ -285,6 +343,14 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
+github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/oasdiff/oasdiff v1.11.10 h1:4I9VrktUoHmwydkJqVOC7Bd6BXKu9dc4UUP3PIu1VjM=
github.com/oasdiff/oasdiff v1.11.10/go.mod h1:GXARzmqBKN8lZHsTQD35ZM41ePbu6JdAZza4sRMeEKg=
@@ -321,6 +387,13 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
+github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
+github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -400,6 +473,8 @@ github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQs
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
@@ -429,6 +504,8 @@ go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4A
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
+go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
+go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
@@ -476,6 +553,7 @@ golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -530,3 +608,31 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
+modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
+modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
+modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
+modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
+modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
+modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
+modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
+modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
+modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
+modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
+modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
+modernc.org/libc v1.67.7 h1:H+gYQw2PyidyxwxQsGTwQw6+6H+xUk+plvOKW7+d3TI=
+modernc.org/libc v1.67.7/go.mod h1:UjCSJFl2sYbJbReVQeVpq/MgzlbmDM4cRHIYFelnaDk=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
+modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
+modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
+modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
+modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
+modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
+modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
+modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
+modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/go.work b/go.work
deleted file mode 100644
index 5b53a4fd..00000000
--- a/go.work
+++ /dev/null
@@ -1,18 +0,0 @@
-go 1.25.5
-
-use (
- .
- ../go
- ../go-agentic
- ../go-ai
- ../go-crypt
- ../go-devops
- ../go-inference
- ../go-ml
- ../go-mlx
- ../go-netops
- ../go-rag
- ../go-scm
- ../go-store
- ../go-webview
-)
diff --git a/main.go b/main.go
index 7c430aea..305505ea 100644
--- a/main.go
+++ b/main.go
@@ -20,7 +20,7 @@ import (
_ "forge.lthn.ai/core/cli/cmd/help"
_ "forge.lthn.ai/core/cli/cmd/lab"
_ "forge.lthn.ai/core/cli/cmd/mcpcmd"
- _ "forge.lthn.ai/core/cli/cmd/ml"
+ _ "forge.lthn.ai/core/go-ml/cmd"
_ "forge.lthn.ai/core/cli/cmd/module"
_ "forge.lthn.ai/core/cli/cmd/monitor"
_ "forge.lthn.ai/core/cli/cmd/pkgcmd"