Ship 0.15.0: installable persona packs and slimmer bundled set.

Load Assistent/extensions packs between bundled and overlay, keep neutral/aggressive/dreamer, drop cinema/lewd/private shelves, and make horny slash commands schema-driven.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-23 08:28:31 +03:00
co-authored by Cursor
parent e2d48b4bf9
commit 47d352a7af
42 changed files with 353 additions and 429 deletions
+215 -47
View File
@@ -199,6 +199,125 @@ public sealed class AssistentConfig
return last;
}
/// <summary>Installed git persona pack: id → shelf directory under Assistent/extensions/.</summary>
public sealed record PackPersona(string Id, string ShelfRoot);
/// <summary>Parse minimal assistent-pack.yaml (kind + id). No full YAML dependency.</summary>
public static Dictionary<string, string> TryParsePackManifest(string path)
{
Dictionary<string, string> 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;
}
/// <summary>Discover persona packs under Assistent/extensions/*/.</summary>
public List<PackPersona> DiscoverPackPersonas()
{
List<PackPersona> found = [];
HashSet<string> 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<string, string> 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"));
}
/// <summary>Ancestor-first chain ending with the current persona (for overlay merge and vector retrieve).</summary>
public List<string> 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<string> LayerRoots(string personaId)
{
Dictionary<string, string> 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<string, (string title, string accent, string source)> 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<bool?>() == 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<bool?>() == 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;
}
/// <summary>Bundled or installed pack — not deletable / not overwritten in place.</summary>
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<string> 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;