mlx-bun 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/docs/dag/training-inference-map.html +575 -0
- package/docs/design/memory-system.md +193 -295
- package/docs/design/orpo-dynamic-lambda.md +179 -0
- package/docs/design/orpo-future-enhancements.md +42 -0
- package/docs/design/orpo-training.md +13 -0
- package/docs/design/parity-tier-dag.md +143 -0
- package/docs/design/w4a16-compute-precision-spike.md +232 -0
- package/docs/investigations/orpo-base-uf-experiment-and-directions.md +120 -0
- package/docs/investigations/orpo-flash-cce-pin-leak.md +167 -0
- package/docs/investigations/orpo-uf-testing-handoff.md +126 -0
- package/docs/investigations/steel-flash-cce-handoff.md +58 -0
- package/docs/investigations/trainer-validation-experiment.md +173 -0
- package/docs/reference/memory.md +77 -0
- package/docs/reference/orpo-quickstart.md +44 -5
- package/docs/reference/training.md +9 -5
- package/package.json +3 -3
- package/src/cli.ts +651 -18
- package/src/eval/runner.ts +76 -6
- package/src/eval/tasks/bfcl.ts +49 -15
- package/src/eval/tasks/gsm8k.ts +21 -9
- package/src/eval/tasks/hashhop.ts +33 -4
- package/src/eval/tasks/humaneval.ts +30 -8
- package/src/eval/tasks/ifeval.ts +52 -18
- package/src/eval/tasks/mmlu.ts +85 -74
- package/src/jobs/runner.ts +6 -0
- package/src/memory/chunk.ts +25 -0
- package/src/memory/cluster.ts +24 -0
- package/src/memory/db.ts +114 -0
- package/src/memory/ingest.ts +22 -0
- package/src/memory/pipeline.ts +63 -0
- package/src/memory/prompts.ts +23 -0
- package/src/memory/schedule.ts +163 -0
- package/src/memory/synthesize.ts +29 -0
- package/src/memory/tools.ts +399 -0
- package/src/memory/vault.ts +694 -0
- package/src/memory/wikify.ts +23 -0
- package/src/mlx/metal-kernel.ts +13 -0
- package/src/model/flash-attention.ts +5 -3
- package/src/pi-provider.ts +29 -13
- package/src/pi-session.ts +81 -0
- package/src/pi-terminal.ts +13 -24
- package/src/pi-web.ts +246 -166
- package/src/quantize/job.ts +17 -2
- package/src/quantize/quantizer.ts +26 -2
- package/src/server.ts +213 -44
- package/src/tokenizer.ts +6 -2
- package/src/train/dataset.ts +11 -3
- package/src/train/flash-cce.ts +18 -5
- package/src/train/job.ts +41 -4
- package/src/train/loss.ts +10 -4
- package/src/train/trainer.ts +62 -3
- package/src/train/watch.ts +542 -0
- package/src/web/app.html +264 -14
- package/src/web/skills/memory/SKILL.md +97 -0
- package/src/web/skills/web-research/SKILL.md +3 -1
- package/src/web/skills.ts +46 -8
package/src/web/app.html
CHANGED
|
@@ -105,6 +105,39 @@
|
|
|
105
105
|
#chat-think.think-on .dot{background:var(--blue);box-shadow:0 0 11px var(--blue)}
|
|
106
106
|
.pill.bad .dot{background:var(--red);box-shadow:0 0 11px var(--red)}
|
|
107
107
|
.pill.warn .dot{background:var(--orange);box-shadow:0 0 11px var(--orange)}
|
|
108
|
+
/* sampling disclosure: a small pill that opens a popover above the composer */
|
|
109
|
+
#chat-sampling-wrap{position:relative;display:inline-flex}
|
|
110
|
+
#chat-sampling{cursor:pointer;gap:6px;padding:5px 11px;font-size:12px;user-select:none;-webkit-user-select:none;
|
|
111
|
+
transition:color .22s var(--ease),border-color .22s var(--ease)}
|
|
112
|
+
#chat-sampling:hover{border-color:rgba(255,255,255,.28);color:var(--ink)}
|
|
113
|
+
#chat-sampling.on{color:var(--ink);border-color:rgba(255,255,255,.28)}
|
|
114
|
+
#chat-sampling.dirty .dot{background:var(--blue);box-shadow:0 0 11px var(--blue)}
|
|
115
|
+
#chat-sampling .dot{width:7px;height:7px}
|
|
116
|
+
.samplepop{position:absolute;bottom:calc(100% + 9px);right:0;z-index:40;width:300px;
|
|
117
|
+
background:rgba(30,30,34,.98);border:1px solid rgba(255,255,255,.12);border-radius:13px;padding:13px 14px;
|
|
118
|
+
backdrop-filter:blur(22px) saturate(1.3);-webkit-backdrop-filter:blur(22px) saturate(1.3);
|
|
119
|
+
box-shadow:0 18px 50px rgba(0,0,0,.7);display:none;
|
|
120
|
+
transform:translateY(4px);opacity:0;transition:opacity .16s var(--ease),transform .16s var(--ease)}
|
|
121
|
+
.samplepop.open{display:block;transform:translateY(0);opacity:1}
|
|
122
|
+
.samplepop h4{font-size:12px;font-weight:600;color:var(--dim);letter-spacing:.02em;
|
|
123
|
+
text-transform:uppercase;margin:0 0 10px}
|
|
124
|
+
.samprow{display:flex;align-items:center;gap:9px;margin:0 0 11px}
|
|
125
|
+
.samprow label{font-size:12.5px;color:var(--ink);width:74px;flex:0 0 auto}
|
|
126
|
+
.samprow input[type=range]{flex:1 1 auto;-webkit-appearance:none;appearance:none;height:4px;margin:0;
|
|
127
|
+
border-radius:3px;background:rgba(255,255,255,.15);outline:none;cursor:pointer}
|
|
128
|
+
.samprow input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:15px;height:15px;
|
|
129
|
+
border-radius:50%;background:var(--ink);border:0;box-shadow:0 1px 4px rgba(0,0,0,.5);cursor:pointer;
|
|
130
|
+
transition:background .15s var(--ease)}
|
|
131
|
+
.samprow input[type=range]:hover::-webkit-slider-thumb,
|
|
132
|
+
.samprow input[type=range]:focus::-webkit-slider-thumb{background:var(--blue)}
|
|
133
|
+
.samp-val{width:40px;flex:0 0 auto;text-align:right;font-family:var(--mono);font-size:12px;color:var(--ink)}
|
|
134
|
+
.samp-val.auto{color:var(--dimmer)}
|
|
135
|
+
.samppop-foot{display:flex;justify-content:space-between;align-items:center;margin-top:3px}
|
|
136
|
+
.samppop-foot .note{font-size:10.5px;color:var(--dimmer);line-height:1.3;flex:1 1 auto;padding-right:8px}
|
|
137
|
+
.samppop-foot button{appearance:none;cursor:pointer;background:var(--card);border:1px solid var(--hairline);
|
|
138
|
+
color:var(--dim);border-radius:8px;padding:5px 11px;font-size:11.5px;font-weight:600;flex:0 0 auto;
|
|
139
|
+
transition:color .18s var(--ease),border-color .18s var(--ease)}
|
|
140
|
+
.samppop-foot button:hover{color:var(--ink);border-color:rgba(255,255,255,.28)}
|
|
108
141
|
#nav-download{color:var(--ink);font-family:var(--mono);font-size:11.5px;font-weight:500}
|
|
109
142
|
#nav-download .dot{background:var(--orange);box-shadow:0 0 11px var(--orange);animation:dlpulse 1.4s ease-in-out infinite}
|
|
110
143
|
@keyframes dlpulse{0%,100%{opacity:.4}50%{opacity:1}}
|
|
@@ -485,6 +518,7 @@
|
|
|
485
518
|
<a class="tab" href="#/dataset" data-tab="dataset">Build Dataset</a>
|
|
486
519
|
<a class="tab" href="#/status" data-tab="status">Status</a>
|
|
487
520
|
<a class="tab" href="/curves" data-tab="curves">Curves</a>
|
|
521
|
+
<a class="tab" href="#/routes" data-tab="routes">Routes</a>
|
|
488
522
|
</div>
|
|
489
523
|
<div class="right">
|
|
490
524
|
<button class="hf-gear" id="nav-hf" title="Hugging Face token settings" aria-label="Hugging Face settings">
|
|
@@ -552,6 +586,32 @@
|
|
|
552
586
|
<div class="chat-hint">
|
|
553
587
|
<span id="chat-status-line">Enter to send · Shift+Enter for a new line · type while it streams to steer</span>
|
|
554
588
|
<span class="hint-right">
|
|
589
|
+
<span id="chat-sampling-wrap">
|
|
590
|
+
<span id="chat-sampling" class="pill" role="button" tabindex="0" aria-haspopup="true" aria-expanded="false"
|
|
591
|
+
title="Sampling controls (temperature · top_p · top_k)"><span class="dot"></span>Sampling</span>
|
|
592
|
+
<div id="chat-sampling-pop" class="samplepop" role="dialog" aria-label="Sampling controls">
|
|
593
|
+
<h4>Sampling</h4>
|
|
594
|
+
<div class="samprow">
|
|
595
|
+
<label for="samp-temp">Temperature</label>
|
|
596
|
+
<input id="samp-temp" type="range" min="0" max="2" step="0.05" aria-label="Temperature">
|
|
597
|
+
<output id="samp-temp-val" class="samp-val auto">0.70</output>
|
|
598
|
+
</div>
|
|
599
|
+
<div class="samprow">
|
|
600
|
+
<label for="samp-topp">top_p</label>
|
|
601
|
+
<input id="samp-topp" type="range" min="0" max="1" step="0.01" aria-label="top_p">
|
|
602
|
+
<output id="samp-topp-val" class="samp-val auto">0.95</output>
|
|
603
|
+
</div>
|
|
604
|
+
<div class="samprow">
|
|
605
|
+
<label for="samp-topk">top_k</label>
|
|
606
|
+
<input id="samp-topk" type="range" min="0" max="100" step="1" aria-label="top_k">
|
|
607
|
+
<output id="samp-topk-val" class="samp-val auto">off</output>
|
|
608
|
+
</div>
|
|
609
|
+
<div class="samppop-foot">
|
|
610
|
+
<span class="note">Sliders default to the recommended value — temperature follows the thinking toggle. Reset returns to defaults.</span>
|
|
611
|
+
<button id="samp-reset" type="button">Reset</button>
|
|
612
|
+
</div>
|
|
613
|
+
</div>
|
|
614
|
+
</span>
|
|
555
615
|
<select id="chat-adapter" class="pill" title="LoRA adapter for this chat (none = base model)">
|
|
556
616
|
<option value="">no adapter</option>
|
|
557
617
|
</select>
|
|
@@ -581,10 +641,14 @@
|
|
|
581
641
|
<p class="lead" style="margin:6px 0 22px;font-size:14.5px">Point at a Hugging Face repo id or a local snapshot path. Inspect first to confirm support and size.</p>
|
|
582
642
|
<div class="field">
|
|
583
643
|
<label>Model id or path</label>
|
|
584
|
-
<
|
|
585
|
-
|
|
644
|
+
<div id="q-drop">
|
|
645
|
+
<input type="text" id="q-model" placeholder="mlx-community/Qwen3-8B-bf16" autocomplete="off">
|
|
646
|
+
</div>
|
|
647
|
+
<input type="file" id="q-folder" webkitdirectory directory style="display:none">
|
|
648
|
+
<div class="hint">Anything on huggingface.co, an absolute path to a local snapshot, or <strong>choose / drop a folder</strong> below — we read its <code>config.json</code> and find the snapshot on disk.</div>
|
|
586
649
|
</div>
|
|
587
650
|
<div class="btnrow">
|
|
651
|
+
<button class="btn ghost" id="q-browse">Choose folder…</button>
|
|
588
652
|
<button class="btn ghost" id="q-inspect">Inspect</button>
|
|
589
653
|
</div>
|
|
590
654
|
<div id="q-inspect-out"></div>
|
|
@@ -643,7 +707,7 @@
|
|
|
643
707
|
<p class="lead" style="margin:6px 0 18px;font-size:14.5px">Your quantized model is written to disk.</p>
|
|
644
708
|
<div class="field"><label>Output directory</label>
|
|
645
709
|
<pre class="logbox" id="q-out" style="max-height:none;margin-top:0"></pre></div>
|
|
646
|
-
<div class="flash ok">
|
|
710
|
+
<div class="flash ok">Written to your model cache and registered automatically — it's already serveable from the Library and the model picker.</div>
|
|
647
711
|
<div class="grid g4" style="margin-top:14px">
|
|
648
712
|
<div class="card"><h3>Push to Hugging Face</h3><div class="cap">Publish this quantized model to a Hugging Face repo.</div>
|
|
649
713
|
<div class="btnrow" style="margin-top:14px"><button class="btn ghost sm" id="q-push">Push to HF</button></div>
|
|
@@ -671,10 +735,16 @@
|
|
|
671
735
|
<p class="lead" style="margin:6px 0 22px;font-size:14.5px">The model you're adapting. Point at a local model directory or snapshot path.</p>
|
|
672
736
|
<div class="field">
|
|
673
737
|
<label>Model directory / path</label>
|
|
674
|
-
<
|
|
675
|
-
|
|
738
|
+
<div id="f-drop">
|
|
739
|
+
<input type="text" id="f-model" placeholder="/abs/path/to/model or mlx-community/Qwen3-8B-4bit" autocomplete="off">
|
|
740
|
+
</div>
|
|
741
|
+
<input type="file" id="f-folder" webkitdirectory directory style="display:none">
|
|
742
|
+
<div class="hint">An OptiQ quant or any supported local model works — type a path/repo id, or <strong>choose / drop a folder</strong> below.</div>
|
|
743
|
+
</div>
|
|
744
|
+
<div class="btnrow">
|
|
745
|
+
<button class="btn ghost" id="f-browse">Choose folder…</button>
|
|
746
|
+
<button class="btn primary" id="f-next0">Continue</button>
|
|
676
747
|
</div>
|
|
677
|
-
<div class="btnrow"><button class="btn primary" id="f-next0">Continue</button></div>
|
|
678
748
|
</div>
|
|
679
749
|
|
|
680
750
|
<!-- step 2: dataset -->
|
|
@@ -962,6 +1032,12 @@
|
|
|
962
1032
|
</div></div>
|
|
963
1033
|
</section>
|
|
964
1034
|
|
|
1035
|
+
<!-- ═══════════════════ ROUTES ═══════════════════ -->
|
|
1036
|
+
<section data-route="routes" id="s-routes">
|
|
1037
|
+
<iframe src="/dag" title="Training · inference route map"
|
|
1038
|
+
style="width:100%;height:100%;border:0;display:block"></iframe>
|
|
1039
|
+
</section>
|
|
1040
|
+
|
|
965
1041
|
</div>
|
|
966
1042
|
|
|
967
1043
|
<div id="toasts"></div>
|
|
@@ -1039,7 +1115,7 @@ function mdInline(t) {
|
|
|
1039
1115
|
// Pull code spans out first (any run of backticks), then escape the rest.
|
|
1040
1116
|
t = String(t).replace(/(`+)([\s\S]+?)\1/g, (_m, _ticks, c) => {
|
|
1041
1117
|
codes.push(c.replace(/^ | $/g, ""));
|
|
1042
|
-
return "
|
|
1118
|
+
return "\u0000C" + (codes.length - 1) + "\u0000";
|
|
1043
1119
|
});
|
|
1044
1120
|
let s = esc(t);
|
|
1045
1121
|
// [text](url "title") — title optional and ignored.
|
|
@@ -1057,7 +1133,7 @@ function mdInline(t) {
|
|
|
1057
1133
|
// Autolink bare URLs that markdown didn't already wrap.
|
|
1058
1134
|
s = s.replace(/(^|[\s(])(https?:\/\/[^\s<)]+[^\s<).,;:!?'"])/g,
|
|
1059
1135
|
(_m, pre, url) => pre + '<a href="' + url + '" target="_blank" rel="noopener noreferrer">' + url + "</a>");
|
|
1060
|
-
return s.replace(
|
|
1136
|
+
return s.replace(/\u0000C(\d+)\u0000/g, (_m, i) => "<code>" + esc(codes[+i]) + "</code>");
|
|
1061
1137
|
}
|
|
1062
1138
|
|
|
1063
1139
|
function mdCodeBlock(lang, code) {
|
|
@@ -1288,7 +1364,7 @@ async function pushToHub(panel, { kind, job_id, source_path }) {
|
|
|
1288
1364
|
/* ════════════════════════════════════════════════════════════════════
|
|
1289
1365
|
ROUTER — toggles section[data-route]; lazily inits each controller.
|
|
1290
1366
|
════════════════════════════════════════════════════════════════════ */
|
|
1291
|
-
const ROUTES = ["chat", "quantize", "finetune", "dataset", "status"];
|
|
1367
|
+
const ROUTES = ["chat", "quantize", "finetune", "dataset", "status", "routes"];
|
|
1292
1368
|
const inited = {};
|
|
1293
1369
|
const controllers = {}; // name -> { init, enter, leave }
|
|
1294
1370
|
|
|
@@ -1431,7 +1507,7 @@ controllers.chat = (() => {
|
|
|
1431
1507
|
el("div", "who", m).textContent = "local agent";
|
|
1432
1508
|
const bubble = el("div", "bubble", m);
|
|
1433
1509
|
const thinkBox = el("details", "thinkbox", bubble);
|
|
1434
|
-
thinkBox.open =
|
|
1510
|
+
thinkBox.open = false; // collapsed by default; the reasoning stays tucked behind "Thinking" — click to expand
|
|
1435
1511
|
el("summary", "", thinkBox).textContent = "Thinking";
|
|
1436
1512
|
const thinkBody = el("div", "thinkbody", thinkBox);
|
|
1437
1513
|
thinkBox.style.display = "none";
|
|
@@ -1738,7 +1814,15 @@ controllers.chat = (() => {
|
|
|
1738
1814
|
let attachments = []; // { id, kind:"image"|"text", name, mimeType, data, text, truncated }
|
|
1739
1815
|
let visionCapable = false; // from the `ready` frame; gates image attachments
|
|
1740
1816
|
let thinkingCapable = false; // from `ready`; gates the reasoning toggle
|
|
1741
|
-
let thinkingOn = true; // on/
|
|
1817
|
+
let thinkingOn = true; // default on: with tools (read/web_search) the reasoning step gives the small model far better tool judgment (answers math/writing itself, only searches when needed). The thinking box defaults collapsed. Users can toggle it off.
|
|
1818
|
+
// Per-request sampling. Each field is either a user override (a number) or
|
|
1819
|
+
// null = "use the recommended default" (resolved server-side). The recommended
|
|
1820
|
+
// TEMPERATURE tracks the thinking toggle (0.7 off ↔ 0.9 on); top_p/top_k recs
|
|
1821
|
+
// are fixed mirrors of the server defaults (display only). The placeholder in
|
|
1822
|
+
// each input shows the live recommended value when the field isn't overridden.
|
|
1823
|
+
const sampling = { temperature: null, top_p: null, top_k: null };
|
|
1824
|
+
const SAMP_REC = { topP: 0.95, topK: 0 }; // fixed recommended top_p / top_k (server genDefaults)
|
|
1825
|
+
function recTemp() { return thinkingOn ? 0.9 : 0.7; } // mirrors server toOptions defaultTemp
|
|
1742
1826
|
let attachSeq = 0;
|
|
1743
1827
|
const MAX_TEXT_BYTES = 256 * 1024;
|
|
1744
1828
|
const TEXT_EXTS = /\.(txt|md|markdown|csv|tsv|json|jsonl|ya?ml|toml|ini|cfg|conf|log|xml|html?|css|scss|js|mjs|cjs|ts|tsx|jsx|py|rs|go|java|c|h|cpp|hpp|cc|cs|rb|php|sh|bash|zsh|fish|sql|swift|kt|lua|r|jl|tex|svg|env|gitignore|dockerfile|makefile)$/i;
|
|
@@ -1835,6 +1919,81 @@ controllers.chat = (() => {
|
|
|
1835
1919
|
b.classList.toggle("think-on", thinkingOn);
|
|
1836
1920
|
b.title = thinkingOn ? "Reasoning on — click to answer directly" : "Reasoning off — click to let the model think";
|
|
1837
1921
|
b.setAttribute("aria-pressed", String(thinkingOn));
|
|
1922
|
+
refreshSamplingRecs(); // recommended temperature follows the thinking state
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/* — sampling controls — */
|
|
1926
|
+
// Each row is a slider + a live value readout. A field is "auto" (null
|
|
1927
|
+
// override → the server uses its mode-aware default; temperature's default
|
|
1928
|
+
// follows the thinking toggle) until the user drags it, which records an
|
|
1929
|
+
// explicit override. Reset returns every field to auto.
|
|
1930
|
+
const SAMP_FIELDS = [
|
|
1931
|
+
{ id: "samp-temp", field: "temperature", valId: "samp-temp-val", rec: () => recTemp(), fmt: (v) => v.toFixed(2) },
|
|
1932
|
+
{ id: "samp-topp", field: "top_p", valId: "samp-topp-val", rec: () => SAMP_REC.topP, fmt: (v) => v.toFixed(2) },
|
|
1933
|
+
{ id: "samp-topk", field: "top_k", valId: "samp-topk-val", rec: () => SAMP_REC.topK, fmt: (v) => (v >= 1 ? String(Math.round(v)) : "off") },
|
|
1934
|
+
];
|
|
1935
|
+
|
|
1936
|
+
// Sync every slider + readout to current state: an overridden field shows its
|
|
1937
|
+
// set value (bright readout); an auto field shows the recommended value
|
|
1938
|
+
// (dimmed) and tracks it live — e.g. temperature when the thinking toggle
|
|
1939
|
+
// flips. Called on thinking-toggle flips, popover open, and reset.
|
|
1940
|
+
function refreshSamplingRecs() {
|
|
1941
|
+
for (const f of SAMP_FIELDS) {
|
|
1942
|
+
const slider = $(f.id), val = $(f.valId); if (!slider || !val) continue;
|
|
1943
|
+
const over = sampling[f.field];
|
|
1944
|
+
const v = over != null ? over : f.rec();
|
|
1945
|
+
slider.value = String(v);
|
|
1946
|
+
val.textContent = f.fmt(v);
|
|
1947
|
+
val.classList.toggle("auto", over == null);
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// User dragged a slider → record the explicit override, brighten the readout,
|
|
1952
|
+
// and push the whole sampling state to the server.
|
|
1953
|
+
function onSlide(f) {
|
|
1954
|
+
const slider = $(f.id); if (!slider) return;
|
|
1955
|
+
const v = Number(slider.value);
|
|
1956
|
+
sampling[f.field] = v;
|
|
1957
|
+
const val = $(f.valId); if (val) { val.textContent = f.fmt(v); val.classList.remove("auto"); }
|
|
1958
|
+
updateSamplingUi();
|
|
1959
|
+
send({ type: "set_sampling", temperature: sampling.temperature, top_p: sampling.top_p, top_k: sampling.top_k });
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// The pill shows a lit dot whenever any field is overridden.
|
|
1963
|
+
function updateSamplingUi() {
|
|
1964
|
+
const dirty = sampling.temperature != null || sampling.top_p != null || sampling.top_k != null;
|
|
1965
|
+
const pill = $("chat-sampling");
|
|
1966
|
+
if (pill) { pill.classList.toggle("dirty", dirty); pill.title = dirty
|
|
1967
|
+
? "Sampling overridden — click to edit or reset" : "Sampling controls (temperature · top_p · top_k)"; }
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function resetSampling() {
|
|
1971
|
+
sampling.temperature = null; sampling.top_p = null; sampling.top_k = null;
|
|
1972
|
+
refreshSamplingRecs(); updateSamplingUi();
|
|
1973
|
+
send({ type: "set_sampling", temperature: null, top_p: null, top_k: null });
|
|
1974
|
+
}
|
|
1975
|
+
function initSampling() {
|
|
1976
|
+
const pill = $("chat-sampling"), pop = $("chat-sampling-pop");
|
|
1977
|
+
if (!pill || !pop) return;
|
|
1978
|
+
const setOpen = (open) => {
|
|
1979
|
+
pop.classList.toggle("open", open);
|
|
1980
|
+
pill.classList.toggle("on", open);
|
|
1981
|
+
pill.setAttribute("aria-expanded", String(open));
|
|
1982
|
+
if (open) refreshSamplingRecs();
|
|
1983
|
+
};
|
|
1984
|
+
const toggle = () => setOpen(!pop.classList.contains("open"));
|
|
1985
|
+
pill.onclick = (e) => { e.stopPropagation(); toggle(); };
|
|
1986
|
+
pill.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); } });
|
|
1987
|
+
pop.addEventListener("click", (e) => e.stopPropagation());
|
|
1988
|
+
// Click-away / Escape closes the popover.
|
|
1989
|
+
document.addEventListener("click", () => setOpen(false));
|
|
1990
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape") setOpen(false); });
|
|
1991
|
+
for (const f of SAMP_FIELDS) {
|
|
1992
|
+
const e = $(f.id); if (!e) continue;
|
|
1993
|
+
e.addEventListener("input", () => onSlide(f));
|
|
1994
|
+
}
|
|
1995
|
+
const reset = $("samp-reset"); if (reset) reset.onclick = (e) => { e.stopPropagation(); resetSampling(); };
|
|
1996
|
+
updateSamplingUi(); refreshSamplingRecs();
|
|
1838
1997
|
}
|
|
1839
1998
|
|
|
1840
1999
|
/* — composer — */
|
|
@@ -1854,8 +2013,10 @@ controllers.chat = (() => {
|
|
|
1854
2013
|
addUserMsg(text, attachments);
|
|
1855
2014
|
box.value = ""; box.style.height = "auto";
|
|
1856
2015
|
const frame = imgs.length ? { text: combined, images: imgs } : { text: combined };
|
|
1857
|
-
|
|
1858
|
-
|
|
2016
|
+
// A composer submission is always a new user turn. If the model is still
|
|
2017
|
+
// streaming, the server passes it to pi with streamingBehavior:"followUp"
|
|
2018
|
+
// so it becomes the next turn instead of steering/mutating the current one.
|
|
2019
|
+
send({ type: "prompt", ...frame });
|
|
1859
2020
|
clearAttachments();
|
|
1860
2021
|
}
|
|
1861
2022
|
const scroll = () => $("chat-scroll");
|
|
@@ -1872,6 +2033,7 @@ controllers.chat = (() => {
|
|
|
1872
2033
|
$("chat-think").addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); thinkToggle(); } });
|
|
1873
2034
|
const adapterSel = $("chat-adapter");
|
|
1874
2035
|
if (adapterSel) adapterSel.onchange = () => onSelectAdapter(adapterSel);
|
|
2036
|
+
initSampling();
|
|
1875
2037
|
refreshAdapters();
|
|
1876
2038
|
const box = $("chat-box");
|
|
1877
2039
|
box.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); } });
|
|
@@ -1934,6 +2096,33 @@ controllers.quantize = (() => {
|
|
|
1934
2096
|
}));
|
|
1935
2097
|
}
|
|
1936
2098
|
|
|
2099
|
+
// --- OS folder picker / drag-drop → real on-disk snapshot path ---
|
|
2100
|
+
// The browser can't reveal an absolute path, and the HF cache uses symlinked
|
|
2101
|
+
// snapshots over a blobs/ store — so we DON'T read any files. We only take the
|
|
2102
|
+
// picked folder's NAME (which encodes the repo id) and let the server
|
|
2103
|
+
// reconstruct the snapshot path it owns. No upload, no "config.json" hunt.
|
|
2104
|
+
async function resolveByName(name, relPath) {
|
|
2105
|
+
if (!name && !relPath) return;
|
|
2106
|
+
const out = $("q-inspect-out");
|
|
2107
|
+
out.innerHTML = '<div class="flash"><span class="shimmer">locating on disk…</span></div>';
|
|
2108
|
+
const d = await api("/api/quantize/resolve-folder", { method: "POST", body: { folder_name: name || "", rel_path: relPath || "" } })
|
|
2109
|
+
.catch(() => ({ ok: false, error: "request failed" }));
|
|
2110
|
+
if (!d.ok) { out.innerHTML = '<div class="flash err">' + esc(d.error || "could not locate this folder on the server") + "</div>"; return; }
|
|
2111
|
+
$("q-model").value = d.path;
|
|
2112
|
+
inspect();
|
|
2113
|
+
}
|
|
2114
|
+
async function pickFolder() {
|
|
2115
|
+
// Native OS folder dialog with no "upload N files" prompt; we only read the
|
|
2116
|
+
// folder's .name, never its contents.
|
|
2117
|
+
if (window.showDirectoryPicker) {
|
|
2118
|
+
try {
|
|
2119
|
+
const handle = await window.showDirectoryPicker({ id: "mlx-models", mode: "read" });
|
|
2120
|
+
return resolveByName(handle.name);
|
|
2121
|
+
} catch (e) { return; } // user cancelled / denied
|
|
2122
|
+
}
|
|
2123
|
+
$("q-folder").click(); // fallback for browsers without the File System Access API
|
|
2124
|
+
}
|
|
2125
|
+
|
|
1937
2126
|
async function inspect() {
|
|
1938
2127
|
const id = $("q-model").value.trim();
|
|
1939
2128
|
if (!id) { toast("Enter a model id or path first.", "err"); return; }
|
|
@@ -1990,6 +2179,26 @@ controllers.quantize = (() => {
|
|
|
1990
2179
|
show(0);
|
|
1991
2180
|
$("q-inspect").onclick = inspect;
|
|
1992
2181
|
$("q-model").addEventListener("keydown", (e) => { if (e.key === "Enter") inspect(); });
|
|
2182
|
+
// OS folder dialog (native; resolves by folder name, no upload)
|
|
2183
|
+
$("q-browse").onclick = pickFolder;
|
|
2184
|
+
$("q-folder").addEventListener("change", (e) => {
|
|
2185
|
+
const f = e.target.files[0]; e.target.value = "";
|
|
2186
|
+
if (f) resolveByName((f.webkitRelativePath || f.name).split("/")[0], f.webkitRelativePath || "");
|
|
2187
|
+
});
|
|
2188
|
+
// drag a folder onto the box — use the directory entry's NAME (no read)
|
|
2189
|
+
const drop = $("q-drop");
|
|
2190
|
+
const hi = (on) => { drop.style.outline = on ? "2px dashed var(--blue)" : ""; drop.style.outlineOffset = on ? "4px" : ""; };
|
|
2191
|
+
["dragenter", "dragover"].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); hi(true); }));
|
|
2192
|
+
["dragleave", "drop"].forEach((ev) => drop.addEventListener(ev, (e) => {
|
|
2193
|
+
e.preventDefault(); hi(false);
|
|
2194
|
+
if (ev !== "drop" || !e.dataTransfer) return;
|
|
2195
|
+
for (const it of [...(e.dataTransfer.items || [])]) {
|
|
2196
|
+
const en = it.webkitGetAsEntry && it.webkitGetAsEntry();
|
|
2197
|
+
if (en && en.isDirectory) return resolveByName(en.name);
|
|
2198
|
+
}
|
|
2199
|
+
const f = e.dataTransfer.files && e.dataTransfer.files[0];
|
|
2200
|
+
if (f) resolveByName((f.webkitRelativePath || f.name).split("/")[0]);
|
|
2201
|
+
}));
|
|
1993
2202
|
seg($("q-bits"), (v) => { bits = +v; $("q-bits-hint").textContent = v === "4"
|
|
1994
2203
|
? "4-bit · ~4× smaller than bf16 · the recommended default for local serving."
|
|
1995
2204
|
: "8-bit · ~2× smaller than bf16 · highest fidelity quant."; });
|
|
@@ -2009,7 +2218,7 @@ controllers.quantize = (() => {
|
|
|
2009
2218
|
════════════════════════════════════════════════════════════════════ */
|
|
2010
2219
|
controllers.finetune = (() => {
|
|
2011
2220
|
const STEPS = ["Base", "Dataset", "Hyperparams", "Train", "Done"];
|
|
2012
|
-
let step = 0, method = "sft", lrTouched = false, es = null, log = [];
|
|
2221
|
+
let step = 0, method = "sft", lrTouched = false, rankTouched = false, es = null, log = [];
|
|
2013
2222
|
let datasetOk = false, adapterPath = "";
|
|
2014
2223
|
const trainPts = [], valPts = [];
|
|
2015
2224
|
|
|
@@ -2019,6 +2228,23 @@ controllers.finetune = (() => {
|
|
|
2019
2228
|
renderSteps($("f-steps"), STEPS, n);
|
|
2020
2229
|
}
|
|
2021
2230
|
|
|
2231
|
+
// OS folder picker for the base model — resolve by NAME server-side (no
|
|
2232
|
+
// upload, works with the HF cache's symlinked layout). Mirrors Quantize.
|
|
2233
|
+
async function resolveBaseByName(name, relPath) {
|
|
2234
|
+
if (!name && !relPath) return;
|
|
2235
|
+
const d = await api("/api/model/resolve-folder", { method: "POST", body: { folder_name: name || "", rel_path: relPath || "" } })
|
|
2236
|
+
.catch(() => ({ ok: false, error: "request failed" }));
|
|
2237
|
+
if (!d.ok) { toast(d.error || "could not locate this folder", "err"); return; }
|
|
2238
|
+
$("f-model").value = d.path;
|
|
2239
|
+
}
|
|
2240
|
+
async function pickBaseFolder() {
|
|
2241
|
+
if (window.showDirectoryPicker) {
|
|
2242
|
+
try { const h = await window.showDirectoryPicker({ id: "mlx-models", mode: "read" }); return resolveBaseByName(h.name); }
|
|
2243
|
+
catch (e) { return; }
|
|
2244
|
+
}
|
|
2245
|
+
$("f-folder").click();
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2022
2248
|
async function inspectDataset() {
|
|
2023
2249
|
const path = $("f-data").value.trim();
|
|
2024
2250
|
if (!path) { toast("Enter a dataset directory.", "err"); return; }
|
|
@@ -2043,6 +2269,8 @@ controllers.finetune = (() => {
|
|
|
2043
2269
|
? "Odds Ratio Preference Optimization (reference-free) — data is {prompt, chosen, rejected}."
|
|
2044
2270
|
: "Supervised fine-tuning — data is messages or prompt+completion.";
|
|
2045
2271
|
if (!lrTouched) $("f-lr").value = m === "dpo" ? 0.00005 : m === "orpo" ? 0.00001 : 0.0002;
|
|
2272
|
+
// ORPO's validated LoRA recipe uses rank 16 (vs 8 for SFT); apply unless edited.
|
|
2273
|
+
if (!rankTouched) $("f-rank").value = m === "orpo" ? 16 : 8;
|
|
2046
2274
|
}
|
|
2047
2275
|
|
|
2048
2276
|
function collectHP() {
|
|
@@ -2178,12 +2406,34 @@ controllers.finetune = (() => {
|
|
|
2178
2406
|
init() {
|
|
2179
2407
|
show(0);
|
|
2180
2408
|
$("f-next0").onclick = () => { if (!$("f-model").value.trim()) { toast("Enter a base model path.", "err"); return; } show(1); };
|
|
2409
|
+
// OS folder dialog for the base model (native; resolves by name, no upload)
|
|
2410
|
+
$("f-browse").onclick = pickBaseFolder;
|
|
2411
|
+
$("f-folder").addEventListener("change", (e) => {
|
|
2412
|
+
const f = e.target.files[0]; e.target.value = "";
|
|
2413
|
+
if (f) resolveBaseByName((f.webkitRelativePath || f.name).split("/")[0], f.webkitRelativePath || "");
|
|
2414
|
+
});
|
|
2415
|
+
{
|
|
2416
|
+
const drop = $("f-drop");
|
|
2417
|
+
const hi = (on) => { drop.style.outline = on ? "2px dashed var(--blue)" : ""; drop.style.outlineOffset = on ? "4px" : ""; };
|
|
2418
|
+
["dragenter", "dragover"].forEach((ev) => drop.addEventListener(ev, (e) => { e.preventDefault(); hi(true); }));
|
|
2419
|
+
["dragleave", "drop"].forEach((ev) => drop.addEventListener(ev, (e) => {
|
|
2420
|
+
e.preventDefault(); hi(false);
|
|
2421
|
+
if (ev !== "drop" || !e.dataTransfer) return;
|
|
2422
|
+
for (const it of [...(e.dataTransfer.items || [])]) {
|
|
2423
|
+
const en = it.webkitGetAsEntry && it.webkitGetAsEntry();
|
|
2424
|
+
if (en && en.isDirectory) return resolveBaseByName(en.name);
|
|
2425
|
+
}
|
|
2426
|
+
const f = e.dataTransfer.files && e.dataTransfer.files[0];
|
|
2427
|
+
if (f) resolveBaseByName((f.webkitRelativePath || f.name).split("/")[0]);
|
|
2428
|
+
}));
|
|
2429
|
+
}
|
|
2181
2430
|
$("f-inspect").onclick = inspectDataset;
|
|
2182
2431
|
$("f-data").addEventListener("keydown", (e) => { if (e.key === "Enter") inspectDataset(); });
|
|
2183
2432
|
$("f-data").addEventListener("input", () => { datasetOk = false; $("f-next1").disabled = true; $("f-inspect-out").innerHTML = ""; });
|
|
2184
2433
|
$("f-next1").onclick = () => { if (datasetOk) show(2); };
|
|
2185
2434
|
$("f-method").querySelectorAll("button").forEach((b) => b.onclick = () => setMethod(b.dataset.v));
|
|
2186
2435
|
$("f-lr").addEventListener("input", () => { lrTouched = true; });
|
|
2436
|
+
$("f-rank").addEventListener("input", () => { rankTouched = true; });
|
|
2187
2437
|
document.querySelectorAll("#s-finetune [data-fback]").forEach((b) => b.onclick = () => show(step - 1));
|
|
2188
2438
|
$("f-submit").onclick = submit;
|
|
2189
2439
|
$("f-merge-go").onclick = mergeAdapters;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory
|
|
3
|
+
description: Use when the answer depends on the user's prior conversations or personal context: explicit memory/recall requests, named personal projects, people, preferences, decisions, or history not explained in the current chat. Do not use for weather, current public facts, generic web research, or ordinary coding/file tasks when current files are sufficient. Also use when the user wants to set up, inspect, or manage memory.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory
|
|
7
|
+
|
|
8
|
+
The user has a personal **memory** — a wiki of Markdown articles synthesized
|
|
9
|
+
from their past conversations, stored locally at `~/.mlx-bun/wiki/`. User memory
|
|
10
|
+
articles live under `articles/`. Read-only mlx-bun reference docs live under
|
|
11
|
+
`Reference/` as symlinks and are also searchable/readable, but synthesis never
|
|
12
|
+
writes them. Each user article is the consolidated, finished understanding of one
|
|
13
|
+
topic. Your job is to *use it* so the user never has to re-explain what they've
|
|
14
|
+
already told you.
|
|
15
|
+
|
|
16
|
+
It is entirely local: the articles, the synthesis, and these tools all run on
|
|
17
|
+
the user's machine. Nothing about their memory leaves the device.
|
|
18
|
+
|
|
19
|
+
## Consulting memory (the common case)
|
|
20
|
+
|
|
21
|
+
When the answer depends on personal continuity — an explicit memory/recall
|
|
22
|
+
request, or a named project, person, decision, preference, or history that is
|
|
23
|
+
not explained in the current chat — consult memory before answering or asking
|
|
24
|
+
the user to re-explain.
|
|
25
|
+
|
|
26
|
+
Do **not** use memory for weather, current public facts, generic web research,
|
|
27
|
+
or ordinary coding/file tasks when the current files or the web tools answer the
|
|
28
|
+
question. Memory is for user-specific context, not for performing familiarity.
|
|
29
|
+
|
|
30
|
+
1. **Search.** Call `memory_search` with the topic (plain phrasing is fine —
|
|
31
|
+
a name or key noun). It returns ranked articles and sample lines.
|
|
32
|
+
2. **Read enough.** For short or central articles, call `memory_read`. For long
|
|
33
|
+
articles, use `memory_toc` then `memory_section` to read the relevant part.
|
|
34
|
+
The article is the answer — read it rather than reconstructing the user's
|
|
35
|
+
position from scattered fragments. Treat it as their current view.
|
|
36
|
+
3. **Follow nearby context when useful.** Use `memory_links` if backlinks or
|
|
37
|
+
related concepts matter. `memory_list` gives an overview when search comes up
|
|
38
|
+
short. mlx-bun reference docs are separate from personal memory; use
|
|
39
|
+
`reference_search`, `reference_read`, and `reference_list` for those.
|
|
40
|
+
`memory_status` answers setup/schedule/location questions.
|
|
41
|
+
4. **Use it silently, as a prior.** Let what you find shape your response like
|
|
42
|
+
a long-time colleague would — don't announce "I searched your memory."
|
|
43
|
+
|
|
44
|
+
Use memory as an invisible prior: it should narrow relevance and improve the
|
|
45
|
+
answer, not become the topic of the answer. Avoid performative recall — do not
|
|
46
|
+
name-drop unrelated remembered facts to sound familiar.
|
|
47
|
+
|
|
48
|
+
## Opening memory for the user
|
|
49
|
+
|
|
50
|
+
The wiki is a normal Markdown/Obsidian vault. If the user asks to browse, open,
|
|
51
|
+
inspect, or edit their memory directly, you may open it for them after a brief
|
|
52
|
+
confirmation:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
mlx-bun memory open
|
|
56
|
+
mlx-bun memory open <article>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The first form opens `~/.mlx-bun/wiki/` in Obsidian when available, falling back
|
|
60
|
+
to the folder in Finder. The second form opens a specific article stem such as
|
|
61
|
+
`Archie_Project` directly in Obsidian, falling back to the default Markdown app.
|
|
62
|
+
Opening the vault or article is allowed; editing articles during chat is not. If
|
|
63
|
+
the user wants a change to become durable memory, keep the conversation natural
|
|
64
|
+
and let synthesis incorporate it later, unless they explicitly ask to edit files
|
|
65
|
+
by hand in Obsidian.
|
|
66
|
+
|
|
67
|
+
## Setting up memory (if it doesn't exist yet)
|
|
68
|
+
|
|
69
|
+
If memory isn't set up (the memory tools aren't available, or the user asks to
|
|
70
|
+
turn it on), explain it plainly and set it up on agreement:
|
|
71
|
+
|
|
72
|
+
> Your memory is a folder of Markdown articles at `~/.mlx-bun/wiki/`,
|
|
73
|
+
> synthesized from your conversations so I can remember your context across
|
|
74
|
+
> sessions. It's a normal git-tracked wiki you fully own and can edit or
|
|
75
|
+
> delete — and it never leaves your machine.
|
|
76
|
+
|
|
77
|
+
On a yes, run the setup via the bash tool (the user approves it):
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
mlx-bun memory init
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This creates the vault and walks through optional steps (importing an existing
|
|
84
|
+
wiki, scheduling nightly synthesis). Confirm before each persistent action.
|
|
85
|
+
|
|
86
|
+
## What not to do
|
|
87
|
+
|
|
88
|
+
- Don't claim a fact "from memory" that you didn't read in an article — search
|
|
89
|
+
and read first, or say you don't have it.
|
|
90
|
+
- Don't write to or edit the wiki during a normal conversation; the read tools
|
|
91
|
+
are read-only by design. Synthesis is a separate, deliberate step.
|
|
92
|
+
- Do open the wiki or a specific article in Obsidian/Finder if the user asks to
|
|
93
|
+
browse or edit their memory themselves (`mlx-bun memory open [article]`).
|
|
94
|
+
- Don't dump article contents at the user verbatim — use them to inform a
|
|
95
|
+
direct, relevant answer.
|
|
96
|
+
- Don't perform familiarity. If the user asks for weather, current facts, or a
|
|
97
|
+
normal coding task, use the task-appropriate tool instead of memory.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: web-research
|
|
3
|
-
description: Use when the user asks you to research something, do a deep dive, fact-check a claim, compare options, or answer a question that needs current, accurate, real-world information ("what's the latest on…", prices, releases, news, specs).
|
|
3
|
+
description: Use only when the user asks you to research something, do a deep dive, fact-check a claim, compare options, or answer a question that needs current, accurate, real-world information ("what's the latest on…", prices, releases, news, specs). Do not use for ordinary conversation, ordinary coding tasks, or questions answerable from the current conversation/files.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Web Research
|
|
@@ -16,6 +16,8 @@ trustworthy, well-sourced answer — not a guess dressed up as one.
|
|
|
16
16
|
- The user wants a thorough or authoritative answer, or asks for sources.
|
|
17
17
|
- A specific claim needs verifying, or you would otherwise be speculating.
|
|
18
18
|
|
|
19
|
+
Do **not** use this for ordinary conversation that does not require external information. In that case, reply naturally instead of explaining the research workflow.
|
|
20
|
+
|
|
19
21
|
## Workflow
|
|
20
22
|
|
|
21
23
|
1. **Plan.** Restate the question in your own words and list the 2–4 concrete
|
package/src/web/skills.ts
CHANGED
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
// the user's personal ~/.pi and project .pi skills, so the chat stays
|
|
14
14
|
// isolated while still getting our curated set (resource-loader.js:277).
|
|
15
15
|
//
|
|
16
|
+
// Memory is deliberately gated by vault existence. If memory is not enabled,
|
|
17
|
+
// we do not expose its skill at all; otherwise small local models can fixate on
|
|
18
|
+
// the skill description even when the user asks unrelated things like weather.
|
|
19
|
+
//
|
|
16
20
|
// To add a skill: drop a folder under ./skills/<name>/SKILL.md, add a text
|
|
17
21
|
// import for it, and append it to BUNDLED_SKILLS.
|
|
18
22
|
|
|
@@ -20,6 +24,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
|
|
|
20
24
|
import { homedir } from "node:os";
|
|
21
25
|
import { join } from "node:path";
|
|
22
26
|
import webResearchMd from "./skills/web-research/SKILL.md" with { type: "text" };
|
|
27
|
+
import memoryMd from "./skills/memory/SKILL.md" with { type: "text" };
|
|
23
28
|
|
|
24
29
|
interface BundledSkill {
|
|
25
30
|
/** Directory name under the skills root; should match the SKILL.md `name`. */
|
|
@@ -30,15 +35,23 @@ interface BundledSkill {
|
|
|
30
35
|
|
|
31
36
|
const BUNDLED_SKILLS: readonly BundledSkill[] = [
|
|
32
37
|
{ name: "web-research", md: webResearchMd },
|
|
38
|
+
{ name: "memory", md: memoryMd },
|
|
33
39
|
];
|
|
34
40
|
|
|
41
|
+
export interface BundledSkillOptions {
|
|
42
|
+
/** Include the web-research workflow skill. Default false: tool schemas are enough, and small local models can over-apply workflow skills. */
|
|
43
|
+
includeWebResearch?: boolean;
|
|
44
|
+
/** Include the personal-memory skill. Default true for backward compatibility. */
|
|
45
|
+
includeMemory?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
/** Where the bundled skills are written; also pi's per-session scratch root. */
|
|
36
49
|
export function bundledSkillsRoot(): string {
|
|
37
50
|
return join(homedir(), ".mlx-bun", "skills");
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
/** Memoize: the embedded content can't change within a process run. */
|
|
41
|
-
|
|
54
|
+
const materialized = new Set<string>();
|
|
42
55
|
|
|
43
56
|
/**
|
|
44
57
|
* Write the embedded skills to disk (overwriting, so updates propagate) and
|
|
@@ -46,14 +59,39 @@ let materialized = false;
|
|
|
46
59
|
* pi scans it for per-skill SKILL.md files. Runs its disk writes once per
|
|
47
60
|
* process even when called per browser connection.
|
|
48
61
|
*/
|
|
49
|
-
|
|
62
|
+
function selectedSkills(opts: BundledSkillOptions = {}): readonly BundledSkill[] {
|
|
63
|
+
const includeMemory = opts.includeMemory ?? true;
|
|
64
|
+
const includeWebResearch = opts.includeWebResearch ?? false;
|
|
65
|
+
return BUNDLED_SKILLS.filter((skill) => {
|
|
66
|
+
if (!includeMemory && skill.name === "memory") return false;
|
|
67
|
+
if (!includeWebResearch && skill.name === "web-research") return false;
|
|
68
|
+
return true;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Materialize selected bundled skills and return exact skill directories. Use
|
|
74
|
+
* these paths in DefaultResourceLoader.additionalSkillPaths. Returning exact
|
|
75
|
+
* directories (rather than the shared root) prevents an old on-disk memory
|
|
76
|
+
* skill from being discovered when memory is disabled for the current session.
|
|
77
|
+
*/
|
|
78
|
+
export function materializeBundledSkillPaths(opts: BundledSkillOptions = {}): string[] {
|
|
50
79
|
const root = bundledSkillsRoot();
|
|
51
|
-
|
|
52
|
-
for (const skill of
|
|
80
|
+
const paths: string[] = [];
|
|
81
|
+
for (const skill of selectedSkills(opts)) {
|
|
53
82
|
const dir = join(root, skill.name);
|
|
54
|
-
|
|
55
|
-
|
|
83
|
+
if (!materialized.has(skill.name)) {
|
|
84
|
+
mkdirSync(dir, { recursive: true });
|
|
85
|
+
writeFileSync(join(dir, "SKILL.md"), skill.md);
|
|
86
|
+
materialized.add(skill.name);
|
|
87
|
+
}
|
|
88
|
+
paths.push(dir);
|
|
56
89
|
}
|
|
57
|
-
|
|
58
|
-
|
|
90
|
+
return paths;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Backward-compatible root materializer for older call sites. Prefer materializeBundledSkillPaths(). */
|
|
94
|
+
export function materializeBundledSkills(): string {
|
|
95
|
+
materializeBundledSkillPaths({ includeMemory: true, includeWebResearch: true });
|
|
96
|
+
return bundledSkillsRoot();
|
|
59
97
|
}
|