Add sssf skill, installable via the skills CLI

Port the sssf skill from ~/.agents/skills/sssf into this repo so it can be
distributed and installed with the skills CLI (skills add INDigitalStudio/skills
--skill sssf).

- Copy the skill (SKILL.md, cookbooks, references, scripts, templates, and the
  visualizer app source) into sssf/.
- Gitignore build/runtime artifacts: the visualizer's node_modules/ and dist/,
  Python bytecode, and the machine-specific repos.json.
- Make the skill location-independent: install.py now stamps the skill's real
  path into the stamped justfile's skill_dir (replacing the hardcoded
  ~/.agents/skills/sssf), so 'just obs' finds the visualizer wherever the CLI
  installed the skill.
- Update cookbooks to use <skill>/scripts/... instead of the hardcoded path,
  and document the skills CLI install command.
- Update the repo README with install instructions.
This commit is contained in:
INDigitalStudio 2026-08-09 21:00:28 +00:00
parent a42608f602
commit 2cc766aabe
98 changed files with 12508 additions and 0 deletions

View file

@ -0,0 +1,146 @@
/**
* Repo registry for the multi-repo visualizer.
*
* The visualizer can serve several repos' trace DBs from one process. A repo
* is a slug → sssf.db mapping, declared in a JSON file:
*
* [
* { "slug": "tailsandstays", "name": "Tails & Stays",
* "db": "/home/ima/projects/tailsandstays/adws/adw_data/sssf.db" },
* { "slug": "mempalace", "name": "MemPalace",
* "db": "/home/ima/projects/mempalace/adws/adw_data/sssf.db" }
* ]
*
* Point the server at it with SSSF_REPOS=/path/to/repos.json. When SSSF_REPOS
* is unset the server auto-discovers repos under a projects root (default
* ~/projects, override with SSSF_PROJECTS_ROOT) by scanning for a trace db at
* <root>/<dir>/adws/adw_data/sssf.db. If nothing is discovered it falls back to
* the single-repo behaviour (--db / SSSF_DB / <cwd>/adws/adw_data/sssf.db)
* under a slug derived from the repo dir name, so the original single-repo
* deployment keeps working.
*/
import { existsSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
import { SssfDb, resolveDbPath } from "./db.ts";
export interface RepoEntry {
slug: string;
name: string;
db: string;
}
export interface RepoRuntime {
slug: string;
name: string;
db: SssfDb;
}
const SAFE_SLUG = /^[A-Za-z0-9._-]+$/;
function isSafeSlug(slug: string): boolean {
return SAFE_SLUG.test(slug) && slug !== "." && slug !== "..";
}
/** Derive a display slug from a db path: <repo>/adws/adw_data/sssf.db → repo dir name. */
function slugFromDb(dbPath: string): string {
// adw_data → adws → repo root
const repoDir = dirname(dirname(dirname(dbPath)));
const name = basename(repoDir);
return name && isSafeSlug(name) ? name : "default";
}
async function loadReposFile(path: string): Promise<RepoEntry[]> {
const raw = Bun.file(path);
if (!existsSync(path)) {
throw new Error(`SSSF_REPOS file not found: ${path}`);
}
const data = JSON.parse(await raw.text()) as unknown;
if (!Array.isArray(data)) {
throw new Error(`SSSF_REPOS file must be a JSON array of { slug, name, db }`);
}
const entries: RepoEntry[] = [];
const seen = new Set<string>();
for (const item of data) {
const entry = item as Partial<RepoEntry>;
if (typeof entry.slug !== "string" || !isSafeSlug(entry.slug)) {
throw new Error(`invalid repo slug: ${String(entry.slug)}`);
}
if (typeof entry.db !== "string") {
throw new Error(`repo "${entry.slug}" is missing a db path`);
}
if (seen.has(entry.slug)) {
throw new Error(`duplicate repo slug: ${entry.slug}`);
}
seen.add(entry.slug);
const db = isAbsolute(entry.db) ? entry.db : resolve(dirname(path), entry.db);
entries.push({
slug: entry.slug,
name: typeof entry.name === "string" && entry.name ? entry.name : entry.slug,
db,
});
}
if (entries.length === 0) {
throw new Error("SSSF_REPOS file declares no repos");
}
return entries;
}
/**
* Auto-discover repos under a projects root by scanning for a trace db at
* <root>/<dir>/adws/adw_data/sssf.db. Used when SSSF_REPOS is unset so new
* repos appear without editing a registry file. The root defaults to
* ~/projects and can be overridden with SSSF_PROJECTS_ROOT.
*/
function discoverRepos(root: string): RepoEntry[] {
if (!existsSync(root)) {
return [];
}
const entries: RepoEntry[] = [];
for (const name of readdirSync(root, { withFileTypes: true })) {
if (!name.isDirectory() || name.name.startsWith(".")) {
continue;
}
const db = join(root, name.name, "adws", "adw_data", "sssf.db");
if (existsSync(db) && isSafeSlug(name.name)) {
entries.push({ slug: name.name, name: name.name, db });
}
}
return entries;
}
/**
* Build the repo registry. The first repo is served under the legacy
* unprefixed /api routes (the single repo in single-repo mode, or the first
* repo in multi-repo mode).
*/
export async function buildRepos(): Promise<{ repos: RepoRuntime[] }> {
const reposFile = process.env.SSSF_REPOS;
const entries: RepoEntry[] = reposFile
? await loadReposFile(reposFile)
: discoverRepos(process.env.SSSF_PROJECTS_ROOT ?? join(homedir(), "projects"));
if (entries.length === 0) {
// No discovered repos — fall back to the single-repo behaviour so the
// original deployment (--db / SSSF_DB / <cwd>/adws/adw_data/sssf.db) works.
entries.push({ slug: slugFromDb(resolveDbPath()), name: "", db: resolveDbPath() });
}
const repos: RepoRuntime[] = [];
for (const entry of entries) {
let db: SssfDb;
try {
db = new SssfDb(entry.db);
} catch (error) {
console.error(`[sssf] repo "${entry.slug}": ${(error as Error).message}`);
continue;
}
repos.push({ slug: entry.slug, name: entry.name, db });
}
if (repos.length === 0) {
console.error("[sssf] no repos could be opened — exiting");
process.exit(1);
}
return { repos };
}