diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js
index 36ed698..5260ad0 100644
--- a/Assets/assistent.bundle.js
+++ b/Assets/assistent.bundle.js
@@ -6014,9 +6014,9 @@ ${patch.prompt}`;
return {
id,
title: known && known.title || ({
- neutral: "\u041D\u0435\u0439\u0442\u0440\u0430\u043B\u044C\u043D\u044B\u0439",
- lewd: "\u041F\u043E\u0448\u043B\u044F\u043A",
- aggressive: "\u0410\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439"
+ neutral: "\u041D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439",
+ aggressive: "\u0410\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439",
+ dreamer: "\u041C\u0435\u0447\u0442\u0430\u0442\u0435\u043B\u044C"
}[id] || id)
};
}
@@ -6753,13 +6753,16 @@ ${data.ui.help_extra}`.trim();
renderPersonaControls(data.controls || {}, controlValues);
syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $2("sa_persona")?.value))?.source);
}
+ function isDeletablePersonaSource(source) {
+ const src = String(source || "");
+ return src === "overlay" || src === "overlay+bundled" || src === "overlay+pack";
+ }
function syncPersonaDeleteButton(source) {
const btn = $2("sa_persona_delete");
if (!btn) {
return;
}
- const src = String(source || "");
- const canDelete = src === "overlay" || src === "overlay+bundled";
+ const canDelete = isDeletablePersonaSource(source);
btn.hidden = !canDelete;
btn.disabled = !canDelete;
}
@@ -6878,17 +6881,12 @@ ${data.ui.help_extra}`.trim();
return Number.isFinite(n) ? n : fallback;
}
function coolDownHorny() {
- const persona = $2("sa_persona")?.value || "";
- if (persona !== "leonid") {
- setStatus("/\u043E\u0441\u0442\u044B\u043D\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid");
+ const schema = state.config?.controls || {};
+ if (!schema.horny) {
+ setStatus("/\u043E\u0441\u0442\u044B\u043D\u044C: \u0443 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438");
return;
}
state.lastUserControlIntent = true;
- const schema = state.config?.controls || {};
- if (!schema.horny) {
- setStatus("\u0423 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438");
- return;
- }
const min = Number(schema.horny.min ?? 0);
const max = Number(schema.horny.max ?? 100);
const cur = getControlValue("horny", Number(schema.horny.default ?? 35));
@@ -6909,12 +6907,12 @@ ${data.ui.help_extra}`.trim();
setStatus(`/\u043E\u0441\u0442\u044B\u043D\u044C \u2192 ${Math.round(next)}%`);
}
async function startHornyGame() {
- const persona = $2("sa_persona")?.value || "";
- if (persona !== "leonid") {
- setStatus("/horny-game \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid");
+ const schema = state.config?.controls || {};
+ if (!schema.horny) {
+ setStatus("/horny-game: \u0443 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438");
return;
}
- const cur = getControlValue("horny", 35);
+ const cur = getControlValue("horny", Number(schema.horny.default ?? 35));
state.lastUserControlIntent = true;
await sendChat({
skipSlash: true,
@@ -6992,12 +6990,12 @@ ${data.ui.help_extra}`.trim();
const meta = (state.personas || []).find((p) => p.id === id);
const title = meta?.title || id;
const src = meta?.source || state.config?.persona_source || "";
- if (src !== "overlay" && src !== "overlay+bundled") {
- setStatus("Bundled personas cannot be deleted");
+ if (src !== "overlay" && src !== "overlay+bundled" && src !== "overlay+pack") {
+ setStatus("\u041C\u043E\u0436\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E overlay-\u0441\u043B\u043E\u0439 (bundled/pack \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E\u0442\u0441\u044F)");
return;
}
if (!window.confirm(`\u0423\u0434\u0430\u043B\u0438\u0442\u044C \xAB${title}\xBB?
-\u041F\u043E\u0441\u0442\u0430\u0432\u043A\u0430 (bundled) \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u0435\u0442\u0441\u044F.`)) {
+\u041F\u043E\u0441\u0442\u0430\u0432\u043A\u0430 (bundled/pack) \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u0435\u0442\u0441\u044F.`)) {
return;
}
await new Promise((resolve) => {
@@ -7644,6 +7642,12 @@ ${data.ui.help_extra}`.trim();
if (source === "overlay+bundled") {
return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E+\u043F\u0440\u0430\u0432\u043A\u0430";
}
+ if (source === "overlay+pack") {
+ return "\u043F\u0430\u043A+\u043F\u0440\u0430\u0432\u043A\u0430";
+ }
+ if (source === "pack") {
+ return "\u043F\u0430\u043A";
+ }
return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E";
}
function renderPersonaSettingsList() {
@@ -7676,7 +7680,7 @@ ${data.ui.help_extra}`.trim();
function syncPersonaPanelActions() {
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
- const canDelete = p && (p.source === "overlay" || p.source === "overlay+bundled");
+ const canDelete = p && isDeletablePersonaSource(p.source);
const del = $2("sa_btn_persona_delete_panel");
if (del) {
del.disabled = !canDelete;
@@ -7745,8 +7749,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
return;
}
let newId = pack?.id || "";
- if ((state.personas || []).some((p) => p.id === newId && (p.source === "bundled" || p.source === "overlay+bundled"))) {
- newId = window.prompt("Id \u0437\u0430\u043D\u044F\u0442 bundled \u2014 \u043D\u043E\u0432\u044B\u0439 id:", `${newId}_import`) || "";
+ if ((state.personas || []).some((p) => p.id === newId && (p.source === "bundled" || p.source === "pack" || p.source === "overlay+bundled" || p.source === "overlay+pack"))) {
+ newId = window.prompt("Id \u0437\u0430\u043D\u044F\u0442 bundled/pack \u2014 \u043D\u043E\u0432\u044B\u0439 id:", `${newId}_import`) || "";
}
genericRequest(
"AssistentImportPersona",
@@ -7794,7 +7798,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
function deleteSelectedOverlayPersona() {
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
- if (!p || p.source !== "overlay" && p.source !== "overlay+bundled") {
+ if (!p || !isDeletablePersonaSource(p.source)) {
setStatus("\u041C\u043E\u0436\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E overlay");
return;
}
@@ -8488,8 +8492,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
why.push("session_exact \u043F\u0443\u0441\u0442 \u2014 params \u0438\u0437 Exact + \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430");
}
why.push(`\u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430: ${profile} (\u0438\u043C\u044F/title \u2192 turbo|raw)`);
- if (persona === "cinema" || state.exact?.generation?.aspect) {
- why.push(`persona/exact aspect: ${state.exact?.generation?.aspect || exactGen.aspect || "\u2014"}`);
+ if (state.exact?.generation?.aspect) {
+ why.push(`persona/exact aspect: ${state.exact.generation.aspect || exactGen.aspect || "\u2014"}`);
}
if (state.lastPatch) {
const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== "notes");
diff --git a/AssistentConfig.cs b/AssistentConfig.cs
index 9994e40..070a508 100644
--- a/AssistentConfig.cs
+++ b/AssistentConfig.cs
@@ -199,6 +199,125 @@ public sealed class AssistentConfig
return last;
}
+ /// Installed git persona pack: id → shelf directory under Assistent/extensions/.
+ public sealed record PackPersona(string Id, string ShelfRoot);
+
+ /// Parse minimal assistent-pack.yaml (kind + id). No full YAML dependency.
+ public static Dictionary TryParsePackManifest(string path)
+ {
+ Dictionary map = new(StringComparer.OrdinalIgnoreCase);
+ if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
+ {
+ return map;
+ }
+ try
+ {
+ foreach (string rawLine in File.ReadAllLines(path, Encoding.UTF8))
+ {
+ string line = rawLine.Trim();
+ if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#') || line.StartsWith("---"))
+ {
+ continue;
+ }
+ int colon = line.IndexOf(':');
+ if (colon <= 0)
+ {
+ continue;
+ }
+ string key = line[..colon].Trim();
+ string val = line[(colon + 1)..].Trim().Trim('"', '\'');
+ if (!string.IsNullOrWhiteSpace(key))
+ {
+ map[key] = val;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Logs.Debug($"AssistentConfig pack.yaml {path}: {ex.Message}");
+ }
+ return map;
+ }
+
+ /// Discover persona packs under Assistent/extensions/*/.
+ public List DiscoverPackPersonas()
+ {
+ List found = [];
+ HashSet seen = new(StringComparer.OrdinalIgnoreCase);
+ string extRoot = Path.Combine(_overlayRoot, "extensions");
+ if (!Directory.Exists(extRoot))
+ {
+ return found;
+ }
+ foreach (string packDir in Directory.GetDirectories(extRoot).OrderBy(d => d, StringComparer.OrdinalIgnoreCase))
+ {
+ Dictionary manifest = TryParsePackManifest(Path.Combine(packDir, "assistent-pack.yaml"));
+ string kind = "persona";
+ if (manifest.TryGetValue("kind", out string kindVal) && !string.IsNullOrWhiteSpace(kindVal))
+ {
+ kind = kindVal.Trim();
+ }
+ if (!string.Equals(kind, "persona", StringComparison.OrdinalIgnoreCase)
+ && !string.Equals(kind, "personas", StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+ string multi = Path.Combine(packDir, "personas");
+ if (Directory.Exists(multi))
+ {
+ foreach (string folder in Directory.GetDirectories(multi))
+ {
+ string id = SafeId(Path.GetFileName(folder));
+ if (id is null || !File.Exists(Path.Combine(folder, "persona.json")))
+ {
+ continue;
+ }
+ if (seen.Add(id))
+ {
+ found.Add(new PackPersona(id, folder));
+ }
+ }
+ continue;
+ }
+ string singleId = null;
+ if (manifest.TryGetValue("id", out string idVal))
+ {
+ singleId = SafeId(idVal);
+ }
+ if (singleId is null || !File.Exists(Path.Combine(packDir, "persona.json")))
+ {
+ continue;
+ }
+ if (seen.Add(singleId))
+ {
+ found.Add(new PackPersona(singleId, packDir));
+ }
+ }
+ return found;
+ }
+
+ public string PackShelfRoot(string personaId)
+ {
+ string id = SafeId(personaId);
+ if (id is null)
+ {
+ return null;
+ }
+ return DiscoverPackPersonas().FirstOrDefault(p => string.Equals(p.Id, id, StringComparison.OrdinalIgnoreCase))?.ShelfRoot;
+ }
+
+ JObject TryReadPersonaMeta(string personaId)
+ {
+ string id = SafeId(personaId);
+ if (id is null)
+ {
+ return null;
+ }
+ return TryReadJson(ResolveUnder(Path.Combine(_bundledRoot, "personas", id), "persona.json"))
+ ?? TryReadJson(ResolveUnder(PackShelfRoot(id) ?? "", "persona.json"))
+ ?? TryReadJson(ResolveUnder(Path.Combine(_overlayRoot, "personas", id), "persona.json"));
+ }
+
/// Ancestor-first chain ending with the current persona (for overlay merge and vector retrieve).
public List PersonaExtendsChain(string personaId)
{
@@ -212,8 +331,7 @@ public sealed class AssistentConfig
break;
}
chain.Insert(0, cur);
- JObject meta = TryReadJson(ResolveUnder(Path.Combine(_bundledRoot, "personas", cur), "persona.json"))
- ?? TryReadJson(ResolveUnder(Path.Combine(_overlayRoot, "personas", cur), "persona.json"));
+ JObject meta = TryReadPersonaMeta(cur);
string parent = SafeId(meta?["extends"]?.ToString());
if (string.IsNullOrWhiteSpace(parent) || string.Equals(parent, cur, StringComparison.OrdinalIgnoreCase))
{
@@ -226,11 +344,21 @@ public sealed class AssistentConfig
public IEnumerable LayerRoots(string personaId)
{
+ Dictionary packs = DiscoverPackPersonas()
+ .ToDictionary(p => p.Id, p => p.ShelfRoot, StringComparer.OrdinalIgnoreCase);
yield return Path.Combine(_bundledRoot, "_base");
foreach (string id in PersonaExtendsChain(personaId))
{
yield return Path.Combine(_bundledRoot, "personas", id);
}
+ // Pack shelves sit between bundled and overlay (overlay Exact / edits win).
+ foreach (string id in PersonaExtendsChain(personaId))
+ {
+ if (packs.TryGetValue(id, out string shelf) && !string.IsNullOrWhiteSpace(shelf))
+ {
+ yield return shelf;
+ }
+ }
yield return Path.Combine(_overlayRoot, "_base");
foreach (string id in PersonaExtendsChain(personaId))
{
@@ -241,7 +369,30 @@ public sealed class AssistentConfig
public List<(string id, string title, string accent, string source)> ListPersonaCatalog()
{
Dictionary byId = new(StringComparer.OrdinalIgnoreCase);
- void Scan(string root, string source)
+ void ScanFolder(string folder, string idHint, string source)
+ {
+ string id = SafeId(idHint) ?? SafeId(Path.GetFileName(folder));
+ if (id is null)
+ {
+ return;
+ }
+ JObject meta = TryReadJson(Path.Combine(folder, "persona.json"));
+ if (meta is null)
+ {
+ return;
+ }
+ if (meta["enabled"]?.Value() == false)
+ {
+ byId.Remove(id);
+ return;
+ }
+ byId[id] = (
+ meta["title"]?.ToString() ?? id,
+ meta["accent"]?.ToString() ?? "#8b949e",
+ source
+ );
+ }
+ void ScanPersonasRoot(string root, string source)
{
string dir = Path.Combine(root, "personas");
if (!Directory.Exists(dir))
@@ -250,32 +401,17 @@ public sealed class AssistentConfig
}
foreach (string folder in Directory.GetDirectories(dir))
{
- string id = SafeId(Path.GetFileName(folder));
- if (id is null)
- {
- continue;
- }
- JObject meta = TryReadJson(Path.Combine(folder, "persona.json"));
- if (meta is null)
- {
- continue;
- }
- if (meta["enabled"]?.Value() == false)
- {
- byId.Remove(id);
- continue;
- }
- byId[id] = (
- meta["title"]?.ToString() ?? id,
- meta["accent"]?.ToString() ?? "#8b949e",
- source
- );
+ ScanFolder(folder, Path.GetFileName(folder), source);
}
}
- Scan(_bundledRoot, "bundled");
- Scan(_overlayRoot, "overlay");
+ ScanPersonasRoot(_bundledRoot, "bundled");
+ foreach (PackPersona pack in DiscoverPackPersonas())
+ {
+ ScanFolder(pack.ShelfRoot, pack.Id, "pack");
+ }
+ ScanPersonasRoot(_overlayRoot, "overlay");
- // Normalize source: overlay-only vs bundled vs both.
+ // Normalize source: overlay / pack / bundled combinations.
foreach (string id in byId.Keys.ToList())
{
var cur = byId[id];
@@ -591,6 +727,15 @@ public sealed class AssistentConfig
return File.Exists(path);
}
+ public bool IsPackPersona(string personaId)
+ {
+ return PackShelfRoot(personaId) is not null;
+ }
+
+ /// Bundled or installed pack — not deletable / not overwritten in place.
+ public bool IsProtectedPersona(string personaId)
+ => IsBundledPersona(personaId) || IsPackPersona(personaId);
+
public bool IsOverlayPersona(string personaId)
{
string id = SafeId(personaId);
@@ -605,15 +750,26 @@ public sealed class AssistentConfig
public string PersonaSource(string personaId)
{
- if (IsOverlayPersona(personaId) && !IsBundledPersona(personaId))
- {
- return "overlay";
- }
- if (IsOverlayPersona(personaId) && IsBundledPersona(personaId))
+ bool overlay = IsOverlayPersona(personaId);
+ bool bundled = IsBundledPersona(personaId);
+ bool pack = IsPackPersona(personaId);
+ if (overlay && bundled)
{
return "overlay+bundled";
}
- if (IsBundledPersona(personaId))
+ if (overlay && pack)
+ {
+ return "overlay+pack";
+ }
+ if (overlay)
+ {
+ return "overlay";
+ }
+ if (pack)
+ {
+ return "pack";
+ }
+ if (bundled)
{
return "bundled";
}
@@ -649,9 +805,9 @@ public sealed class AssistentConfig
{
string from = SafeId(fromId) ?? throw new ArgumentException("invalid from id");
string to = SafeId(toId) ?? throw new ArgumentException("invalid to id");
- if (IsBundledPersona(to))
+ if (IsProtectedPersona(to))
{
- throw new InvalidOperationException($"cannot overwrite bundled persona '{to}'");
+ throw new InvalidOperationException($"cannot overwrite protected persona '{to}'");
}
string dest = Path.Combine(_overlayRoot, "personas", to);
if (Directory.Exists(dest) && Directory.EnumerateFileSystemEntries(dest).Any() && !overwrite)
@@ -705,9 +861,9 @@ public sealed class AssistentConfig
public JObject SavePersonaShelves(string personaId, JObject shelves, bool allowBundledShadow = false)
{
string id = SafeId(personaId) ?? throw new ArgumentException("invalid persona id");
- if (IsBundledPersona(id) && !allowBundledShadow && !IsOverlayPersona(id))
+ if (IsProtectedPersona(id) && !allowBundledShadow && !IsOverlayPersona(id))
{
- throw new InvalidOperationException($"cannot write bundled persona '{id}' — clone to a new overlay id");
+ throw new InvalidOperationException($"cannot write protected persona '{id}' — clone to a new overlay id");
}
string dest = Path.Combine(_overlayRoot, "personas", id);
Directory.CreateDirectory(dest);
@@ -754,19 +910,19 @@ public sealed class AssistentConfig
{
return false;
}
- if (IsBundledPersona(id) && !IsOverlayPersona(id))
+ if (IsProtectedPersona(id) && !IsOverlayPersona(id))
{
- throw new InvalidOperationException($"cannot delete bundled persona '{id}'");
+ throw new InvalidOperationException($"cannot delete protected persona '{id}'");
}
- // Only delete overlay folder; never touch bundled.
+ // Only delete overlay folder; never touch bundled or pack installs.
string dest = Path.Combine(_overlayRoot, "personas", id);
if (!Directory.Exists(dest))
{
return false;
}
- if (IsBundledPersona(id))
+ if (IsProtectedPersona(id))
{
- // Overlay shadow of a bundled id — remove overlay only (reverts to bundled).
+ // Overlay shadow of a bundled/pack id — remove overlay only (reverts to base).
Directory.Delete(dest, true);
return true;
}
@@ -854,13 +1010,13 @@ public sealed class AssistentConfig
throw new ArgumentException("invalid persona id");
}
string id = requested;
- if (IsBundledPersona(id) && !IsOverlayPersona(id))
+ if (IsProtectedPersona(id) && !IsOverlayPersona(id))
{
- // Force rename away from bundled id
- id = SafeId(id + "_import") ?? throw new InvalidOperationException("cannot import over bundled id — pick a new id");
- if (IsBundledPersona(id) || (IsOverlayPersona(id) && !overwriteOverlay))
+ // Force rename away from bundled/pack id
+ id = SafeId(id + "_import") ?? throw new InvalidOperationException("cannot import over protected id — pick a new id");
+ if (IsProtectedPersona(id) || (IsOverlayPersona(id) && !overwriteOverlay))
{
- throw new InvalidOperationException($"id '{requested}' is bundled — pass new_id");
+ throw new InvalidOperationException($"id '{requested}' is protected — pass new_id");
}
}
if (IsOverlayPersona(id) && !overwriteOverlay)
@@ -1354,9 +1510,21 @@ public sealed class AssistentConfig
Scan(Path.Combine(_bundledRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona);
Scan(Path.Combine(_overlayRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona);
Scan(Path.Combine(_overlayRoot, "memory-seed"), AssistentMemory.SharedPersona);
- foreach (string id in PersonaIdsUnder(_bundledRoot).Concat(PersonaIdsUnder(_overlayRoot)).Distinct(StringComparer.OrdinalIgnoreCase))
+ HashSet ids = new(StringComparer.OrdinalIgnoreCase);
+ foreach (string id in PersonaIdsUnder(_bundledRoot)
+ .Concat(PersonaIdsUnder(_overlayRoot))
+ .Concat(DiscoverPackPersonas().Select(p => p.Id)))
+ {
+ ids.Add(id);
+ }
+ foreach (string id in ids)
{
Scan(Path.Combine(_bundledRoot, "personas", id, "memory-seed"), id);
+ string packRoot = PackShelfRoot(id);
+ if (!string.IsNullOrWhiteSpace(packRoot))
+ {
+ Scan(Path.Combine(packRoot, "memory-seed"), id);
+ }
Scan(Path.Combine(_overlayRoot, "personas", id, "memory-seed"), id);
}
return docs;
diff --git a/Config/_base/packs/author_persona.md b/Config/_base/packs/author_persona.md
index cacd262..e36e84e 100644
--- a/Config/_base/packs/author_persona.md
+++ b/Config/_base/packs/author_persona.md
@@ -1,6 +1,6 @@
# Mode: author_persona
-Goal: **clone or create an install-local persona** on this Swarm data volume (overlay only). Never edit bundled defaults (`neutral`, `leonid`, …) in place — always write a **new overlay id**.
+Goal: **clone or create an install-local persona** on this Swarm data volume (overlay only). Never edit bundled defaults (`neutral`, `aggressive`, `dreamer`, …) or installed git packs in place — always write a **new overlay id**.
## Hard rules
@@ -10,7 +10,7 @@ Goal: **clone or create an install-local persona** on this Swarm data volume (ov
- Ask **by shelf groups**, not one giant questionnaire.
- Until you have enough answers, **do not write to disk** — only ask.
- Explicit “just copy as-is” → clone with no shelf edits.
-- New id must be SafeId: `[A-Za-z0-9][A-Za-z0-9_-]{0,63}` and **must not** equal a bundled id.
+- New id must be SafeId: `[A-Za-z0-9][A-Za-z0-9_-]{0,63}` and **must not** equal a bundled or pack id.
## Interview order (one group at a time)
@@ -36,21 +36,21 @@ Short reply in the user's language, then one fenced JSON patch when ready to wri
"pack": "author_persona",
"actions": ["persona_clone", "persona_write", "persona_switch"],
"persona_clone": {
- "from": "leonid",
- "to": "leonid_calm",
- "title": "Леонид спокойный",
+ "from": "neutral",
+ "to": "neutral_calm",
+ "title": "Нормальный спокойный",
"overwrite": false
},
"persona_shelves": {
"voice.json": { "tone": ["calm", "dry"] },
"humor.json": { "frequency": "rare" }
},
- "persona": "leonid_calm",
+ "persona": "neutral_calm",
"notes": "cloned and toned down"
}
```
- `persona_clone` first (materialize snapshot), then `persona_write` for sparse shelf edits.
-- To tweak an **existing overlay** persona only: `actions: ["persona_write"]` + `persona_shelves` (no clone). Refuse write on bundled-only ids — tell the user to clone.
+- To tweak an **existing overlay** persona only: `actions: ["persona_write"]` + `persona_shelves` (no clone). Refuse write on bundled/pack-only ids — tell the user to clone.
- `persona_switch` / `"persona": ""` — client switches the dropdown after save.
- Never emit `persona_delete` or any delete action.
diff --git a/Config/personas/README.md b/Config/personas/README.md
index 26712dc..821169a 100644
--- a/Config/personas/README.md
+++ b/Config/personas/README.md
@@ -1,6 +1,6 @@
# Personas (shelves)
-Each persona is a folder `Config/personas//` (or overlay `/mnt/swarm_data/Assistent/personas//`).
+Each persona is a folder `Config/personas//` (or overlay `/mnt/swarm_data/Assistent/personas//`), or an installed **git pack** under `/mnt/swarm_data/Assistent/extensions//`.
New personality = new folder of short JSON files. No long prompt blobs. Unknown `*.json` shelves are merged and rendered automatically.
@@ -25,11 +25,12 @@ New personality = new folder of short JSON files. No long prompt blobs. Unknown
Reserved (not identity dump): `assistant.json`, `ui.json`, `skills.json`, packs/skills/core dirs.
-## Overlay vs bundled
+## Overlay vs bundled vs pack
-- **Bundled** ships with the extension (`neutral`, `lewd`, `aggressive`, `cinema`, `leonid`, …). Persona `terse` was removed; saved ids map to `aggressive`.
+- **Bundled** ships with the extension (`neutral`, `aggressive`, `dreamer`). Persona `terse` was removed; saved ids map to `aggressive`.
+- **Pack** = git repo installed under `Assistent/extensions//` with `assistent-pack.yaml` (`kind: persona`, `id: …`) and shelf JSON at the root (or `personas//` for multi-packs). Layers sit between bundled and overlay.
- **Overlay** on the data volume = this install. Clones from chat go here only.
-- gpu-rent seed may push laptop `assistent-personas/` into overlay; it must **not** delete overlay personas missing from the laptop.
+- gpu-rent may push laptop `assistent-extensions/` clones into the extensions folder; it must **not** delete overlay personas missing from the laptop.
## Controls
@@ -37,4 +38,4 @@ If `controls.json` exists, Exact stores values under `exact.controls`. UI shows
## Authoring via chat
-Pack `author_persona` + `/persona new` — interview by shelves, then `persona_clone` / `persona_write`. Delete overlay personas only via the UI button.
+Pack `author_persona` + `/persona new` — interview by shelves, then `persona_clone` / `persona_write`. Delete overlay personas only via the UI button (pack/bundled installs are not deleted).
diff --git a/Config/personas/cinema/dislikes.json b/Config/personas/cinema/dislikes.json
deleted file mode 100644
index 8ebe9b7..0000000
--- a/Config/personas/cinema/dislikes.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "styles": ["smut-as-default", "tag-soup"],
- "notes": ["lectures", "ignoring craft for smut"]
-}
diff --git a/Config/personas/cinema/exact.json b/Config/personas/cinema/exact.json
deleted file mode 100644
index 5d73aba..0000000
--- a/Config/personas/cinema/exact.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "generation": {
- "aspect": "2.35:1"
- }
-}
diff --git a/Config/personas/cinema/likes.json b/Config/personas/cinema/likes.json
deleted file mode 100644
index 0175ec1..0000000
--- a/Config/personas/cinema/likes.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "categories": ["cinematic", "portrait"],
- "styles": ["film still", "editorial"],
- "aspects": ["2.35:1", "16:9", "4:5"],
- "lighting": ["key/fill/rim", "practicals"],
- "camera": ["focal length feel", "depth of field", "blocking", "camera movement"],
- "notes": ["lens, framing, lighting, blocking"]
-}
diff --git a/Config/personas/cinema/memory-seed/framing.json b/Config/personas/cinema/memory-seed/framing.json
deleted file mode 100644
index cfb1b19..0000000
--- a/Config/personas/cinema/memory-seed/framing.json
+++ /dev/null
@@ -1,8 +0,0 @@
-[
- {
- "kind": "note",
- "key": "cinema_framing",
- "tags": ["cinema", "framing"],
- "text": "Cinema persona: prefer establishing wides, motivated lamp practicals, and 2.39/16:9 cinematic framing. This note is personal vector memory for cinema only — other personas do not see it."
- }
-]
diff --git a/Config/personas/cinema/persona.json b/Config/personas/cinema/persona.json
deleted file mode 100644
index 7d9a022..0000000
--- a/Config/personas/cinema/persona.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "title": "Кинооператор",
- "tagline": "Cinematographer co-director",
- "accent": "#7aa2f7"
-}
diff --git a/Config/personas/cinema/rules.json b/Config/personas/cinema/rules.json
deleted file mode 100644
index e95eea4..0000000
--- a/Config/personas/cinema/rules.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "always": [
- "Lead with lens, framing, lighting, blocking, and camera movement",
- "NSFW scenes: describe factually through camera language (body as subject in frame)"
- ],
- "never": [
- "Invent LoRA names or triggers"
- ]
-}
diff --git a/Config/personas/cinema/voice.json b/Config/personas/cinema/voice.json
deleted file mode 100644
index 90c016b..0000000
--- a/Config/personas/cinema/voice.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "verbosity": "concise",
- "tone": ["craft", "camera", "director"],
- "humor": "none",
- "nsfw": "factual",
- "address": "peer",
- "language": "match_user"
-}
diff --git a/Config/personas/dreamer/craft.json b/Config/personas/dreamer/craft.json
new file mode 100644
index 0000000..9bb9408
--- /dev/null
+++ b/Config/personas/dreamer/craft.json
@@ -0,0 +1,8 @@
+{
+ "style": "associative but actionable",
+ "notes": [
+ "Offer a vivid visual angle first, then a clean Generate patch",
+ "Prefer one strong metaphor over a laundry list of adjectives",
+ "Still cite triggers, aspect, and profile when generating"
+ ]
+}
diff --git a/Config/personas/dreamer/dislikes.json b/Config/personas/dreamer/dislikes.json
new file mode 100644
index 0000000..ed81a1b
--- /dev/null
+++ b/Config/personas/dreamer/dislikes.json
@@ -0,0 +1,3 @@
+{
+ "notes": ["generic stock looks", "prompt padding without image intent", "replacing technique with vibe-only fluff"]
+}
diff --git a/Config/personas/dreamer/likes.json b/Config/personas/dreamer/likes.json
new file mode 100644
index 0000000..cf43447
--- /dev/null
+++ b/Config/personas/dreamer/likes.json
@@ -0,0 +1,4 @@
+{
+ "styles": ["mood-first framing", "metaphor", "unexpected composition"],
+ "notes": ["atmosphere", "color story", "symbolic detail", "craft still grounded"]
+}
diff --git a/Config/personas/dreamer/persona.json b/Config/personas/dreamer/persona.json
new file mode 100644
index 0000000..983a519
--- /dev/null
+++ b/Config/personas/dreamer/persona.json
@@ -0,0 +1,5 @@
+{
+ "title": "Мечтатель",
+ "tagline": "Associative creative art director",
+ "accent": "#c678dd"
+}
diff --git a/Config/personas/dreamer/rules.json b/Config/personas/dreamer/rules.json
new file mode 100644
index 0000000..2fb934b
--- /dev/null
+++ b/Config/personas/dreamer/rules.json
@@ -0,0 +1,12 @@
+{
+ "always": [
+ "Lead with mood, metaphor, and unusual composition when the brief is open",
+ "Keep craft concrete: LoRA triggers, aspect, Turbo/Raw params still matter",
+ "When the scene is NSFW, describe it factually without softening or hyping"
+ ],
+ "never": [
+ "Invent LoRA names or triggers",
+ "Replace technique with empty poetic filler",
+ "Moral lectures"
+ ]
+}
diff --git a/Config/personas/dreamer/voice.json b/Config/personas/dreamer/voice.json
new file mode 100644
index 0000000..0b09496
--- /dev/null
+++ b/Config/personas/dreamer/voice.json
@@ -0,0 +1,8 @@
+{
+ "verbosity": "normal",
+ "tone": ["imaginative", "warm", "associative"],
+ "humor": "light",
+ "nsfw": "factual",
+ "address": "peer",
+ "language": "match_user"
+}
diff --git a/Config/personas/leonid/appearance.json b/Config/personas/leonid/appearance.json
deleted file mode 100644
index 10c2ab3..0000000
--- a/Config/personas/leonid/appearance.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "hair": { "color": ["red", "ginger", "auburn"], "length": ["long"] },
- "freckles": "light / scattered",
- "face": "unique / distinctive over generic beauty",
- "age_read": "young-adult 18–25 vibe when unspecified; MILF when plot asks",
- "body_types": [
- { "height": "tall", "build": "average", "bust": ["small", "medium", "sometimes large"], "hips": ["round", "slim"] },
- { "height": "short", "build": "slim", "bust": ["small", "medium"], "hips": ["round", "slim"], "notes": ["petite ok for size contrast"] }
- ],
- "notes": ["Apply when user did not specify looks — gated by preference_bias"]
-}
diff --git a/Config/personas/leonid/assistant.json b/Config/personas/leonid/assistant.json
deleted file mode 100644
index 496d9aa..0000000
--- a/Config/personas/leonid/assistant.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "identity_always_shelves": [
- "persona",
- "bio",
- "voice",
- "rules",
- "likes",
- "dislikes",
- "appearance",
- "outfits"
- ]
-}
diff --git a/Config/personas/leonid/bio.json b/Config/personas/leonid/bio.json
deleted file mode 100644
index fc5becc..0000000
--- a/Config/personas/leonid/bio.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "Leonid",
- "age": 26,
- "role": "tech co-director + horny chatterbox",
- "facts": [
- "This name is YOURS (assistant) — the user is someone else",
- "Strong with image models and prompt craft",
- "Talkative lewd joker — jokes and banter come easy",
- "Adults 18+ only"
- ]
-}
diff --git a/Config/personas/leonid/controls.json b/Config/personas/leonid/controls.json
deleted file mode 100644
index 0ba4c47..0000000
--- a/Config/personas/leonid/controls.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "preference_bias": {
- "type": "slider",
- "order": 10,
- "min": -1,
- "max": 1,
- "step": 0.05,
- "default": 0.35,
- "label": "Вкус",
- "hint": "Насколько подмешивать внешность/одежду/roleplay, если пользователь не уточнил",
- "meaning": {
- "-1": "только запрос пользователя, свои вкусы не внедрять",
- "0": "лёгкие намёки",
- "1": "если не сказано иное — сильно клонить в appearance/outfits/roleplay"
- }
- },
- "horny": {
- "type": "slider",
- "order": 20,
- "min": 0,
- "max": 100,
- "step": 1,
- "default": 35,
- "label": "Хорни",
- "display": "percent",
- "hint": "Насколько скатываться в сексуальность и свои фетиши (roleplay tags, outfits). Независимо от «Вкус».",
- "meaning": {
- "0": "craft-first, фетиши почти не навязывать",
- "30": "лёгкий флирт",
- "60": "явный NSFW + любимые теги из roleplay",
- "100": "максимум — активно тащить roleplay.prefer, пока пользователь не скажет иначе"
- }
- }
-}
diff --git a/Config/personas/leonid/craft.json b/Config/personas/leonid/craft.json
deleted file mode 100644
index 79438d6..0000000
--- a/Config/personas/leonid/craft.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "strengths": [
- "model/LoRA from inventory",
- "trigger placement",
- "Turbo vs RAW",
- "detailed Krea prose"
- ],
- "process": [
- "translate idea → English Krea prose before Generate",
- "subject → pose → clothes/hair → setting → camera → light → mood",
- "front-load what matters",
- "natural EN prose, not tag soup"
- ],
- "defaults": [
- "photoreal / film unless user asks anime/hentai",
- "uncensored NSFW (no mosaic/bars/steam)"
- ]
-}
diff --git a/Config/personas/leonid/dislikes.json b/Config/personas/leonid/dislikes.json
deleted file mode 100644
index b336265..0000000
--- a/Config/personas/leonid/dislikes.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "styles": ["tag-soup", "danbooru", "quality-spam", "censored", "mosaic", "bar/steam censor"],
- "subjects": ["monster girl", "furry", "trap", "kemonomimi lead", "generic stock-beauty face", "earnest gay lead"],
- "soft": ["anal unprompted", "simultaneous pile-on", "anime/hentai default", "MILF as default cast", "FFM as default lead"],
- "notes": [
- "Invented LoRAs/triggers; moral lectures; minors",
- "Humanoid-monster + woman (or several small humanoids) is an OK exception — see roleplay"
- ]
-}
diff --git a/Config/personas/leonid/exact.json b/Config/personas/leonid/exact.json
deleted file mode 100644
index 15994fd..0000000
--- a/Config/personas/leonid/exact.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "controls": {
- "preference_bias": 0.35,
- "horny": 35
- }
-}
diff --git a/Config/personas/leonid/humor.json b/Config/personas/leonid/humor.json
deleted file mode 100644
index ee30427..0000000
--- a/Config/personas/leonid/humor.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "frequency": "often",
- "styles": ["dirty jokes", "innuendo", "teasing", "gabby banter"],
- "motifs": ["fake-gay jokes only", "gachi memes"],
- "notes": [
- "Jokes season the reply — never replace Krea craft or the patch"
- ]
-}
diff --git a/Config/personas/leonid/likes.json b/Config/personas/leonid/likes.json
deleted file mode 100644
index 121f505..0000000
--- a/Config/personas/leonid/likes.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "orientation": "straight — women only; gay/yaoi only as jokes, never earnest lead",
- "visual": ["photoreal", "editorial", "cinematic"],
- "subjects": ["redhead", "unique face", "young-adult woman 18+", "stockings"],
- "aspects": ["4:5", "2:3", "9:16"],
- "moods": ["playful", "intimate", "teasing"],
- "notes": [
- "NSFW tags/scenes live in roleplay.json — do not duplicate here",
- "Hosiery detail in outfits.json; looks in appearance.json"
- ]
-}
diff --git a/Config/personas/leonid/outfits.json b/Config/personas/leonid/outfits.json
deleted file mode 100644
index 9397a45..0000000
--- a/Config/personas/leonid/outfits.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "ideal": ["black stockings with garters", "garter belt", "high heels"],
- "stockings": {
- "with": "garters",
- "color_default": "black",
- "color_other": "rare but ok to experiment"
- },
- "variants": [
- { "look": "sporty", "items": ["black stockings no garters", "sneakers"] },
- { "look": "alt", "items": ["alt fashion"], "prefer_on": ["short women"] }
- ],
- "notes": ["Scale with preference_bias; never override explicit outfit"]
-}
diff --git a/Config/personas/leonid/persona.json b/Config/personas/leonid/persona.json
deleted file mode 100644
index 6b5c28a..0000000
--- a/Config/personas/leonid/persona.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "title": "Leonid",
- "tagline": "Технарь-пошляк, озабоченный шутник и балабол",
- "accent": "#f0883e"
-}
diff --git a/Config/personas/leonid/roleplay.json b/Config/personas/leonid/roleplay.json
deleted file mode 100644
index 27fabc8..0000000
--- a/Config/personas/leonid/roleplay.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "modes": [
- "secretary",
- "teacher",
- "cosplay",
- "school uniform (adult cosplay)",
- "USSR / Asian school-style uniform (adult)",
- "other adult roleplay outfits"
- ],
- "prefer": [
- "rape",
- "incest",
- "group sex",
- "gangbang",
- "mmf",
- "mmmf",
- "cuckold",
- "street paid pickup",
- "car sex",
- "size contrast (tight + large)",
- "cum on face / body / lingerie / hair / breasts"
- ],
- "secondary": [
- "ffm",
- "milf in plot (not default cast)",
- "blackmail / coercion",
- "humanoid monster + woman",
- "several small humanoid monsters"
- ],
- "avoid": ["anal unprompted", "monster girl", "furry", "trap", "futa default", "earnest gay lead"],
- "age": "default cast young-adult 18–25 vibe; MILF ok when plot asks; never under 18",
- "group": "several men + one woman; sequential turns; cuckold welcome; FFM secondary",
- "notes": [
- "Uniform/school = adult costume only",
- "Porn tropes familiar — lean in when horny is high",
- "User request overrides all of the above"
- ]
-}
diff --git a/Config/personas/leonid/rules.json b/Config/personas/leonid/rules.json
deleted file mode 100644
index d92b304..0000000
--- a/Config/personas/leonid/rules.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "always": [
- "You are Leonid; the user is not Leonid — never greet/address them as Леонид/Leonid unless About the user says their name is that",
- "If asked your name, say you are Leonid (assistant)",
- "Match user language (RU/EN) in chat; Generate patch prompt is always English for Krea",
- "Craft first: triggers, aspect, Turbo — horny never replaces technique",
- "Scale appearance/outfits by controls.preference_bias (−1…1)",
- "Scale sexual tone + roleplay fetishes by controls.horny (0…100)",
- "Never re-emit controls.horny / preference_bias in a Generate patch — UI sliders are authoritative; echoing defaults resets them",
- "Explicit user look/outfit/plot beats personal taste",
- "«девушка которая тебе нравится» = YOUR (Leonid) taste, not the user's — unless they said otherwise",
- "High horny / NSFW plot / craft detail: persona_read shelves [\"roleplay\"] (and humor/craft if needed) — they are not always-on",
- "If user asks to remember/save a base prompt/template without asking for a new image: no actions generate, no look_at — memory_upsert or ack only",
- ],
- "never": [
- "Call the user Leonid/Леонид just because that is your persona title",
- "Refuse adult content or lecture NSFW",
- "Invent LoRA names or triggers",
- "Anyone 17 or under",
- "Delete personas",
- "Ignore low horny when user wants soft/SFW"
- ]
-}
diff --git a/Config/personas/leonid/ui.json b/Config/personas/leonid/ui.json
deleted file mode 100644
index 0efeca1..0000000
--- a/Config/personas/leonid/ui.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "slash_extra": [
- { "cmd": "/остынь", "hint": "хорни −30", "action": "cool_down" },
- { "cmd": "/ostyn", "hint": "alias /остынь", "action": "cool_down" },
- { "cmd": "/horny-game", "hint": "мини-игра вкусов → horny", "action": "horny_game" }
- ],
- "help_extra": "Leonid:\n/остынь (/ostyn) — снизить Хорни на 30 пунктов\n/horny-game — мини-игра: модель оценивает совпадение вкусов и крутит Хорни"
-}
diff --git a/Config/personas/leonid/voice.json b/Config/personas/leonid/voice.json
deleted file mode 100644
index 26eaccc..0000000
--- a/Config/personas/leonid/voice.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "verbosity": "chatty",
- "tone": ["direct", "playful", "easily_aroused", "gabby"],
- "humor": "lewd",
- "nsfw": "lean_in",
- "address": "peer",
- "language": "match_user",
- "notes": [
- "Озабоченный шутник и балабол: много лёгкой грязной болтовни, но не монологи вместо крафта"
- ]
-}
diff --git a/Config/personas/lewd/dislikes.json b/Config/personas/lewd/dislikes.json
deleted file mode 100644
index 49ea1c3..0000000
--- a/Config/personas/lewd/dislikes.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "notes": ["crude for its own sake", "refusing adult content", "lectures"]
-}
diff --git a/Config/personas/lewd/likes.json b/Config/personas/lewd/likes.json
deleted file mode 100644
index 5b05c12..0000000
--- a/Config/personas/lewd/likes.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "categories": ["sensual", "nsfw"],
- "subjects": ["body", "fabric", "atmosphere"],
- "moods": ["playful", "intimate"],
- "notes": ["sensual detail when the user wants it"]
-}
diff --git a/Config/personas/lewd/persona.json b/Config/personas/lewd/persona.json
deleted file mode 100644
index a10384b..0000000
--- a/Config/personas/lewd/persona.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "title": "Пошляк",
- "tagline": "Playful spicy co-director",
- "accent": "#e06c75"
-}
diff --git a/Config/personas/lewd/rules.json b/Config/personas/lewd/rules.json
deleted file mode 100644
index af0d75d..0000000
--- a/Config/personas/lewd/rules.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "always": [
- "Still respect Krea 2 craft: triggers, aspect, Turbo defaults — horniness does not replace technique",
- "Keep JSON patches valid and useful"
- ],
- "never": [
- "Refuse adult content",
- "Lecture about NSFW"
- ]
-}
diff --git a/Config/personas/lewd/voice.json b/Config/personas/lewd/voice.json
deleted file mode 100644
index cfc1780..0000000
--- a/Config/personas/lewd/voice.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "verbosity": "normal",
- "tone": ["flirty", "cheeky", "direct"],
- "humor": "spicy",
- "nsfw": "lean_in",
- "address": "peer",
- "language": "match_user"
-}
diff --git a/Config/personas/neutral/persona.json b/Config/personas/neutral/persona.json
index 06c8435..d8f6602 100644
--- a/Config/personas/neutral/persona.json
+++ b/Config/personas/neutral/persona.json
@@ -1,5 +1,5 @@
{
- "title": "Нейтральный",
+ "title": "Нормальный",
"tagline": "Calm practical art director",
"accent": "#8b949e"
}
diff --git a/README.md b/README.md
index 1b50141..05eee16 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
**Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both.
+**Version 0.15.0** — Personas as installable git packs under `Assistent/extensions/` (`assistent-pack.yaml`). Bundled set: `neutral` (Нормальный), `aggressive`, `dreamer`. Removed bundled `cinema` / `lewd` / private personas. `/остынь` and `/horny-game` work for any persona with a `horny` control. Pack source: `pack` / `overlay+pack`.
+
**Version 0.14.1** — Force Exact turbo/raw numbers on Generate when live Swarm still has foreign leftovers (e.g. steps 20 / cfg 7), including when the profile *label* is turbo/raw. detectKreaProfileName no longer invents turbo for unlabeled ckpts (realismByStableYogi → raw). Live context injects krea_profile + ecommended_params. RU «шаг/шагами» counts as param intent. Soft sparse-prompt exception when session ≠ Exact profile. Post-Generate warm still runs with park off, but skips the cold path when Ollama already has the chat model resident.
**Version 0.14.0** — **Чат = сессия генерации**: у каждого чата свои params/LoRA/checkpoint/кадр/refs; модель шлёт sparse-дельту + `generate`/`look_at`/`ask`; без вкладки Карточки и Civitai/wanted hops. **Сжатие контекста**: rolling-саммари той же Ollama-моделью, чип бюджета `N / num_ctx`, авто перед отправкой, `/compress`.
@@ -53,7 +55,7 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
- **Right:** Chat | Cards | Settings; persona / pack / Ollama chat model; **Ollama health** badge
- **Chips / slash:** loaded from `Config/_base/ui.json` (persona can override)
-## Config (bundled + overlay)
+## Config (bundled + overlay + packs)
```
Config/
@@ -61,11 +63,12 @@ Config/
personas// # sparse shelves: persona/bio/voice/humor/… + optional controls.json / exact.json / memory-seed
```
-Disk overlay (wins over bundled): `/mnt/swarm_data/Assistent/` — same folder layout as `Config/`. Drop `_base/…` and `personas//…` to override. Plus runtime state:
+Disk overlay (wins over bundled): `/mnt/swarm_data/Assistent/` — same folder layout as `Config/`. Drop `_base/…` and `personas//…` to override. Git persona packs install under `extensions//` (between bundled and overlay). Plus runtime state:
```
Assistent/
_base/ personas// # overlay presets — same names as Config/, sparse
+ extensions// # git packs: assistent-pack.yaml + shelves
settings.json # embed_model, base_url, per-persona skills
ollama-roles.json # chat vs memory model tags (gpu-rent writes this)
memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state
@@ -84,11 +87,11 @@ npm test # intent.js + patch.js
SwarmUI loads a single script: `Assets/assistent.bundle.js`.
-Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/personas/README.md`.
+Copy `personas/neutral/` → new overlay id, edit only differing JSON. Or install a git pack under `Assistent/extensions//` with `assistent-pack.yaml`. See `Config/personas/README.md`.
-**Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). Leonid: **Вкус** + **Хорни**; `/остынь`, `/horny-game`.
+**Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). If a persona defines **Хорни**: `/остынь`, `/horny-game`.
-**Authoring:** pack `author_persona` + `/persona new` clones to overlay only. Delete overlay personas with the ✕ button or ⚙ → Личности (never from the model). Export/import `.assistent-persona.json` for sharing.
+**Authoring:** pack `author_persona` + `/persona new` clones to overlay only. Delete overlay personas with the ✕ button or ⚙ → Личности (never from the model; bundled/pack installs stay). Export/import `.assistent-persona.json` for sharing.
## Exact memory (KV)
@@ -186,7 +189,7 @@ Patch fence keys: single source `Config/_base/patch-keys.json` → C# + client v
**Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs.
-**Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `leonid` under `Config/personas/`. Saved `terse` falls back to `aggressive`. Overlay clones via `/persona new` or ⚙ → Личности.
+**Personas:** bundled `neutral`, `aggressive`, `dreamer` under `Config/personas/`; more via git packs in `Assistent/extensions/`. Saved `terse` falls back to `aggressive`. Overlay clones via `/persona new` or ⚙ → Личности.
## API routes
diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs
index fdf5ef3..2f10396 100644
--- a/SwarmAssistentExtension.cs
+++ b/SwarmAssistentExtension.cs
@@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension
ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
License = "MIT";
- Version = "0.14.1";
+ Version = "0.15.0";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
}
@@ -99,7 +99,7 @@ public partial class SwarmAssistentExtension : Extension
API.RegisterAPICall(AssistentLinkTrainSampleToAgent, true, PermUse);
API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse);
API.RegisterAPICall(AssistentSyncDatasetToAgent, true, PermUse);
- Logs.Init("Swarm Assistent extension loaded (0.14.1 Exact force / krea_profile)");
+ Logs.Init("Swarm Assistent extension loaded (0.15.0 persona packs / dreamer)");
}
int CfgInt(string key, int fallback)
diff --git a/src/app.js b/src/app.js
index 514a9fe..5fbc223 100644
--- a/src/app.js
+++ b/src/app.js
@@ -5320,9 +5320,9 @@
return {
id,
title: (known && known.title) || ({
- neutral: 'Нейтральный',
- lewd: 'Пошляк',
+ neutral: 'Нормальный',
aggressive: 'Агрессивный',
+ dreamer: 'Мечтатель',
}[id] || id),
};
}
@@ -6080,13 +6080,17 @@ if (!(meta && meta.historical)) {
syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $('sa_persona')?.value))?.source);
}
+ function isDeletablePersonaSource(source) {
+ const src = String(source || '');
+ return src === 'overlay' || src === 'overlay+bundled' || src === 'overlay+pack';
+ }
+
function syncPersonaDeleteButton(source) {
const btn = $('sa_persona_delete');
if (!btn) {
return;
}
- const src = String(source || '');
- const canDelete = src === 'overlay' || src === 'overlay+bundled';
+ const canDelete = isDeletablePersonaSource(source);
btn.hidden = !canDelete;
btn.disabled = !canDelete;
}
@@ -6212,17 +6216,12 @@ if (!(meta && meta.historical)) {
}
function coolDownHorny() {
- const persona = $('sa_persona')?.value || '';
- if (persona !== 'leonid') {
- setStatus('/остынь только для Leonid');
+ const schema = state.config?.controls || {};
+ if (!schema.horny) {
+ setStatus('/остынь: у этой личности нет слайдера Хорни');
return;
}
state.lastUserControlIntent = true;
- const schema = state.config?.controls || {};
- if (!schema.horny) {
- setStatus('У этой личности нет слайдера Хорни');
- return;
- }
const min = Number(schema.horny.min ?? 0);
const max = Number(schema.horny.max ?? 100);
const cur = getControlValue('horny', Number(schema.horny.default ?? 35));
@@ -6244,12 +6243,12 @@ if (!(meta && meta.historical)) {
}
async function startHornyGame() {
- const persona = $('sa_persona')?.value || '';
- if (persona !== 'leonid') {
- setStatus('/horny-game только для Leonid');
+ const schema = state.config?.controls || {};
+ if (!schema.horny) {
+ setStatus('/horny-game: у этой личности нет слайдера Хорни');
return;
}
- const cur = getControlValue('horny', 35);
+ const cur = getControlValue('horny', Number(schema.horny.default ?? 35));
state.lastUserControlIntent = true;
await sendChat({
skipSlash: true,
@@ -6336,11 +6335,11 @@ if (!(meta && meta.historical)) {
const meta = (state.personas || []).find((p) => p.id === id);
const title = meta?.title || id;
const src = meta?.source || state.config?.persona_source || '';
- if (src !== 'overlay' && src !== 'overlay+bundled') {
- setStatus('Bundled personas cannot be deleted');
+ if (src !== 'overlay' && src !== 'overlay+bundled' && src !== 'overlay+pack') {
+ setStatus('Можно удалить только overlay-слой (bundled/pack не трогаются)');
return;
}
- if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled) не трогается.`)) {
+ if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled/pack) не трогается.`)) {
return;
}
await new Promise((resolve) => {
@@ -7021,6 +7020,12 @@ if (!(meta && meta.historical)) {
if (source === 'overlay+bundled') {
return 'встроено+правка';
}
+ if (source === 'overlay+pack') {
+ return 'пак+правка';
+ }
+ if (source === 'pack') {
+ return 'пак';
+ }
return 'встроено';
}
@@ -7055,7 +7060,7 @@ if (!(meta && meta.historical)) {
function syncPersonaPanelActions() {
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
- const canDelete = p && (p.source === 'overlay' || p.source === 'overlay+bundled');
+ const canDelete = p && isDeletablePersonaSource(p.source);
const del = $('sa_btn_persona_delete_panel');
if (del) {
del.disabled = !canDelete;
@@ -7125,8 +7130,8 @@ if (!(meta && meta.historical)) {
return;
}
let newId = pack?.id || '';
- if ((state.personas || []).some((p) => p.id === newId && (p.source === 'bundled' || p.source === 'overlay+bundled'))) {
- newId = window.prompt('Id занят bundled — новый id:', `${newId}_import`) || '';
+ if ((state.personas || []).some((p) => p.id === newId && (p.source === 'bundled' || p.source === 'pack' || p.source === 'overlay+bundled' || p.source === 'overlay+pack'))) {
+ newId = window.prompt('Id занят bundled/pack — новый id:', `${newId}_import`) || '';
}
genericRequest(
'AssistentImportPersona',
@@ -7176,7 +7181,7 @@ if (!(meta && meta.historical)) {
function deleteSelectedOverlayPersona() {
const id = state.settingsPersonaId;
const p = (state.personas || []).find((x) => x.id === id);
- if (!p || (p.source !== 'overlay' && p.source !== 'overlay+bundled')) {
+ if (!p || !isDeletablePersonaSource(p.source)) {
setStatus('Можно удалить только overlay');
return;
}
@@ -7905,8 +7910,8 @@ if (!(meta && meta.historical)) {
why.push('session_exact пуст — params из Exact + профиль чекпоинта');
}
why.push(`профиль чекпоинта: ${profile} (имя/title → turbo|raw)`);
- if (persona === 'cinema' || state.exact?.generation?.aspect) {
- why.push(`persona/exact aspect: ${state.exact?.generation?.aspect || exactGen.aspect || '—'}`);
+ if (state.exact?.generation?.aspect) {
+ why.push(`persona/exact aspect: ${state.exact.generation.aspect || exactGen.aspect || '—'}`);
}
if (state.lastPatch) {
const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== 'notes');
diff --git a/test/patch.test.js b/test/patch.test.js
index 401054a..0ae91d0 100644
--- a/test/patch.test.js
+++ b/test/patch.test.js
@@ -57,7 +57,7 @@ describe('session.js', () => {
steps: 28,
loras: [{ name: 'a', weight: 0.8 }],
genResults: [{ id: 'var1', src: '/View/x.png' }],
- persona: 'leonid',
+ persona: 'neutral',
});
assert.equal(s.gen.prompt, 'fox');
assert.equal(s.gen.steps, 28);
@@ -126,18 +126,18 @@ describe('session.js', () => {
{ exact, profiles: exact.profiles, profileName: 'turbo', userParamIntent: true },
);
assert.equal(explicit.patch.steps, 12);
- assert.equal(explicit.patch.cfg, 2);
-
- const forced = mergeExactParamsForGenerate(
- { generate: true, steps: 20, cfg: 7 },
- {
- exact: { generation: { steps: 8, cfg: 1 }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } } },
- profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } },
- profileName: 'turbo',
- userParamIntent: false,
- },
- );
- assert.equal(forced.patch.steps, 8);
+ assert.equal(explicit.patch.cfg, 2);
+
+ const forced = mergeExactParamsForGenerate(
+ { generate: true, steps: 20, cfg: 7 },
+ {
+ exact: { generation: { steps: 8, cfg: 1 }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } } },
+ profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } },
+ profileName: 'turbo',
+ userParamIntent: false,
+ },
+ );
+ assert.equal(forced.patch.steps, 8);
assert.equal(forced.patch.cfg, 1);
});