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:
+215
-47
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user