fix: consolidate extensions into sf, migrate kernel.ts, fix test suite

- Fold sf-usage-bar, sf-notify, sf-inturn-guard, sf-permissions,
  slash-commands into sf extension (ui/, notifications/, guards/,
  permissions/, commands/legacy/)
- Delete vectordrive extension
- Migrate uok/kernel.js to TypeScript (kernel.ts) with full interfaces
- Add allowJs/checkJs:false to tsconfig.resources.json for incremental TS migration
- Add symlink dedup to extension-discovery.ts (seenRealPaths Set)
- Add before_provider_request delegate back to native-search.js so
  session budget tests exercise the middleware end-to-end
- Fix parseSfNativeTools() to return all SF manifest tools (drop sf_ filter)
- Fix test assertions: plan_milestone/complete_task/validate_milestone
- Remove subagent from app-smoke.test.ts (folded into sf/subagent/)
- Remove sf-permissions/sf-inturn-guard/subagent from features-inventory test
- Fix resolveSearchProvider autonomous mode test to pass 'auto' explicitly
- Remove legacy /clear slash command (conflicts with built-in clear_terminal)
- Update web-command-parity-contract.test.ts for clear removal

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Mikael Hugo 2026-05-11 02:40:52 +02:00
parent 24592507c3
commit adb449d642
51 changed files with 786 additions and 3474 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -85,7 +85,7 @@ export function parseSfNativeTools() {
);
}
return uniqueSorted(
tools.filter((tool) => typeof tool === "string" && tool.startsWith("sf_")),
tools.filter((tool) => typeof tool === "string"),
);
}

View file

@ -13,7 +13,7 @@
* the test suite that validates symlink and manifest edge cases.
*/
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
import { join, resolve } from "node:path";
function isExtensionFile(name: string): boolean {
@ -97,6 +97,10 @@ export function discoverExtensionEntryPaths(extensionsDir: string): string[] {
}
const discovered: string[] = [];
// Track real paths to avoid loading the same file via two names
// (e.g. a real directory and a symlink that points to it).
const seenRealPaths = new Set<string>();
for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
const entryPath = join(extensionsDir, entry.name);
@ -104,14 +108,34 @@ export function discoverExtensionEntryPaths(extensionsDir: string): string[] {
(entry.isFile() || entry.isSymbolicLink()) &&
isExtensionFile(entry.name)
) {
discovered.push(entryPath);
const real = tryRealpath(entryPath);
if (!seenRealPaths.has(real)) {
seenRealPaths.add(real);
discovered.push(entryPath);
}
continue;
}
if (entry.isDirectory() || entry.isSymbolicLink()) {
discovered.push(...resolveExtensionEntries(entryPath));
const entries = resolveExtensionEntries(entryPath);
for (const ep of entries) {
const real = tryRealpath(ep);
if (!seenRealPaths.has(real)) {
seenRealPaths.add(real);
discovered.push(ep);
}
}
}
}
return discovered;
}
/** Resolve a real path, falling back to the original path on error (e.g. dangling symlink). */
function tryRealpath(p: string): string {
try {
return realpathSync.native(p);
} catch {
return p;
}
}

View file

@ -37,13 +37,15 @@ export function preferBraveSearch() {
);
}
/**
* Register model_select and session_start hooks for native Anthropic web search.
* Register model_select, before_provider_request, and session_start hooks for native Anthropic web search.
*
* before_provider_request injection runs natively in sdk.ts via webSearchMiddleware
* nothing is registered here for that event.
* before_provider_request delegates to the webSearchMiddleware singleton so that tests
* exercise the same code path as production (sdk.ts calls it natively first; the extension
* delegate is a no-op in production due to the double-injection guard in the middleware).
*/
export function registerNativeSearchHooks(pi) {
let isAnthropicProvider = false;
// null = unknown (model_select not yet fired); true/false = provider is/isn't Anthropic.
let isAnthropicProvider = null;
// Register the PREFERENCES.md-aware resolver so the native middleware (shared
// singleton in web-search-middleware.ts) respects search_provider overrides.
// Called here so each test invocation resets the resolver to the current context.
@ -106,6 +108,17 @@ export function registerNativeSearchHooks(pi) {
);
}
});
// Delegate before_provider_request to the native middleware singleton.
// In production, sdk.ts already ran applyToPayload before extension hooks fire,
// so the double-injection guard makes this a no-op. In tests (mock PI without
// sdk.ts), this is the only path that exercises the injection logic.
pi.on("before_provider_request", async (event, _ctx) => {
let modelHint = event.model;
if (!modelHint && isAnthropicProvider !== null) {
modelHint = { provider: isAnthropicProvider ? "anthropic" : "not-anthropic" };
}
return webSearchMiddleware.applyToPayload(event.payload, modelHint);
});
pi.on("session_start", async (_event, _ctx) => {
// Reset the shared middleware session budget (#1309).
webSearchMiddleware.resetSession();

View file

@ -1,18 +0,0 @@
{
"id": "sf-inturn-guard",
"name": "SF In-Turn Guard",
"version": "1.0.0",
"description": "Detect duplicate tool calls and short retry loops inside one agent turn",
"tier": "bundled",
"requires": { "platform": ">=2.29.0" },
"provides": {
"commands": ["guard-status", "guard-toggle"],
"hooks": [
"agent_start",
"turn_start",
"tool_call",
"tool_result",
"agent_end"
]
}
}

View file

@ -1,19 +0,0 @@
{
"id": "sf-notify",
"name": "SF Notify",
"version": "1.0.0",
"description": "Send completion and attention notifications for long-running agent work",
"tier": "bundled",
"requires": { "platform": ">=2.29.0" },
"provides": {
"commands": [
"notify-beep",
"notify-focus",
"notify-save-global",
"notify-say",
"notify-status",
"notify-threshold"
],
"hooks": ["session_start", "agent_start", "tool_result", "agent_end"]
}
}

View file

@ -1,18 +0,0 @@
{
"id": "sf-permissions",
"name": "SF Permissions",
"version": "1.0.0",
"description": "Enforce layered permission levels for shell, file, and skill-scoped tool use",
"tier": "bundled",
"requires": { "platform": ">=2.29.0" },
"provides": {
"commands": ["permission", "permission-mode"],
"hooks": [
"session_start",
"before_agent_start",
"agent_end",
"tool_call",
"tool_result"
]
}
}

View file

@ -1,14 +0,0 @@
{
"name": "pi-extension-sf-permissions",
"private": true,
"version": "1.0.0",
"type": "module",
"engines": {
"node": ">=26.1.0"
},
"pi": {
"extensions": [
"./index.js"
]
}
}

View file

@ -1,319 +0,0 @@
/**
* Session Color TUI colored status band
*
* Displays a colored band in the footer to visually distinguish sessions.
*/
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
const DEFAULT_CONFIG = {
enabledByDefault: true,
blockChar: "▁",
blockCount: "full",
};
const STATE_FILE = path.join(os.homedir(), ".sf", "session-color-state.json");
const COLOR_PALETTE = [
196, 51, 226, 129, 46, 208, 27, 213, 118, 160, 87, 220, 93, 34, 202, 75, 199,
154, 124, 45, 214, 135, 40, 166, 69, 205, 190, 88, 80, 228, 97, 28, 172, 63,
197, 82, 130, 39, 219, 106,
];
const BLOCK_CHARS = [
{ char: "▁", name: "Lower 1/8 block" },
{ char: "▂", name: "Lower 1/4 block" },
{ char: "▄", name: "Lower half block" },
{ char: "█", name: "Full block" },
{ char: "▔", name: "Upper 1/8 block" },
{ char: "▀", name: "Upper half block" },
{ char: "─", name: "Light horizontal" },
{ char: "━", name: "Heavy horizontal" },
{ char: "═", name: "Double horizontal" },
];
const RESET = "\x1b[0m";
// ─────────────────────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────────────────────
export function registerSessionColor(pi) {
const state = {
colorIndex: null,
assigned: false,
enabledOverride: null,
blockCharOverride: null,
blockCharIndex: 0,
};
let currentCtx = null;
let resizeHandler = null;
function setupResizeListener(ctx, config) {
if (resizeHandler) process.stdout.off("resize", resizeHandler);
if (config.blockCount === "full" && state.colorIndex !== null) {
currentCtx = ctx;
resizeHandler = () => {
if (currentCtx && state.colorIndex !== null) {
const isEnabled = state.enabledOverride ?? config.enabledByDefault;
if (isEnabled) updateStatus(currentCtx, config, state);
}
};
process.stdout.on("resize", resizeHandler);
}
}
registerCommands(pi, state);
// Gate the session-lifecycle work on having a real TUI. The color band is
// pure footer decoration — nothing to render into in headless mode, so
// skip state-file writes and resize listeners entirely.
pi.on("session_start", async (_, ctx) => {
if (!ctx.hasUI) return;
currentCtx = ctx;
initSession(ctx, state, setupResizeListener);
});
pi.on("session_switch", async (event, ctx) => {
if (!ctx.hasUI) return;
if (event.reason === "new") {
currentCtx = ctx;
initSession(ctx, state, setupResizeListener);
}
});
}
// ─────────────────────────────────────────────────────────────────────────────
// Session Lifecycle
// ─────────────────────────────────────────────────────────────────────────────
function initSession(ctx, state, setupResize) {
Object.assign(state, {
colorIndex: null,
assigned: false,
enabledOverride: null,
blockCharOverride: null,
blockCharIndex: 0,
});
const config = getConfig(ctx);
if (!config.enabledByDefault) {
ctx.ui.setStatus("0-color-band", "");
return;
}
const sessionId = ctx.sessionManager.getSessionId();
const persisted = readColorState();
if (persisted?.sessionId === sessionId) {
state.colorIndex = persisted.lastColorIndex;
state.assigned = true;
updateStatus(ctx, config, state);
setupResize(ctx, config);
return;
}
const lastIndex = persisted?.lastColorIndex ?? -1;
const nextIndex = (lastIndex + 1) % COLOR_PALETTE.length;
state.colorIndex = nextIndex;
state.assigned = true;
writeColorState({
lastColorIndex: nextIndex,
sessionId,
timestamp: Date.now(),
});
updateStatus(ctx, config, state);
setupResize(ctx, config);
}
// ─────────────────────────────────────────────────────────────────────────────
// Status Display
// ─────────────────────────────────────────────────────────────────────────────
function updateStatus(ctx, config, state) {
if (state.colorIndex === null) return;
const color = COLOR_PALETTE[state.colorIndex];
const count =
config.blockCount === "full"
? process.stdout.columns || 80
: config.blockCount;
const char = state.blockCharOverride ?? config.blockChar;
const block = char.repeat(count);
ctx.ui.setStatus("0-color-band", `\x1b[38;5;${color}m${block}${RESET}`);
}
// ─────────────────────────────────────────────────────────────────────────────
// Persistence
// ─────────────────────────────────────────────────────────────────────────────
function readColorState() {
try {
if (fs.existsSync(STATE_FILE)) {
return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
}
} catch {} // file missing or corrupt → return null (no saved state)
return null;
}
function writeColorState(s) {
try {
const dir = path.dirname(STATE_FILE);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(STATE_FILE, JSON.stringify(s, null, 2), "utf8");
} catch {} // write failure → state not persisted, but operation continues
}
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
function getConfig(ctx) {
const settings = ctx.settingsManager?.getSettings() ?? {};
return { ...DEFAULT_CONFIG, ...(settings.sessionColor ?? {}) };
}
// ─────────────────────────────────────────────────────────────────────────────
// Commands
// ─────────────────────────────────────────────────────────────────────────────
function registerCommands(pi, state) {
pi.registerCommand("color", {
description: "Toggle color band on/off",
handler: async (_, ctx) => {
const config = getConfig(ctx);
const current = state.enabledOverride ?? config.enabledByDefault;
state.enabledOverride = !current;
if (state.enabledOverride) {
ctx.ui.notify("🎨 Color band ON", "info");
if (state.colorIndex !== null) {
updateStatus(ctx, config, state);
} else {
const persisted = readColorState();
const nextIndex =
((persisted?.lastColorIndex ?? -1) + 1) % COLOR_PALETTE.length;
state.colorIndex = nextIndex;
state.assigned = true;
writeColorState({
lastColorIndex: nextIndex,
sessionId: ctx.sessionManager.getSessionId(),
timestamp: Date.now(),
});
updateStatus(ctx, config, state);
}
} else {
ctx.ui.notify("⬜ Color band OFF", "warning");
ctx.ui.setStatus("0-color-band", "");
}
},
});
pi.registerCommand("color-set", {
description: "Set color by index (0-39)",
handler: async (args, ctx) => {
const _config = getConfig(ctx);
const input = typeof args === "string" ? args.trim() : "";
if (input) {
const index = parseInt(input, 10);
if (Number.isNaN(index) || index < 0 || index >= COLOR_PALETTE.length) {
ctx.ui.notify(
`Invalid index. Use 0-${COLOR_PALETTE.length - 1}`,
"error",
);
return;
}
setColor(ctx, state, index);
ctx.ui.notify(`Color set to index ${index}`, "info");
return;
}
if (!ctx.hasUI) {
ctx.ui.notify(
`Usage: /color-set <0-${COLOR_PALETTE.length - 1}>`,
"info",
);
return;
}
ctx.ui.notify("Color palette:", "info");
for (let i = 0; i < COLOR_PALETTE.length; i += 10) {
const blocks = COLOR_PALETTE.slice(i, i + 10)
.map((c) => `\x1b[38;5;${c}m██${RESET}`)
.join(" ");
ctx.ui.notify(
`${String(i).padStart(2)}-${Math.min(i + 9, 39)}: ${blocks}`,
"info",
);
}
const indexStr = await ctx.ui.input(
`Enter index (0-${COLOR_PALETTE.length - 1}):`,
);
if (!indexStr) return;
const index = parseInt(indexStr, 10);
if (Number.isNaN(index) || index < 0 || index >= COLOR_PALETTE.length) {
ctx.ui.notify("Invalid index", "error");
return;
}
setColor(ctx, state, index);
ctx.ui.notify(`Color set to index ${index}`, "info");
},
});
pi.registerCommand("color-next", {
description: "Skip to next color",
handler: async (_, ctx) => {
const nextIndex = ((state.colorIndex ?? -1) + 1) % COLOR_PALETTE.length;
setColor(ctx, state, nextIndex);
ctx.ui.notify(`Skipped to color ${nextIndex}`, "info");
},
});
pi.registerCommand("color-char", {
description: "Change block character (cycles if no arg)",
handler: async (args, ctx) => {
const config = getConfig(ctx);
const input = typeof args === "string" ? args.trim() : "";
if (state.colorIndex === null) {
ctx.ui.notify("No color assigned yet", "error");
return;
}
if (input) {
state.blockCharOverride = input;
updateStatus(ctx, config, state);
ctx.ui.notify(`Block char set to "${input}"`, "info");
return;
}
state.blockCharIndex = (state.blockCharIndex + 1) % BLOCK_CHARS.length;
const next = BLOCK_CHARS[state.blockCharIndex];
state.blockCharOverride = next.char;
updateStatus(ctx, config, state);
ctx.ui.notify(`${next.char} ${next.name}`, "info");
},
});
pi.registerCommand("color-config", {
description: "View color settings",
handler: async (_, ctx) => {
const config = getConfig(ctx);
const isEnabled = state.enabledOverride ?? config.enabledByDefault;
const persisted = readColorState();
ctx.ui.notify("─── Session Color ───", "info");
ctx.ui.notify(
`Status: ${isEnabled ? "🎨 ON" : "⬜ OFF"} │ Index: ${state.colorIndex ?? "(none)"}`,
"info",
);
ctx.ui.notify(
`Char: "${state.blockCharOverride ?? config.blockChar}" │ Palette: ${COLOR_PALETTE.length} colors`,
"info",
);
if (persisted)
ctx.ui.notify(`Last used: index ${persisted.lastColorIndex}`, "info");
if (!ctx.hasUI) return;
const action = await ctx.ui.select("Options", [
"🎨 Preview all colors",
"🔄 Reset sequence",
"❌ Cancel",
]);
const selectedAction = typeof action === "string" ? action : undefined;
if (!selectedAction) return;
if (selectedAction.startsWith("🎨")) {
for (let i = 0; i < COLOR_PALETTE.length; i += 10) {
const blocks = COLOR_PALETTE.slice(i, i + 10)
.map((c) => `\x1b[38;5;${c}m██${RESET}`)
.join(" ");
ctx.ui.notify(blocks, "info");
}
} else if (selectedAction.startsWith("🔄")) {
writeColorState({
lastColorIndex: -1,
sessionId: "",
timestamp: Date.now(),
});
ctx.ui.notify(
"Sequence reset. Next session starts at color 0.",
"info",
);
}
},
});
}
function setColor(ctx, state, index) {
const config = getConfig(ctx);
state.colorIndex = index;
state.assigned = true;
writeColorState({
lastColorIndex: index,
sessionId: ctx.sessionManager.getSessionId(),
timestamp: Date.now(),
});
updateStatus(ctx, config, state);
}

View file

@ -1,433 +0,0 @@
/**
* Session Emoji TUI status line emoji
*
* Displays an emoji in the footer status line. Supports manual selection,
* AI-powered selection based on conversation, or random assignment.
*/
import { complete } from "@singularity-forge/ai";
const DEFAULT_CONFIG = {
enabledByDefault: true,
autoAssignMode: "ai",
autoAssignThreshold: 3,
contextMessages: 5,
emojiSet: "default",
customEmojis: [],
};
const EMOJI_SETS = {
default: ["🚀", "✨", "🎯", "💡", "🔥", "⚡", "🎨", "🌟", "💻", "🎭"],
animals: ["🐱", "🐶", "🐼", "🦊", "🐻", "🦁", "🐯", "🐨", "🐰", "🦉"],
tech: ["💻", "🖥️", "⌨️", "🖱️", "💾", "📱", "🔌", "🔋", "🖨️", "📡"],
fun: ["🎉", "🎊", "🎈", "🎁", "🎂", "🍕", "🍩", "🌮", "🎮", "🎲"],
};
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
const AI_PROMPTS = {
select: `You are an emoji selector. Given a conversation context and a list of recently used emojis, choose ONE unique emoji that:
1. Represents the main topic/theme of the conversation
2. Is NOT in the recently used list
3. Is relevant and appropriate
4. Stands alone (no skin tone modifiers)
Output ONLY the single emoji character, nothing else.`,
fromText: `You are an emoji selector. Given a text description, choose ONE emoji that best represents it.
Output ONLY the single emoji character, nothing else.`,
};
// ─────────────────────────────────────────────────────────────────────────────
// Main
// ─────────────────────────────────────────────────────────────────────────────
export function registerSessionEmoji(pi) {
const state = {
emoji: null,
messageCount: 0,
assigned: false,
selecting: false,
enabledOverride: null,
};
registerCommands(pi, state);
// Gate the session-lifecycle work on having a real TUI. Headless mode
// (sf headless autonomous, --print, CI) has no footer to render into, and the
// AI auto-assign path would spend tokens choosing an emoji nothing sees.
pi.on("session_start", (_, ctx) => {
if (!ctx.hasUI) return;
return initSession(ctx, pi, state);
});
pi.on("agent_start", (_, ctx) => {
if (!ctx.hasUI) return;
return handleAgentStart(ctx, pi, state);
});
}
// ─────────────────────────────────────────────────────────────────────────────
// Session Lifecycle
// ─────────────────────────────────────────────────────────────────────────────
async function initSession(ctx, pi, state) {
Object.assign(state, {
emoji: null,
messageCount: 0,
assigned: false,
selecting: false,
enabledOverride: null,
});
const config = getConfig(ctx);
if (!config.enabledByDefault) {
ctx.ui.setStatus("0-emoji", "");
return;
}
const existing = findExistingEmoji(ctx);
if (existing) {
state.emoji = existing;
state.assigned = true;
ctx.ui.setStatus("0-emoji", existing);
return;
}
if (config.autoAssignMode === "immediate") {
await assignEmoji(ctx, pi, state, config);
} else {
ctx.ui.setStatus("0-emoji", `⏳ (${config.autoAssignThreshold})`);
}
}
async function handleAgentStart(ctx, pi, state) {
const config = getConfig(ctx);
const isEnabled = state.enabledOverride ?? config.enabledByDefault;
if (!isEnabled || state.assigned || config.autoAssignMode === "immediate")
return;
state.messageCount++;
if (state.messageCount >= config.autoAssignThreshold) {
await assignEmoji(ctx, pi, state, config);
} else {
ctx.ui.setStatus(
"0-emoji",
`⏳ (${config.autoAssignThreshold - state.messageCount})`,
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Emoji Selection
// ─────────────────────────────────────────────────────────────────────────────
async function assignEmoji(ctx, pi, state, config) {
if (state.assigned || state.selecting) return;
state.selecting = true;
try {
if (config.autoAssignMode === "ai") ctx.ui.setStatus("0-emoji", "🔄");
const emoji =
config.autoAssignMode === "ai"
? await selectEmojiWithAI(ctx, config)
: selectRandomEmoji(ctx, config);
state.emoji = emoji;
state.assigned = true;
persistEmoji(ctx, pi, emoji);
ctx.ui.setStatus("0-emoji", emoji);
} finally {
state.selecting = false;
}
}
function selectRandomEmoji(ctx, config) {
const emojis = getEmojiList(config);
const recent = getRecentEmojis(ctx);
const available = emojis.filter((e) => !recent.has(e));
const pool = available.length > 0 ? available : emojis;
return pool[Math.floor(Math.random() * pool.length)];
}
async function selectEmojiWithAI(ctx, config) {
if (!ctx.model) return selectRandomEmoji(ctx, config);
try {
const context = getConversationContext(ctx, config.contextMessages);
const recent = getRecentEmojis(ctx);
const prompt = `Conversation context:\n${context || "(No messages yet - choose a welcoming, friendly emoji)"}\n\nRecently used emojis (DO NOT use these):\n${recent.size > 0 ? Array.from(recent).join(", ") : "(none)"}\n\nChoose a unique, topical emoji for this session.`;
const emoji = await callAI(ctx, AI_PROMPTS.select, prompt);
if (emoji) return emoji;
} catch {
// Fall through to random
}
return selectRandomEmoji(ctx, config);
}
async function selectEmojiFromText(ctx, description) {
if (!ctx.model) return null;
try {
return await callAI(ctx, AI_PROMPTS.fromText, description);
} catch {
return null;
}
}
async function callAI(ctx, systemPrompt, userText) {
const apiKey = await ctx.modelRegistry.getApiKey(ctx.model);
const userMessage = {
role: "user",
content: [{ type: "text", text: userText }],
timestamp: Date.now(),
};
const response = await complete(
ctx.model,
{ systemPrompt, messages: [userMessage] },
{ apiKey, maxTokens: 10 },
);
const emoji = response.content
.filter((c) => c.type === "text")
.map((c) => c.text.trim())
.join("")
.slice(0, 10);
return emoji && emoji.length > 0 && emoji.length <= 10 ? emoji : null;
}
// ─────────────────────────────────────────────────────────────────────────────
// Persistence & History
// ─────────────────────────────────────────────────────────────────────────────
function persistEmoji(ctx, pi, emoji) {
const context =
getConversationContext(ctx, 2).slice(0, 100) || "(initial session)";
pi.appendEntry("session-emoji-history", {
sessionId: ctx.sessionManager.getSessionId(),
emoji,
timestamp: Date.now(),
context,
});
}
function findExistingEmoji(ctx) {
const sessionId = ctx.sessionManager.getSessionId();
for (const entry of ctx.sessionManager.getEntries()) {
if (
entry.type === "custom" &&
entry.customType === "session-emoji-history"
) {
const data = entry.data;
if (data?.sessionId === sessionId) return data.emoji;
}
}
return null;
}
function getRecentEmojis(ctx) {
const cutoff = Date.now() - ONE_DAY_MS;
const recent = new Set();
for (const entry of ctx.sessionManager.getEntries()) {
if (
entry.type === "custom" &&
entry.customType === "session-emoji-history"
) {
const data = entry.data;
if (data?.timestamp >= cutoff) recent.add(data.emoji);
}
}
return recent;
}
function getEmojiHistory(ctx) {
const cutoff = Date.now() - ONE_DAY_MS;
const history = [];
for (const entry of ctx.sessionManager.getEntries()) {
if (
entry.type === "custom" &&
entry.customType === "session-emoji-history"
) {
const data = entry.data;
if (data?.timestamp >= cutoff) history.push(data);
}
}
return history.sort((a, b) => b.timestamp - a.timestamp);
}
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
function getConfig(ctx) {
const settings = ctx.settingsManager?.getSettings() ?? {};
return { ...DEFAULT_CONFIG, ...(settings.sessionEmoji ?? {}) };
}
function getEmojiList(config) {
if (config.emojiSet === "custom" && config.customEmojis?.length > 0) {
return config.customEmojis;
}
return EMOJI_SETS[config.emojiSet] ?? EMOJI_SETS.default;
}
function getConversationContext(ctx, maxMessages) {
const branch = ctx.sessionManager.getBranch();
const messages = [];
for (
let i = branch.length - 1;
i >= 0 && messages.length < maxMessages;
i--
) {
const entry = branch[i];
if (
entry.type === "message" &&
"message" in entry &&
entry.message.role === "user"
) {
const content = entry.message.content;
const text =
typeof content === "string"
? content
: Array.isArray(content)
? content
.filter((c) => c.type === "text")
.map((c) => c.text)
.join("\n")
: "";
if (text.trim()) messages.unshift(text);
}
}
return messages.join("\n\n");
}
function formatTimeAgo(timestamp) {
const mins = Math.round((Date.now() - timestamp) / 60000);
return mins < 60 ? `${mins}m ago` : `${Math.round(mins / 60)}h ago`;
}
// ─────────────────────────────────────────────────────────────────────────────
// Commands
// ─────────────────────────────────────────────────────────────────────────────
function registerCommands(pi, state) {
pi.registerCommand("emoji", {
description: "Toggle session emoji on/off",
handler: async (_, ctx) => {
const config = getConfig(ctx);
const current = state.enabledOverride ?? config.enabledByDefault;
state.enabledOverride = !current;
if (state.enabledOverride) {
ctx.ui.notify("🎨 Session emoji ON", "info");
ctx.ui.setStatus(
"0-emoji",
state.emoji ?? `⏳ (${config.autoAssignThreshold})`,
);
} else {
ctx.ui.notify("⬜ Session emoji OFF", "warning");
ctx.ui.setStatus("0-emoji", "");
}
},
});
pi.registerCommand("emoji-set", {
description: "Set emoji manually (emoji or description)",
handler: async (args, ctx) => {
const input = typeof args === "string" ? args.trim() : "";
if (!input) {
if (!ctx.hasUI) {
ctx.ui.notify("Usage: /emoji-set <emoji|description>", "info");
return;
}
const choice = await ctx.ui.select("Set emoji how?", [
"📝 Enter emoji directly",
"💬 Describe what you want",
"🎲 Pick random from set",
"❌ Cancel",
]);
const selectedChoice = typeof choice === "string" ? choice : undefined;
if (!selectedChoice || selectedChoice.startsWith("❌")) return;
if (selectedChoice.startsWith("📝")) {
const emoji = await ctx.ui.input("Enter emoji:");
if (emoji) {
setManualEmoji(ctx, pi, state, emoji.trim());
ctx.ui.notify(`Emoji set to ${emoji.trim()}`, "info");
}
} else if (selectedChoice.startsWith("💬")) {
const desc = await ctx.ui.input("Describe the emoji:");
if (desc) {
ctx.ui.notify("🔄 Selecting...", "info");
const emoji = await selectEmojiFromText(ctx, desc);
if (emoji) {
setManualEmoji(ctx, pi, state, emoji);
ctx.ui.notify(`Emoji set to ${emoji}`, "info");
} else {
ctx.ui.notify("Could not select emoji", "error");
}
}
} else if (selectedChoice.startsWith("🎲")) {
const setChoice = await ctx.ui.select(
"Choose set:",
Object.keys(EMOJI_SETS),
);
const selectedSet =
typeof setChoice === "string" ? setChoice : undefined;
if (!selectedSet) return;
const emojis = EMOJI_SETS[selectedSet] ?? EMOJI_SETS.default;
const emoji = emojis[Math.floor(Math.random() * emojis.length)];
setManualEmoji(ctx, pi, state, emoji);
ctx.ui.notify(`Emoji set to ${emoji}`, "info");
}
return;
}
const emojiRegex = /^[\p{Emoji_Presentation}\p{Emoji}\u200d]+/u;
if (emojiRegex.test(input)) {
const emoji = input.match(emojiRegex)?.[0] ?? input;
setManualEmoji(ctx, pi, state, emoji);
ctx.ui.notify(`Emoji set to ${emoji}`, "info");
} else {
ctx.ui.notify("🔄 Selecting...", "info");
const emoji = await selectEmojiFromText(ctx, input);
if (emoji) {
setManualEmoji(ctx, pi, state, emoji);
ctx.ui.notify(`Emoji set to ${emoji}`, "info");
} else {
ctx.ui.notify("Could not select emoji", "error");
}
}
},
});
pi.registerCommand("emoji-config", {
description: "View emoji settings",
handler: async (_, ctx) => {
const config = getConfig(ctx);
const isEnabled = state.enabledOverride ?? config.enabledByDefault;
ctx.ui.notify("─── Session Emoji ───", "info");
ctx.ui.notify(
`Status: ${isEnabled ? "🎨 ON" : "⬜ OFF"} │ Current: ${state.emoji ?? "(none)"}`,
"info",
);
ctx.ui.notify(
`Mode: ${config.autoAssignMode} │ Threshold: ${config.autoAssignThreshold} │ Set: ${config.emojiSet}`,
"info",
);
if (!ctx.hasUI) return;
const action = await ctx.ui.select("Options", [
"🎨 Preview sets",
"📋 View history",
"❌ Cancel",
]);
const selectedAction = typeof action === "string" ? action : undefined;
if (!selectedAction) return;
if (selectedAction.startsWith("🎨")) {
for (const [name, emojis] of Object.entries(EMOJI_SETS)) {
ctx.ui.notify(`${name}: ${emojis.join(" ")}`, "info");
}
} else if (selectedAction.startsWith("📋")) {
const history = getEmojiHistory(ctx);
if (history.length === 0) {
ctx.ui.notify("No history in past 24h", "info");
} else {
history.slice(0, 10).forEach((h, i) => {
const current =
h.sessionId === ctx.sessionManager.getSessionId()
? " (current)"
: "";
ctx.ui.notify(
`${i + 1}. ${h.emoji} - ${formatTimeAgo(h.timestamp)}${current}`,
"info",
);
});
}
}
},
});
pi.registerCommand("emoji-history", {
description: "Show emoji history (24h)",
handler: async (_, ctx) => {
const history = getEmojiHistory(ctx);
if (history.length === 0) {
ctx.ui.notify("No history in past 24h", "info");
return;
}
const unique = new Set(history.map((h) => h.emoji));
ctx.ui.notify(
`📊 Emoji History - ${history.length} sessions, ${unique.size} unique`,
"info",
);
history.slice(0, 15).forEach((h, i) => {
const current =
h.sessionId === ctx.sessionManager.getSessionId() ? " (current)" : "";
ctx.ui.notify(
`${i + 1}. ${h.emoji} - ${formatTimeAgo(h.timestamp)}${current}`,
"info",
);
});
},
});
}
function setManualEmoji(ctx, pi, state, emoji) {
state.emoji = emoji;
state.assigned = true;
persistEmoji(ctx, pi, emoji);
ctx.ui.setStatus("0-emoji", emoji);
}

View file

@ -1,30 +0,0 @@
{
"id": "sf-tui",
"name": "SF TUI",
"version": "1.0.0",
"description": "Adds SF-specific header, footer, prompt history, color, emoji, and marketplace UI controls",
"tier": "bundled",
"requires": { "platform": ">=2.29.0" },
"provides": {
"commands": [
"color",
"color-char",
"color-config",
"color-next",
"color-set",
"emoji",
"emoji-config",
"emoji-history",
"emoji-set"
],
"hooks": [
"session_start",
"session_switch",
"before_agent_start",
"tool_result",
"agent_start",
"agent_end"
],
"shortcuts": ["Ctrl+Alt+H", "Ctrl+Shift+H", "Ctrl+Alt+M"]
}
}

View file

@ -1,227 +0,0 @@
import { truncateToWidth, visibleWidth } from "@singularity-forge/tui";
import { getAutoSession } from "../sf/auto/session.js";
import { refreshGitStatus } from "./git.js";
import { renderModeBadge } from "./header.js";
const RESET = "\x1b[0m";
const BOLD = "\x1b[1m";
const SE = {
ember40: "#ff8838",
gray60: "#8d877a",
stone60: "#6b6659",
paper: "#f7f5f1",
warning: "#ff8838",
success: "#24a148",
error: "#da1e28",
};
function hexToRgb(hex) {
const cleaned = hex.replace("#", "");
return {
r: parseInt(cleaned.slice(0, 2), 16),
g: parseInt(cleaned.slice(2, 4), 16),
b: parseInt(cleaned.slice(4, 6), 16),
};
}
function ansiFg(hex, text, bold = false) {
// Use 16-color ANSI codes for Termius compatibility
// Map hex colors to nearest standard ANSI color
const { r, g, b } = hexToRgb(hex);
const brightness = (r + g + b) / 3;
let colorCode;
if (brightness < 50) {
colorCode = 30; // black
} else if (brightness < 100) {
colorCode = 90; // bright black
} else if (r > g + b) {
colorCode = bold ? 91 : 31; // red
} else if (g > r + b) {
colorCode = bold ? 92 : 32; // green
} else if (b > r + g) {
colorCode = bold ? 94 : 34; // blue
} else if (r > 200 && g > 150) {
colorCode = bold ? 93 : 33; // yellow/orange
} else if (r > 200 && g < 100 && b > 150) {
colorCode = bold ? 95 : 35; // magenta
} else if (g > 200 && b > 150) {
colorCode = bold ? 96 : 36; // cyan
} else if (brightness > 200) {
colorCode = bold ? 97 : 37; // white
} else {
colorCode = bold ? 97 : 37; // default white
}
return `\x1b[${bold ? "1;" : ""}${colorCode}m${text}${RESET}`;
}
function toneHex(tone) {
switch (tone) {
case "accent":
case "warning":
return SE.ember40;
case "success":
return SE.success;
case "error":
return SE.error;
case "text":
return SE.paper;
default:
return SE.gray60;
}
}
function chip(label, value, tone = "text") {
return `${ansiFg(SE.gray60, `${label} `)}${ansiFg(toneHex(tone), value)}`;
}
function join(parts) {
return parts.filter(Boolean).join(ansiFg(SE.stone60, " | "));
}
function shorten(text, max) {
return text.length > max ? `${text.slice(0, Math.max(0, max - 3))}...` : text;
}
/** Minimal theme adapter so renderModeBadge (header.js) can run with footer's ANSI helpers. */
const FOOTER_THEME = {
fg: (tone, text) => ansiFg(toneHex(tone), text),
bold: (text) => `${BOLD}${text}${RESET}`,
};
function getSessionStats(ctx) {
let cost = 0;
let tokens = 0;
let cxPct = 0;
try {
for (const entry of ctx.sessionManager.getEntries()) {
if (entry.type === "message") {
const msg = entry.message;
if (msg?.role === "assistant" && msg.usage) {
cost += msg.usage.cost?.total || 0;
tokens += (msg.usage.input || 0) + (msg.usage.output || 0);
}
}
}
const cx = ctx.getContextUsage?.();
if (cx?.percent != null) cxPct = cx.percent;
} catch {
/* ignore */
}
return { cost, tokens, cxPct };
}
export function renderFooter(_theme, footerData, ctx, width) {
const git = refreshGitStatus(process.cwd());
const { cost, tokens, cxPct } = getSessionStats(ctx);
const session = getAutoSession();
const mode = session?.getMode?.();
const leftParts = [];
if (git.repo) {
leftParts.push(ansiFg(SE.ember40, git.repo, true));
} else {
leftParts.push(`${BOLD}${ansiFg(SE.ember40, "SF")}`);
}
if (git.branch) {
leftParts.push(chip("branch", git.branch, "muted"));
const state = git.dirty ? "dirty" : git.untracked ? "new" : "clean";
leftParts.push(
chip("state", state, state === "clean" ? "success" : "warning"),
);
if (git.added || git.deleted) {
leftParts.push(chip("diff", `+${git.added}/-${git.deleted}`, "warning"));
}
if (git.ahead || git.behind) {
const syncParts = [];
if (git.ahead) syncParts.push(`${git.ahead}`);
if (git.behind) syncParts.push(`${git.behind}`);
leftParts.push(chip("sync", syncParts.join(" "), "warning"));
}
if (git.lastCommit) {
leftParts.push(
chip(
"last",
`${git.lastCommit.timeAgo} ${shorten(git.lastCommit.message, 26)}`,
"muted",
),
);
}
}
const statuses = Array.from(footerData.getExtensionStatuses().entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([, text]) => String(text ?? "").trim())
.filter(Boolean);
if (statuses.length) {
leftParts.push(chip("status", statuses.join(" "), "accent"));
}
const rightParts = [];
if (mode) {
rightParts.push(renderModeBadge(FOOTER_THEME, mode, width < 80));
}
if (ctx.model) {
rightParts.push(
chip("model", `${ctx.model.provider}/${ctx.model.id}`, "text"),
);
}
if (cost > 0) {
rightParts.push(chip("spent", `$${cost.toFixed(2)}`, "warning"));
}
// Only show ctx% once the session has sent at least one message (avoid "1%" noise from system prompt at startup)
if (tokens > 0) {
const cxTone = cxPct >= 85 ? "error" : cxPct >= 60 ? "warning" : "success";
rightParts.push(chip("ctx", `${Math.round(cxPct)}%`, cxTone));
}
let rightLine = join(rightParts);
const maxRightWidth = Math.max(16, Math.floor(width * 0.55));
if (visibleWidth(rightLine) > maxRightWidth) {
rightLine = truncateToWidth(
rightLine,
maxRightWidth,
ansiFg(SE.gray60, "..."),
);
}
const rightWidth = visibleWidth(rightLine);
const leftBudget = Math.max(1, width - rightWidth - 2);
const leftLine = truncateToWidth(
join(leftParts),
leftBudget,
ansiFg(SE.gray60, "..."),
);
const gap = Math.max(1, width - visibleWidth(leftLine) - rightWidth);
const line = leftLine + " ".repeat(gap) + rightLine;
return [truncateToWidth(line, width, ansiFg(SE.gray60, "..."))];
}
/**
* Minimal auto-mode footer shows only mode badge + progress hint.
* Keeps the user aware SF is running autonomously without full footer noise.
*/
export function renderAutoFooter(_theme, footerData, ctx, width) {
const session = getAutoSession();
const mode = session?.getMode?.() ?? {
workMode: "build",
runControl: "autonomous",
permissionProfile: "normal",
modelMode: "smart",
};
const badge = renderModeBadge(FOOTER_THEME, mode, width < 80);
const leftParts = [`${BOLD}${ansiFg(SE.ember40, "SF")}`, badge].filter(
Boolean,
);
const statuses = Array.from(footerData.getExtensionStatuses().entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([, text]) => String(text ?? "").trim())
.filter(Boolean);
if (statuses.length) {
leftParts.push(ansiFg(SE.gray60, statuses.join(" ")));
}
const rightParts = [];
if (ctx.model) {
rightParts.push(ansiFg(SE.gray60, `${ctx.model.provider}/${ctx.model.id}`));
}
const { cost } = getSessionStats(ctx);
if (cost > 0) {
rightParts.push(ansiFg(SE.warning, `$${cost.toFixed(2)}`));
}
const leftLine = leftParts.join(" ");
const rightLine = rightParts.join(ansiFg(SE.gray60, " · "));
const rightWidth = visibleWidth(rightLine);
const gap = Math.max(1, width - visibleWidth(leftLine) - rightWidth);
const line = leftLine + " ".repeat(gap) + rightLine;
return [truncateToWidth(line, width, ansiFg(SE.gray60, "..."))];
}

View file

@ -1,158 +0,0 @@
import { execFileSync } from "node:child_process";
import { basename } from "node:path";
let cache = null;
let lastFetch = 0;
function getRepoName(cwd) {
try {
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
}).trim();
return root ? basename(root) : basename(cwd) || null;
} catch {
return basename(cwd) || null;
}
}
function getLastCommit(cwd) {
try {
const raw = execFileSync("git", ["log", "-1", "--format=%cr|%s"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
}).trim();
const sep = raw.indexOf("|");
if (sep > 0) {
return {
timeAgo: raw.slice(0, sep).replace(/ ago$/, ""),
message: raw.slice(sep + 1),
};
}
} catch {
/* ignore */
}
return null;
}
function getDiffStats(cwd) {
try {
const raw = execFileSync("git", ["diff", "HEAD", "--stat"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
});
let added = 0;
let deleted = 0;
let modified = 0;
for (const line of raw.split("\n")) {
const addMatch = line.match(/(\d+) insertion/);
const delMatch = line.match(/(\d+) deletion/);
if (addMatch || delMatch) {
const a = addMatch ? parseInt(addMatch[1], 10) : 0;
const d = delMatch ? parseInt(delMatch[1], 10) : 0;
if (a) added += a;
if (d) deleted += d;
if (a || d) modified++;
}
}
return { added, deleted, modified };
} catch {
return { added: 0, deleted: 0, modified: 0 };
}
}
export function refreshGitStatus(cwd) {
const now = Date.now();
if (now - lastFetch < 400 && cache) return cache;
lastFetch = now;
const repo = getRepoName(cwd);
let branch = null;
try {
branch =
execFileSync("git", ["branch", "--show-current"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
}).trim() || null;
} catch {
cache = {
repo,
branch: null,
dirty: false,
untracked: false,
ahead: 0,
behind: 0,
added: 0,
deleted: 0,
modified: 0,
lastCommit: null,
};
return cache;
}
try {
const status = execFileSync("git", ["status", "--porcelain"], {
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
});
const lines = status.split("\n").filter((l) => l.length > 2);
const dirty = lines.some((l) => {
const x = l[0] ?? " ";
const y = l[1] ?? " ";
return (x !== "?" && x !== " " && x !== "!") || (y !== " " && y !== "?");
});
const untracked = lines.some((l) => l.startsWith("??"));
let ahead = 0;
let behind = 0;
try {
const ab = execFileSync(
"git",
["rev-list", "--left-right", "--count", "HEAD...@{u}"],
{
cwd,
encoding: "utf-8",
stdio: ["pipe", "pipe", "ignore"],
timeout: 1500,
},
).trim();
const [a, b] = ab.split("\t").map((n) => parseInt(n, 10));
ahead = Number.isNaN(a) ? 0 : a;
behind = Number.isNaN(b) ? 0 : b;
} catch {
/* no upstream */
}
const diff = getDiffStats(cwd);
const lastCommit = getLastCommit(cwd);
cache = {
repo,
branch,
dirty,
untracked,
ahead,
behind,
...diff,
lastCommit,
};
} catch {
cache = {
repo,
branch,
dirty: false,
untracked: false,
ahead: 0,
behind: 0,
added: 0,
deleted: 0,
modified: 0,
lastCommit: getLastCommit(cwd),
};
}
return cache;
}
export function invalidateGitStatus() {
lastFetch = 0;
}

View file

@ -1,168 +0,0 @@
import { basename } from "node:path";
import { truncateToWidth, visibleWidth } from "@singularity-forge/tui";
import { getAutoSession } from "../sf/auto/session.js";
import { refreshGitStatus } from "./git.js";
function align(left, right, width, ellipsis) {
const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
return truncateToWidth(left + " ".repeat(gap) + right, width, ellipsis);
}
function compactModeBadge(mode) {
const map = {
chat: "C",
plan: "P",
build: "B",
review: "R",
repair: "F",
research: "S",
};
return map[mode] ?? "?";
}
function compactRunControlBadge(rc) {
const map = {
manual: "M",
assisted: "A",
autonomous: "∞",
};
return map[rc] ?? "?";
}
function compactPermissionBadge(pp) {
const map = {
restricted: "R",
normal: "N",
trusted: "T",
unrestricted: "U",
};
return map[pp] ?? "?";
}
function compactModelModeBadge(mm) {
const map = {
fast: "F",
smart: "S",
deep: "D",
};
return map[mm] ?? "?";
}
function renderModeBadge(theme, mode, compact) {
if (!mode) return "";
const th = theme;
const paused = mode.paused === true;
if (compact) {
const badges = [
paused ? th.fg("dim", "P!") : "",
th.fg(paused ? "dim" : "accent", compactModeBadge(mode.workMode)),
th.fg("dim", compactRunControlBadge(mode.runControl)),
th.fg(
paused ? "dim" : "warning",
compactPermissionBadge(mode.permissionProfile),
),
th.fg(paused ? "dim" : "success", compactModelModeBadge(mode.modelMode)),
].filter(Boolean);
return `[${badges.join("")}]`;
}
const parts = [
paused ? th.fg("dim", "paused") : "",
paused ? th.fg("dim", "·") : "",
th.fg(paused ? "dim" : "accent", mode.workMode),
th.fg("dim", "·"),
th.fg("dim", mode.runControl),
th.fg("dim", "·"),
th.fg(paused ? "dim" : "warning", mode.permissionProfile),
th.fg("dim", "·"),
th.fg(paused ? "dim" : "success", mode.modelMode),
].filter(Boolean);
return parts.join(" ");
}
export { renderModeBadge };
/**
* Minimal auto-mode header shows only mode badge + project name.
* Keeps the user aware SF is running autonomously without full header noise.
*/
export function renderAutoHeader(theme, ctx, width) {
const th = theme;
const projectName = basename(process.cwd());
const session = getAutoSession();
const mode = session?.getMode?.() ?? {
workMode: "build",
runControl: "autonomous",
permissionProfile: "normal",
modelMode: "smart",
};
const modeBadge = renderModeBadge(th, mode, width < 80);
const left = [
th.bold(th.fg("accent", "SF")),
th.fg("dim", "▸"),
th.fg("text", projectName),
modeBadge ? th.fg("dim", "·") : "",
modeBadge,
]
.filter(Boolean)
.join(" ");
const model = ctx.model
? `${ctx.model.provider}/${ctx.model.id}`.replace(/^\/+/, "")
: "";
const right = model ? th.fg("dim", model) : "";
const ellipsis = th.fg("dim", "…");
return [align(left, right, width, ellipsis)];
}
export function renderHeader(theme, ctx, width) {
const th = theme;
const git = refreshGitStatus(process.cwd());
const projectName = basename(process.cwd());
const mode = ctx.sessionManager?.getMode?.() ?? getAutoSession().getMode();
const model = ctx.model
? `${ctx.model.provider}/${ctx.model.id}`.replace(/^\/+/, "")
: "";
const modelLabel = model
? `${th.fg("dim", "model ")}${th.fg("text", model)}`
: "";
const modeBadge = renderModeBadge(th, mode, width < 80);
const topLeft = [
th.fg("accent", "╭─"),
th.bold(th.fg("accent", "SF")),
th.fg("dim", "▸"),
th.fg("text", projectName),
modeBadge ? th.fg("dim", "·") : "",
modeBadge,
]
.filter(Boolean)
.join(" ");
const branchState = git.branch
? git.dirty
? th.fg("warning", "modified")
: git.untracked
? th.fg("warning", "untracked")
: th.fg("success", "clean")
: th.fg("dim", "no git");
const branchLabel = git.branch
? `${th.fg("dim", "branch ")}${th.fg("accent", git.branch)} ${th.fg("dim", "·")} ${branchState}`
: branchState;
const sync = [];
if (git.ahead) sync.push(th.fg("success", `${git.ahead}`));
if (git.behind) sync.push(th.fg("warning", `${git.behind}`));
if (git.added || git.deleted) {
sync.push(th.fg("muted", `Δ +${git.added}/-${git.deleted}`));
}
const bottomRight = sync.join(th.fg("dim", " "));
const ellipsis = th.fg("dim", "…");
const top = align(topLeft, modelLabel, width, ellipsis);
if (width < 64) return [top];
const bottom = align(
`${th.fg("accent", "╰─")} ${branchLabel}`,
bottomRight,
width,
ellipsis,
);
return [top, bottom];
}

View file

@ -1,581 +0,0 @@
/**
* SF-TUI Unified TUI enhancements for Singularity Forge
*
* Features:
* - Powerline footer: git branch, diff stats, last commit, model, cost, context
* - Header: project name + branch + model
* - Prompt history: Ctrl+Alt+H overlay
* - Mode cycling: Ctrl+Shift+M, Ctrl+Shift+R, Ctrl+Shift+A, Ctrl+Shift+S
*/
import { execSync, spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { platform } from "node:os";
import { Key } from "@singularity-forge/tui";
import { getAutoSession } from "../sf/auto/session.js";
import { isAutoActive } from "../sf/auto.js";
import { projectRoot } from "../sf/commands/context.js";
import {
getExperimentalFlag,
setExperimentalFlag,
} from "../sf/experimental.js";
import { registerSessionColor } from "./color-band.js";
import { registerSessionEmoji } from "./emoji.js";
import { renderAutoFooter, renderFooter } from "./footer.js";
import { invalidateGitStatus } from "./git.js";
import { renderAutoHeader, renderHeader } from "./header.js";
import { openMarketplaceOverlay } from "./marketplace.js";
import {
appendPromptHistory,
openPromptHistoryOverlay,
pushPromptHistory,
readPromptHistory,
} from "./prompt-history.js";
const WORK_MODE_CYCLE = [
"chat",
"plan",
"build",
"review",
"repair",
"research",
];
const PERMISSION_PROFILE_CYCLE = [
"restricted",
"normal",
"trusted",
"unrestricted",
];
function cycleWorkMode(ctx) {
const session = getAutoSession();
const current = session.getMode().workMode;
const idx = WORK_MODE_CYCLE.indexOf(current);
const next = WORK_MODE_CYCLE[(idx + 1) % WORK_MODE_CYCLE.length];
const transition = session.setMode({ workMode: next });
ctx.ui.notify(
`Mode: ${transition.from.workMode}${transition.to.workMode}`,
"info",
);
}
function setWorkMode(ctx, mode) {
const session = getAutoSession();
const transition = session.setMode({ workMode: mode });
ctx.ui.notify(
`Mode: ${transition.from.workMode}${transition.to.workMode}`,
"info",
);
}
function setRunControl(ctx, rc) {
const session = getAutoSession();
const transition = session.setMode({ runControl: rc });
ctx.ui.notify(
`Run control: ${transition.from.runControl}${transition.to.runControl}`,
"info",
);
}
function cyclePermissionProfile(ctx) {
const session = getAutoSession();
const current = session.getMode().permissionProfile;
const idx = PERMISSION_PROFILE_CYCLE.indexOf(current);
const next =
PERMISSION_PROFILE_CYCLE[(idx + 1) % PERMISSION_PROFILE_CYCLE.length];
const transition = session.setMode({ permissionProfile: next });
ctx.ui.notify(
`Trust: ${transition.from.permissionProfile}${transition.to.permissionProfile}`,
"info",
);
}
function installHeader(ctx) {
if (!ctx.hasUI) return;
ctx.ui.setHeader((_tui, theme) => {
return {
render: (width) => {
if (isAutoActive()) {
return renderAutoHeader(theme, ctx, width);
}
return renderHeader(theme, ctx, width);
},
invalidate: () => {},
dispose: () => {},
};
});
}
function installFooter(ctx) {
if (!ctx.hasUI) return;
ctx.ui.setFooter((_tui, theme, footerData) => {
return {
render: (width) => {
if (isAutoActive()) {
return renderAutoFooter(theme, footerData, ctx, width);
}
return renderFooter(theme, footerData, ctx, width);
},
invalidate: () => {},
dispose: () => {},
};
});
}
export default function sfTui(pi) {
registerSessionEmoji(pi);
registerSessionColor(pi);
const promptHistory = readPromptHistory();
let promptHistorySessionId = randomUUID();
let projectBasePath = null;
let wasAutoActive = false;
pi.on("session_start", async (_event, ctx) => {
if (!ctx.hasUI) return;
promptHistorySessionId =
ctx.sessionManager?.getSessionId?.() ?? promptHistorySessionId;
try {
projectBasePath = projectRoot();
const projectPromptHistory = readPromptHistory(projectBasePath);
promptHistory.splice(0, promptHistory.length, ...projectPromptHistory);
} catch {
projectBasePath = null;
}
installHeader(ctx);
installFooter(ctx);
const openProjectPromptHistory = (overlayCtx) =>
openPromptHistoryOverlay(overlayCtx, projectBasePath ?? undefined);
pi.registerShortcut(Key.ctrlAlt("h"), {
description: "Open prompt history",
handler: openProjectPromptHistory,
});
pi.registerShortcut(Key.ctrlShift("h"), {
description: "Open prompt history (fallback)",
handler: openProjectPromptHistory,
});
pi.registerShortcut(Key.ctrlAlt("m"), {
description: "Open marketplace browser",
handler: openMarketplaceOverlay,
});
// Mode cycling shortcuts
pi.registerShortcut(Key.ctrlShift("m"), {
description: "Cycle work mode (chat→plan→build→review→repair→research)",
handler: () => cycleWorkMode(ctx),
});
pi.registerShortcut(Key.ctrlShift("r"), {
description: "Set work mode to repair",
handler: () => setWorkMode(ctx, "repair"),
});
pi.registerShortcut(Key.ctrlShift("a"), {
description: "Set run control to autonomous",
handler: () => setRunControl(ctx, "autonomous"),
});
pi.registerShortcut(Key.ctrlShift("s"), {
description: "Set run control to assisted (step)",
handler: () => setRunControl(ctx, "assisted"),
});
pi.registerShortcut(Key.ctrlShift("p"), {
description:
"Cycle permission profile (restricted→normal→trusted→unrestricted)",
handler: () => cyclePermissionProfile(ctx),
});
// Ctrl+G — open current project in $EDITOR (or notify if none)
pi.registerShortcut(Key.ctrl("g"), {
description: "Open project root in $EDITOR",
handler: () => {
const editor = process.env.EDITOR || process.env.VISUAL;
if (!editor) {
ctx.ui.notify(
"No $EDITOR set. Set EDITOR=code (or vim, nano, etc.) in your shell.",
"warning",
);
return;
}
spawn(editor, [projectRoot() ?? "."], {
stdio: "ignore",
detached: true,
}).unref();
ctx.ui.notify(`Opened ${editor} ${projectRoot() ?? "."}`, "info");
},
});
// Ctrl+T — toggle reasoning display
pi.registerShortcut(Key.ctrl("t"), {
description: "Toggle extended thinking / reasoning display",
handler: () => {
const current = getExperimentalFlag("show_reasoning");
setExperimentalFlag("show_reasoning", !current);
ctx.ui.notify(`Reasoning display ${current ? "OFF" : "ON"}`, "info");
},
});
// Ctrl+X B — open background session switcher (BACKGROUND_SESSIONS flag)
pi.registerShortcut(Key.ctrlAlt("b"), {
description: "Open background session switcher",
handler: async () => {
if (!getExperimentalFlag("background_sessions")) {
ctx.sendMessage?.("/tasks");
return;
}
await openBgSessionSwitcher(ctx);
},
});
// Ctrl+X O — open URL from last agent message
pi.registerShortcut(Key.ctrlAlt("o"), {
description: "Open last URL from agent output in browser",
handler: () => {
const entries = ctx.sessionManager?.getEntries?.() ?? [];
const lastText =
[...entries].reverse().find((e) => e.type === "assistant")?.content ??
"";
const urlMatch = String(lastText).match(/https?:\/\/[^\s"'<>)]+/);
if (!urlMatch) {
ctx.ui.notify("No URL found in last agent output.", "info");
return;
}
const url = urlMatch[0];
try {
const openCmd =
platform() === "darwin"
? "open"
: platform() === "win32"
? "start"
: "xdg-open";
execSync(`${openCmd} "${url}"`, { stdio: "ignore" });
ctx.ui.notify(`Opened: ${url}`, "info");
} catch {
ctx.ui.notify(`URL: ${url}`, "info");
}
},
});
// STATUS_LINE — spawn user script every 5s, pipe stdout to footer chip
startStatusLineRunner(ctx);
wasAutoActive = isAutoActive();
});
pi.on("before_agent_start", async (event) => {
const prompt = event.prompt?.trim();
if (prompt) {
pushPromptHistory(promptHistory, prompt);
appendPromptHistory(
prompt,
projectBasePath ?? undefined,
promptHistorySessionId,
);
pi.appendEntry("sf-prompt-history", {
prompt,
projectRoot: projectBasePath,
sessionId: promptHistorySessionId,
timestamp: Date.now(),
});
}
});
pi.on("tool_result", async (_event, ctx) => {
invalidateGitStatus();
const autoNow = isAutoActive();
if (!autoNow && wasAutoActive) {
installHeader(ctx);
installFooter(ctx);
}
wasAutoActive = autoNow;
});
pi.on("agent_end", async (_event, ctx) => {
const autoNow = isAutoActive();
if (!autoNow) {
installHeader(ctx);
installFooter(ctx);
}
wasAutoActive = autoNow;
});
// SHOW_FILE tool — renders a file path + optional line range as a code block
// in the agent timeline when the experimental flag is enabled.
pi.registerTool({
name: "show_file",
description:
"Display a file (or a section of it) as a highlighted code block in the conversation timeline. Use when you want to show the user specific code without just dumping text.",
parameters: {
type: "object",
properties: {
path: {
type: "string",
description: "Absolute or relative path to the file",
},
start_line: {
type: "number",
description: "First line to display (1-indexed, optional)",
},
end_line: {
type: "number",
description: "Last line to display (inclusive, optional)",
},
},
required: ["path"],
},
execute: async ({ path: filePath, start_line, end_line }) => {
if (!getExperimentalFlag("show_file")) {
return {
output:
"SHOW_FILE is not enabled. Run /experimental on show_file to enable.",
};
}
const { readFileSync, existsSync } = await import("node:fs");
const { resolve } = await import("node:path");
const abs = resolve(projectRoot() ?? ".", filePath);
if (!existsSync(abs)) {
return { output: `File not found: ${filePath}` };
}
const raw = readFileSync(abs, "utf-8");
const lines = raw.split("\n");
const from = start_line != null ? Math.max(1, start_line) - 1 : 0;
const to =
end_line != null ? Math.min(lines.length, end_line) : lines.length;
const slice = lines.slice(from, to).join("\n");
const ext = abs.split(".").pop() ?? "";
return { output: `\`\`\`${ext}\n${slice}\n\`\`\`` };
},
renderResult: ({ output }) => output,
});
// ASK_USER_ELICITATION — structured form-based ask_user replacement.
// When the flag is on and the agent calls this tool with choices, a TUI
// select overlay is shown instead of a plain text prompt.
pi.registerTool({
name: "ask_user_elicitation",
description:
"Ask the user a question using a structured form with optional choices. When ASK_USER_ELICITATION is enabled this is preferred over plain ask_user for questions with known choices.",
parameters: {
type: "object",
properties: {
question: {
type: "string",
description: "The question to ask the user",
},
choices: {
type: "array",
items: { type: "string" },
description: "Optional list of choices to show as a select menu",
},
allow_freeform: {
type: "boolean",
description:
"Whether to allow freeform text input in addition to choices",
},
},
required: ["question"],
},
execute: async ({ question, choices, allow_freeform }, ctx) => {
if (!ctx?.hasUI) {
return { output: "No UI available for elicitation." };
}
if (!getExperimentalFlag("ask_elicitation")) {
return {
output:
"ASK_USER_ELICITATION is not enabled. Run /experimental on ask_elicitation to enable.",
};
}
if (choices?.length) {
const answer = await ctx.ui.select(question, choices);
if (!answer && allow_freeform) {
const freeform = await ctx.ui.input(question);
return { output: freeform ?? "" };
}
return { output: answer ?? "" };
}
const answer = await ctx.ui.input(question);
return { output: answer ?? "" };
},
renderResult: ({ output }) => (output ? `**Answer:** ${output}` : ""),
});
// MULTI_TURN_AGENTS — persistent named sub-agent sessions via file-backed state.
// Tool that spawns or resumes a named SF child process, relaying messages.
pi.registerTool({
name: "spawn_agent",
description:
"Spawn or resume a named persistent sub-agent. Sends a message and waits for the response. The agent persists across calls using file-backed state in .sf/agents/<name>/.",
parameters: {
type: "object",
properties: {
name: {
type: "string",
description:
"Unique agent name (alphanumeric + hyphens, e.g. 'researcher')",
},
message: {
type: "string",
description: "Message to send to the agent",
},
reset: {
type: "boolean",
description:
"If true, clear the agent's state and start fresh (default: false)",
},
},
required: ["name", "message"],
},
execute: async ({ name, message, reset }) => {
if (!getExperimentalFlag("multi_turn_agents")) {
return {
output:
"MULTI_TURN_AGENTS is not enabled. Run /experimental on multi_turn_agents to enable.",
};
}
if (!/^[a-z0-9-]{1,32}$/i.test(name)) {
return {
output: "Agent name must be 1-32 alphanumeric/hyphen characters.",
};
}
const { join: pathJoin } = await import("node:path");
const { mkdirSync, writeFileSync, readFileSync, existsSync } =
await import("node:fs");
const stateDir = pathJoin(
projectRoot() ?? process.cwd(),
".sf",
"agents",
name,
);
mkdirSync(stateDir, { recursive: true });
const historyPath = pathJoin(stateDir, "history.jsonl");
if (reset && existsSync(historyPath)) {
writeFileSync(historyPath, "", "utf-8");
}
// Append user message to history
const entry = JSON.stringify({
role: "user",
content: message,
ts: Date.now(),
});
const { appendFileSync } = await import("node:fs");
appendFileSync(historyPath, `${entry}\n`, "utf-8");
// Dispatch to SF headless with the conversation history as context
const historyLines = existsSync(historyPath)
? readFileSync(historyPath, "utf-8")
.trim()
.split("\n")
.filter(Boolean)
.map((l) => {
try {
return JSON.parse(l);
} catch {
return null;
}
})
.filter(Boolean)
: [];
const contextMsg = historyLines
.slice(-10) // last 10 turns for context
.map((e) => `${e.role === "user" ? "User" : "Agent"}: ${e.content}`)
.join("\n");
const fullPrompt = `[Agent: ${name}]\n\nConversation history:\n${contextMsg}\n\nRespond to the last user message only.`;
const { execFile } = await import("node:child_process");
const { promisify } = await import("node:util");
const execFileAsync = promisify(execFile);
try {
const { stdout } = await execFileAsync(
process.execPath,
[
"-y",
"node@24",
process.env.SF_LOADER ?? "dist/loader.js",
"headless",
"--print",
fullPrompt,
],
{
timeout: 60000,
encoding: "utf-8",
env: { ...process.env },
},
);
const response = stdout.trim();
appendFileSync(
historyPath,
`${JSON.stringify({ role: "assistant", content: response, ts: Date.now() })}\n`,
"utf-8",
);
return { output: response };
} catch (err) {
return {
output: `Agent dispatch failed: ${err instanceof Error ? err.message : String(err)}`,
};
}
},
renderResult: ({ output }) => output,
});
}
/** Run the STATUS_LINE user script on a 5s interval, posting stdout to footer. */
let _statusLineInterval = null;
function startStatusLineRunner(ctx) {
if (_statusLineInterval) {
clearInterval(_statusLineInterval);
_statusLineInterval = null;
}
const tick = async () => {
if (!getExperimentalFlag("status_line")) return;
const scriptPath = getExperimentalFlag("status_line_script");
if (!scriptPath || typeof scriptPath !== "string") return;
try {
const { execFile } = await import("node:child_process");
const { promisify } = await import("node:util");
const execFileAsync = promisify(execFile);
const { stdout } = await execFileAsync(scriptPath, [], {
timeout: 4000,
encoding: "utf-8",
});
const text = stdout.trim().slice(0, 120);
if (text) {
ctx.ui.setStatus?.("sf-status-line", text);
}
} catch {
/* Non-fatal — script may be missing or timing out */
}
};
_statusLineInterval = setInterval(tick, 5000);
tick();
}
// ─── Background Session Switcher ─────────────────────────────────────────────
const BG_SESSIONS_FILE = ".sf/sessions-queue.json";
/**
* Open a TUI overlay listing background sessions from .sf/sessions-queue.json.
* Selecting one sends /resume <sessionId> to the chat.
*
* Purpose: BACKGROUND_SESSIONS parity let users switch between concurrent
* sessions without leaving the TUI.
* Consumer: Ctrl+Alt+B keybinding (session_start hook).
*/
async function openBgSessionSwitcher(ctx) {
if (!ctx?.hasUI) return;
const { existsSync, readFileSync } = await import("node:fs");
const { join: pathJoin } = await import("node:path");
let root;
try {
root = projectRoot();
} catch {
root = process.cwd();
}
const queuePath = pathJoin(root ?? process.cwd(), BG_SESSIONS_FILE);
let sessions = [];
if (existsSync(queuePath)) {
try {
sessions = JSON.parse(readFileSync(queuePath, "utf-8"));
} catch {
/* malformed */
}
}
if (!sessions.length) {
ctx.ui.notify(
"No background sessions queued.\nStart one with /resume <description> or via BACKGROUND_SESSIONS controls.",
"info",
);
return;
}
const labels = sessions.map(
(s, i) =>
`${(i + 1).toString().padStart(2)}. ${s.id ?? "unknown"}${s.summary ?? "no summary"}`,
);
const chosen = await ctx.ui.select("Switch to session:", labels);
if (!chosen) return;
const idx = labels.indexOf(chosen);
const session = sessions[idx];
if (session?.id) {
ctx.sendMessage?.(`/resume ${session.id}`);
}
}

View file

@ -1,346 +0,0 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import {
Key,
matchesKey,
truncateToWidth,
visibleWidth,
} from "@singularity-forge/tui";
import { getExperimentalFlag } from "../sf/experimental.js";
const CATEGORIES = ["all", "extension", "skill", "theme"];
const FEATURED = [
{
id: "agents-filter-output",
name: "Filter Output",
source: "featured",
category: "extension",
description: "Redact secrets from tool results",
},
{
id: "agents-security",
name: "Security",
source: "featured",
category: "extension",
description: "Block dangerous commands and protected paths",
},
{
id: "pi-hooks-permission",
name: "Permission",
source: "featured",
category: "extension",
description: "4-level permission control for bash/write/edit",
},
{
id: "shitty-usage-bar",
name: "Usage Bar",
source: "featured",
category: "extension",
description: "Live AI provider quota & status",
},
{
id: "rhubarb-bg-notify",
name: "Background Notify",
source: "featured",
category: "extension",
description: "Notify when background tasks complete",
},
{
id: "pi-dcp",
name: "Dynamic Context Pruning",
source: "featured",
category: "extension",
description: "Intelligent conversation context pruning",
},
{
id: "pi-powerline-footer",
name: "Powerline Footer",
source: "featured",
category: "extension",
description: "Git-integrated status bar components",
},
];
function scanInstalledExtensions(dir, sourceLabel) {
if (!existsSync(dir)) return [];
const items = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const extPath = join(dir, entry.name);
const pkgPath = join(extPath, "package.json");
let name = entry.name;
let description = "";
try {
if (existsSync(pkgPath)) {
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
name = pkg.name || name;
description = pkg.description || "";
}
} catch {
/* ignore */
}
items.push({
id: entry.name,
name,
source: sourceLabel,
category: "extension",
description,
path: extPath,
});
}
return items;
}
function buildCatalog() {
const installed = scanInstalledExtensions(
join(homedir(), ".sf", "agent", "extensions"),
"installed",
);
const piCompat = scanInstalledExtensions(
join(homedir(), ".pi", "agent", "extensions"),
"pi-compat",
);
const piLegacy = scanInstalledExtensions(
join(homedir(), ".pi", "extensions"),
"pi-compat",
);
const all = [...installed, ...piCompat, ...piLegacy];
const seen = new Set(all.map((i) => i.id));
for (const f of FEATURED) {
if (!seen.has(f.id)) all.push(f);
}
return all.sort((a, b) => {
if (a.source === "installed" && b.source !== "installed") return -1;
if (b.source === "installed" && a.source !== "installed") return 1;
return a.name.localeCompare(b.name);
});
}
class MarketplaceOverlay {
tui;
theme;
onClose;
items;
filtered;
sel = 0;
catIdx = 0;
scroll = 0;
cacheW = 0;
cacheL = [];
constructor(tui, theme, items, onClose) {
this.tui = tui;
this.theme = theme;
this.items = items;
this.onClose = onClose;
this.filtered = this.applyFilter();
}
get category() {
return CATEGORIES[this.catIdx];
}
applyFilter() {
if (this.category === "all") return this.items;
return this.items.filter((i) => i.category === this.category);
}
handleInput(data) {
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
this.onClose();
return;
}
if (matchesKey(data, Key.down) || data === "j") {
this.sel = Math.min(this.filtered.length - 1, this.sel + 1);
this.invalidate();
this.tui.requestRender();
return;
}
if (matchesKey(data, Key.up) || data === "k") {
this.sel = Math.max(0, this.sel - 1);
this.invalidate();
this.tui.requestRender();
return;
}
if (data === "f") {
this.catIdx = (this.catIdx + 1) % CATEGORIES.length;
this.sel = 0;
this.scroll = 0;
this.filtered = this.applyFilter();
this.invalidate();
this.tui.requestRender();
return;
}
if (matchesKey(data, Key.return) || matchesKey(data, Key.enter)) {
const item = this.filtered[this.sel];
if (item) {
if (item.source === "installed") {
// Already installed — close
this.onClose();
} else {
// Trigger install via npm
this.onClose();
installExtensionNpm(item.id, item.name);
}
} else {
this.onClose();
}
}
}
invalidate() {
this.cacheW = 0;
}
render(width) {
if (this.cacheW === width) return this.cacheL;
const th = this.theme;
const bw = Math.min(90, width - 4);
const iw = bw - 4;
const maxRows = Math.max(6, (process.stdout.rows || 24) - 10);
const pad = (s) => s + " ".repeat(Math.max(0, width - visibleWidth(s)));
const box = (s) => {
const len = visibleWidth(s);
return (
th.fg("dim", "│ ") +
s +
" ".repeat(Math.max(0, bw - 2 - len)) +
th.fg("dim", " │")
);
};
const lines = [];
lines.push(pad(th.fg("dim", "╭" + "─".repeat(bw) + "╮")));
lines.push(pad(box(th.bold(th.fg("accent", "📦 Marketplace")))));
lines.push(pad(th.fg("dim", "├" + "─".repeat(bw) + "┤")));
const filterLabel =
this.category === "all"
? th.fg("dim", "all")
: th.fg("accent", this.category);
lines.push(
pad(
box(
`${th.fg("dim", "filter:")} ${filterLabel} ${th.fg("dim", "↑/jk navigate • f filter • Enter install • Esc close")}`,
),
),
);
lines.push(pad(box("")));
const visibleItems = this.filtered;
if (!visibleItems.length) {
lines.push(pad(box(th.fg("dim", "No packages found."))));
} else {
this.scroll = Math.min(
this.scroll,
Math.max(0, visibleItems.length - maxRows),
);
this.sel = Math.min(this.sel, visibleItems.length - 1);
if (this.sel < this.scroll) this.scroll = this.sel;
if (this.sel >= this.scroll + maxRows)
this.scroll = this.sel - maxRows + 1;
for (
let i = this.scroll;
i < Math.min(visibleItems.length, this.scroll + maxRows);
i++
) {
const item = visibleItems[i];
const ptr = i === this.sel ? th.fg("accent", " ") : " ";
const srcIcon =
item.source === "installed"
? th.fg("success", "● ")
: item.source === "pi-compat"
? th.fg("warning", "◐ ")
: th.fg("dim", "○ ");
const name =
i === this.sel
? th.fg("accent", item.name)
: th.fg("text", item.name);
const desc = th.fg(
"dim",
truncateToWidth(
item.description,
Math.max(10, iw - visibleWidth(`${ptr}${srcIcon}${item.name} `)),
),
);
lines.push(pad(box(`${ptr}${srcIcon}${name} ${desc}`)));
}
}
lines.push(pad(box("")));
lines.push(pad(th.fg("dim", "├" + "─".repeat(bw) + "┤")));
lines.push(
pad(
box(
th.fg(
"dim",
`${visibleItems.length} packages • ${this.items.filter((i) => i.source === "installed").length} installed`,
),
),
),
);
lines.push(pad(th.fg("dim", "╰" + "─".repeat(bw) + "╯")));
lines.push("");
this.cacheL = lines;
this.cacheW = width;
return lines;
}
}
export async function openMarketplaceOverlay(ctx) {
if (!ctx.hasUI) {
ctx.ui.notify("Marketplace requires interactive mode", "error");
return;
}
const items = buildCatalog();
await ctx.ui.custom(
(tui, theme, _kb, done) => {
const overlay = new MarketplaceOverlay(tui, theme, items, () =>
done(true),
);
return {
render: (w) => overlay.render(w),
invalidate: () => overlay.invalidate(),
handleInput: (d) => overlay.handleInput(d),
};
},
{
overlay: true,
overlayOptions: {
width: "92%",
minWidth: 70,
maxHeight: "88%",
anchor: "center",
backdrop: true,
},
},
);
}
/**
* Install a marketplace extension via npm into ~/.sf/agent/extensions/.
*
* Purpose: wire the EXTENSIONS experimental flag press Enter on a featured
* package to trigger `npm install` into the user extensions directory.
* Consumer: MarketplaceOverlay.handleInput on Enter for non-installed items.
*/
export function installExtensionNpm(packageId, displayName) {
if (!getExperimentalFlag("extensions")) {
// Silently skip if EXTENSIONS flag is off; overlay already closed
return;
}
import("node:child_process").then(({ spawn }) => {
const target = join(homedir(), ".sf", "agent", "extensions");
const proc = spawn("npm", ["install", "--prefix", target, packageId], {
stdio: ["ignore", "pipe", "pipe"],
detached: false,
});
let stderr = "";
proc.stderr?.on("data", (d) => {
stderr += d.toString();
});
proc.on("close", (code) => {
if (code === 0) {
process.stdout.write(
`\r\n\x1b[32m✓ Installed ${displayName || packageId}\x1b[0m\r\n`,
);
} else {
process.stdout.write(
`\r\n\x1b[31m✗ Install failed for ${displayName || packageId}: ${stderr.slice(0, 200)}\x1b[0m\r\n`,
);
}
});
proc.on("error", () => {
process.stdout.write(
`\r\n\x1b[33m⚠ npm not found — install npm to enable extension installs\x1b[0m\r\n`,
);
});
});
}

View file

@ -1,157 +0,0 @@
import { truncateToWidth, visibleWidth } from "@singularity-forge/tui";
const RESET = "\x1b[0m";
function fgCode(color) {
switch (color) {
case "black":
return "30";
case "red":
return "31";
case "green":
return "32";
case "yellow":
return "33";
case "blue":
return "34";
case "magenta":
return "35";
case "cyan":
return "36";
case "white":
return "37";
case "brightBlack":
return "90";
case "brightRed":
return "91";
case "brightGreen":
return "92";
case "brightYellow":
return "93";
case "brightBlue":
return "94";
case "brightMagenta":
return "95";
case "brightCyan":
return "96";
case "brightWhite":
return "97";
default:
return "39";
}
}
function bgCode(color) {
switch (color) {
case "black":
return "40";
case "red":
return "41";
case "green":
return "42";
case "yellow":
return "43";
case "blue":
return "44";
case "magenta":
return "45";
case "cyan":
return "46";
case "white":
return "47";
case "brightBlack":
return "100";
case "brightRed":
return "101";
case "brightGreen":
return "102";
case "brightYellow":
return "103";
case "brightBlue":
return "104";
case "brightMagenta":
return "105";
case "brightCyan":
return "106";
case "brightWhite":
return "107";
default:
return "49";
}
}
function ansi(fg, bg, bold) {
const codes = [];
if (bold) codes.push("1");
if (fg) codes.push(fgCode(fg));
if (bg) codes.push(bgCode(bg));
return codes.length ? `\x1b[${codes.join(";")}m` : RESET;
}
export function renderPowerline(segments, width, theme) {
if (!segments.length) return "";
const SEP = "";
const _SEP_WIDTH = visibleWidth(SEP);
// Build raw segments with separators
const parts = [];
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
const next = segments[i + 1];
const text = ` ${seg.text} `;
const segAnsi = ansi(seg.fg, seg.bg, seg.bold);
parts.push(segAnsi + text);
if (next) {
// Separator uses current bg as fg, next bg as bg
const sepAnsi = ansi(seg.bg, next.bg, false);
parts.push(sepAnsi + SEP);
} else {
// Final separator: current bg as fg, default bg
const sepAnsi = ansi(seg.bg, undefined, false);
parts.push(sepAnsi + SEP);
}
}
const line = parts.join("") + RESET;
const vis = visibleWidth(line);
// If too wide, drop non-essential segments from the right
if (vis > width && segments.length > 2) {
const trimmed = segments.slice(0, -1);
return renderPowerline(trimmed, width, theme);
}
if (vis > width) return truncateToWidth(line, width, "");
// Pad right to fill width
if (vis < width) {
return line + " ".repeat(width - vis) + RESET;
}
return line;
}
export function renderPowerlineRight(segments, width, theme) {
if (!segments.length) return "";
const SEP = "";
// Build right-to-left
const parts = [];
// Start separator: default bg -> first segment bg
const first = segments[0];
parts.push(
ansi(first.bg, undefined, false) +
SEP +
ansi(first.fg, first.bg, first.bold) +
` ${first.text} `,
);
for (let i = 1; i < segments.length; i++) {
const seg = segments[i];
const prev = segments[i - 1];
parts.push(
ansi(prev.bg, seg.bg, false) +
SEP +
ansi(seg.fg, seg.bg, seg.bold) +
` ${seg.text} `,
);
}
const line = parts.join("") + RESET;
const vis = visibleWidth(line);
if (vis > width && segments.length > 1) {
const trimmed = segments.slice(1);
return renderPowerlineRight(trimmed, width, theme);
}
if (vis > width) return truncateToWidth(line, width, "");
if (vis < width) {
return " ".repeat(width - vis) + line + RESET;
}
return line;
}

View file

@ -1,243 +0,0 @@
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import {
Key,
matchesKey,
truncateToWidth,
visibleWidth,
} from "@singularity-forge/tui";
const LIMIT = 20;
const SCAN_LINE_LIMIT = 2000;
function promptHistoryPath() {
return join(homedir(), ".sf", "agent", "prompt-history.jsonl");
}
function isEnvTruthy(value) {
return ["1", "true", "TRUE", "yes", "YES"].includes(String(value ?? ""));
}
function parseEntryLine(line) {
try {
const text = line.trim();
if (!text) return [];
const entry = JSON.parse(text);
if (
!entry ||
typeof entry !== "object" ||
entry.version !== 1 ||
typeof entry.prompt !== "string" ||
entry.prompt.trim().length === 0 ||
typeof entry.projectRoot !== "string" ||
entry.projectRoot.trim().length === 0
) {
return [];
}
return [entry];
} catch {
return [];
}
}
function readEntries() {
try {
const path = promptHistoryPath();
if (!existsSync(path)) return [];
return readFileSync(path, "utf-8")
.split(/\r?\n/)
.reverse()
.slice(0, SCAN_LINE_LIMIT)
.flatMap(parseEntryLine);
} catch {
return [];
}
}
function normalizeHistory(history) {
const seen = new Set();
const merged = [];
for (const item of history) {
const text = String(item ?? "").trim();
if (!text || seen.has(text)) continue;
seen.add(text);
merged.push(text);
if (merged.length >= LIMIT) break;
}
return merged;
}
function appendEntries(entries) {
try {
const path = promptHistoryPath();
mkdirSync(dirname(path), { recursive: true });
appendFileSync(
path,
entries.map((entry) => JSON.stringify(entry)).join("\n") + "\n",
{ encoding: "utf-8", mode: 0o600 },
);
} catch {
/* non-fatal */
}
}
export function readPromptHistory(basePath) {
if (!basePath) return [];
return normalizeHistory(
readEntries()
.filter((entry) => entry.projectRoot === basePath)
.map((entry) => entry.prompt),
);
}
export function appendPromptHistory(prompt, basePath, sessionId) {
if (isEnvTruthy(process.env.SF_SKIP_PROMPT_HISTORY)) return;
if (!basePath) return;
const normalized = normalizeHistory([prompt]);
if (!normalized.length) return;
const now = Date.now();
const entries = normalized.toReversed().map((prompt, index) => ({
version: 1,
prompt,
projectRoot: basePath,
sessionId: sessionId ?? null,
timestamp: now - (normalized.length - index - 1),
}));
appendEntries(entries);
}
export function pushPromptHistory(history, text) {
const t = text.trim();
if (!t || history[0] === t) return;
history.unshift(t);
if (history.length > LIMIT) {
history.length = LIMIT;
}
}
function preview(text, maxWidth) {
const c = text.replace(/\s+/g, " ").trim();
return c ? truncateToWidth(c, maxWidth, "…") : "(empty)";
}
class PromptHistoryOverlay {
tui;
theme;
done;
items;
sel = 0;
cacheW = 0;
cacheL = [];
constructor(tui, theme, items, done) {
this.tui = tui;
this.theme = theme;
this.items = items;
this.done = done;
}
handleInput(data) {
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
this.done(null);
return;
}
if (matchesKey(data, Key.return) || matchesKey(data, Key.enter)) {
this.done(this.items[this.sel] ?? null);
return;
}
if (matchesKey(data, Key.down) || data === "j") {
this.sel = Math.min(this.items.length - 1, this.sel + 1);
this.invalidate();
this.tui.requestRender();
return;
}
if (matchesKey(data, Key.up) || data === "k") {
this.sel = Math.max(0, this.sel - 1);
this.invalidate();
this.tui.requestRender();
return;
}
if (data >= "1" && data <= "9") {
const idx = parseInt(data, 10) - 1;
if (idx >= 0 && idx < this.items.length) {
this.done(this.items[idx] ?? null);
}
}
}
invalidate() {
this.cacheW = 0;
}
render(width) {
if (this.cacheW === width) return this.cacheL;
const th = this.theme;
const bw = Math.min(84, width - 4);
const iw = bw - 4;
const pad = (s) => s + " ".repeat(Math.max(0, width - visibleWidth(s)));
const box = (s) => {
const len = visibleWidth(s);
return (
th.fg("dim", "│ ") +
s +
" ".repeat(Math.max(0, bw - 2 - len)) +
th.fg("dim", " │")
);
};
const lines = [];
lines.push(pad(th.fg("dim", "╭" + "─".repeat(bw) + "╮")));
lines.push(pad(box(th.bold(th.fg("accent", "📜 Prompt History")))));
lines.push(pad(th.fg("dim", "├" + "─".repeat(bw) + "┤")));
lines.push(
pad(
box(
th.fg(
"dim",
"↑/jk navigate • 1-9 quick pick • Enter insert • Esc cancel",
),
),
),
);
lines.push(pad(box("")));
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
const p = preview(item, iw - 8);
const ptr = i === this.sel ? th.fg("accent", " ") : " ";
const num = i < 9 ? th.fg("dim", `${i + 1}`) : " ";
const label = i === this.sel ? th.fg("accent", p) : p;
lines.push(pad(box(`${ptr}${num}. ${label}`)));
}
lines.push(pad(box("")));
lines.push(pad(th.fg("dim", "├" + "─".repeat(bw) + "┤")));
lines.push(pad(box(th.fg("dim", `${this.items.length} prompts`))));
lines.push(pad(th.fg("dim", "╰" + "─".repeat(bw) + "╯")));
lines.push("");
this.cacheL = lines;
this.cacheW = width;
return lines;
}
}
export async function openPromptHistoryOverlay(ctx, basePath) {
if (!ctx.hasUI) {
ctx.ui.notify("Prompt history requires interactive mode", "error");
return;
}
const items = readPromptHistory(basePath ?? undefined);
if (!items.length) {
ctx.ui.notify(
"No prompt history yet. Send a message to build history.",
"info",
);
return;
}
const selected = await ctx.ui.custom(
(tui, theme, _kb, done) => {
const o = new PromptHistoryOverlay(tui, theme, items, done);
return {
render: (w) => o.render(w),
invalidate: () => o.invalidate(),
handleInput: (d) => o.handleInput(d),
};
},
{
overlay: true,
overlayOptions: {
width: "90%",
minWidth: 60,
maxHeight: "85%",
anchor: "center",
backdrop: true,
},
},
);
if (selected) {
ctx.ui.setEditorText(selected);
ctx.ui.notify("Inserted prompt from history", "info");
}
}

View file

@ -1,7 +0,0 @@
import { visibleWidth } from "@singularity-forge/tui";
export function rightAlign(left, right, width) {
const leftVis = visibleWidth(left);
const rightVis = visibleWidth(right);
const gap = Math.max(1, width - leftVis - rightVis);
return left + " ".repeat(gap) + right;
}

View file

@ -1,11 +0,0 @@
{
"id": "sf-usage-bar",
"name": "SF Usage Bar",
"version": "1.0.0",
"description": "Shows configured AI provider usage windows and service status",
"tier": "bundled",
"requires": { "platform": ">=2.29.0" },
"provides": {
"commands": ["usage"]
}
}

View file

@ -1,4 +1,4 @@
import { showInterviewRound } from "../shared/tui.js";
import { showInterviewRound } from "../../../shared/tui.js";
export default function createExtension(pi) {
pi.registerCommand("create-extension", {
description:

View file

@ -1,4 +1,4 @@
import { showInterviewRound } from "../shared/tui.js";
import { showInterviewRound } from "../../../shared/tui.js";
export default function createSlashCommand(pi) {
pi.registerCommand("create-slash-command", {
description:

View file

@ -1,10 +1,8 @@
import auditCommand from "./audit.js";
import clearCommand from "./clear.js";
import createExtension from "./create-extension.js";
import createSlashCommand from "./create-slash-command.js";
export default function slashCommands(pi) {
createSlashCommand(pi);
createExtension(pi);
auditCommand(pi);
clearCommand(pi);
}

View file

@ -59,10 +59,12 @@
"add-tests",
"agent",
"ask",
"audit",
"autonomous",
"backlog",
"capture",
"chronicle",
"clear",
"cleanup",
"cmux",
"codebase",
@ -75,6 +77,8 @@
"configure-agent",
"control",
"cost",
"create-extension",
"create-slash-command",
"debug",
"delegate",
"diff",
@ -94,6 +98,8 @@
"fast",
"find",
"forensics",
"guard-status",
"guard-toggle",
"harness",
"help",
"history",
@ -114,9 +120,17 @@
"new-milestone",
"next",
"notifications",
"notify-beep",
"notify-focus",
"notify-save-global",
"notify-say",
"notify-status",
"notify-threshold",
"parallel",
"park",
"pause",
"permission",
"permission-mode",
"permission-profile",
"plan",
"pr-branch",
@ -164,6 +178,7 @@
"unpark",
"uok",
"update",
"usage",
"visualize",
"widget",
"workflow",
@ -186,6 +201,7 @@
"tool_execution_end",
"tool_execution_start",
"tool_result",
"turn_start",
"turn_end"
],
"shortcuts": [

View file

@ -67,4 +67,64 @@ export default async function registerExtension(pi) {
`SF TUI setup failed — running with default header/footer: ${err instanceof Error ? err.message : String(err)}`,
);
}
// Register SF usage bar (/usage command — provider usage windows + status).
try {
const { default: registerSFUsageBar } = await import("./ui/usage-bar.js");
registerSFUsageBar(pi);
} catch (err) {
const { logWarning } = await import("./workflow-logger.js");
logWarning(
"usage-bar",
`SF usage bar setup failed: ${err instanceof Error ? err.message : String(err)}`,
);
}
// Register SF notifications (completion beep/say/focus/threshold commands).
try {
const { default: registerSFNotify } = await import("./notifications/notify.js");
registerSFNotify(pi);
} catch (err) {
const { logWarning } = await import("./workflow-logger.js");
logWarning(
"notifications",
`SF notifications setup failed: ${err instanceof Error ? err.message : String(err)}`,
);
}
// Register SF in-turn guard (duplicate tool-call loop detection).
try {
const { default: registerSFInturnGuard } = await import("./guards/inturn.js");
registerSFInturnGuard(pi);
} catch (err) {
const { logWarning } = await import("./workflow-logger.js");
logWarning(
"inturn-guard",
`SF in-turn guard setup failed: ${err instanceof Error ? err.message : String(err)}`,
);
}
// Register SF permissions (layered permission enforcement).
try {
const { default: registerSFPermissions } = await import("./permissions/index.js");
registerSFPermissions(pi);
} catch (err) {
const { logWarning } = await import("./workflow-logger.js");
logWarning(
"permissions",
`SF permissions setup failed: ${err instanceof Error ? err.message : String(err)}`,
);
}
// Register SF legacy slash commands (/audit, /clear, /create-extension, /create-slash-command).
try {
const { default: registerSFLegacyCommands } = await import("./commands/legacy/index.js");
registerSFLegacyCommands(pi);
} catch (err) {
const { logWarning } = await import("./workflow-logger.js");
logWarning(
"legacy-commands",
`SF legacy commands setup failed: ${err instanceof Error ? err.message : String(err)}`,
);
}
}

View file

@ -20,7 +20,7 @@ import {
replaceMessageTemplates,
SAY_MESSAGES,
speakMessage,
} from "../shared/notify.js";
} from "../../shared/notify.js";
const DEFAULT_CONFIG = {
thresholdMs: 2000,

View file

@ -0,0 +1,286 @@
/**
* Integration tests for the full remote steering pipeline:
* parse apply format (steer command mode change result display)
*
* Purpose: verify that a remote answer containing steering directives flows
* correctly through all three stages and that the session mode actually reflects
* the requested change.
*
* Consumer: CI gate; regression guard for remote-channel mode steering.
*/
import assert from "node:assert";
import { beforeEach, describe, test, vi } from "vitest";
// ── Mock auto/session before importing the module under test ──────────────────
const _modeState = {
workMode: "ask",
runControl: "manual",
permissionProfile: "normal",
modelMode: "auto",
};
const mockSession = {
setMode(updates) {
Object.assign(_modeState, updates);
},
getMode() {
return { ..._modeState };
},
};
vi.mock("../auto/session.js", () => ({
getAutoSession: () => mockSession,
}));
vi.mock("../workflow-logger.js", () => ({
logWarning: vi.fn(),
}));
// Import AFTER mocks are registered so module sees the mock.
const {
parseRemoteSteeringDirectives,
applyRemoteSteeringDirectives,
formatRemoteSteeringResults,
} = await import("../remote-steering.js");
// ── Helpers ───────────────────────────────────────────────────────────────────
/** Reset mock mode state before each test. */
function resetMode() {
Object.assign(_modeState, {
workMode: "ask",
runControl: "manual",
permissionProfile: "normal",
modelMode: "auto",
});
}
/** Build a unique source key to avoid throttle carry-over between tests. */
let _srcCounter = 0;
function uniqueSrc() {
return `test-pipeline-${_srcCounter++}`;
}
// ─────────────────────────────────────────────────────────────────────────────
// Parse stage
// ─────────────────────────────────────────────────────────────────────────────
describe("parse stage", () => {
test("parseRemoteSteeringDirectives_when_text_has_mode_directive_returns_steering_true", () => {
const result = parseRemoteSteeringDirectives({
text: "/mode build",
});
assert.strictEqual(result.steering, true);
assert.strictEqual(result.directives.length, 1);
assert.deepStrictEqual(result.directives[0], { cmd: "mode", value: "build" });
});
test("parseRemoteSteeringDirectives_when_text_has_all_axes_returns_all_four_directives", () => {
const result = parseRemoteSteeringDirectives({
text: "/mode build /control autonomous /permission-profile trusted /model-mode deep",
});
assert.strictEqual(result.steering, true);
assert.strictEqual(result.directives.length, 4);
const cmds = result.directives.map((d) => d.cmd);
assert.ok(cmds.includes("mode"));
assert.ok(cmds.includes("control"));
assert.ok(cmds.includes("permission-profile"));
assert.ok(cmds.includes("model-mode"));
});
test("parseRemoteSteeringDirectives_when_answers_array_contains_directive_returns_steering_true", () => {
const result = parseRemoteSteeringDirectives({
answers: ["/mode review", "some other text"],
});
assert.strictEqual(result.steering, true);
assert.strictEqual(result.directives[0].value, "review");
});
test("parseRemoteSteeringDirectives_when_value_invalid_ignores_directive", () => {
const result = parseRemoteSteeringDirectives({
text: "/mode notavalidmode",
});
assert.strictEqual(result.steering, false);
assert.strictEqual(result.directives.length, 0);
});
test("parseRemoteSteeringDirectives_when_null_input_returns_no_steering", () => {
const result = parseRemoteSteeringDirectives(null);
assert.strictEqual(result.steering, false);
assert.deepStrictEqual(result.directives, []);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Apply stage
// ─────────────────────────────────────────────────────────────────────────────
describe("apply stage", () => {
beforeEach(resetMode);
test("applyRemoteSteeringDirectives_when_mode_directive_applies_changes_session_workMode", () => {
const src = uniqueSrc();
const results = applyRemoteSteeringDirectives(
[{ cmd: "mode", value: "build" }],
src,
);
assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].applied, true);
assert.strictEqual(_modeState.workMode, "build");
});
test("applyRemoteSteeringDirectives_when_control_directive_applies_changes_runControl", () => {
const src = uniqueSrc();
applyRemoteSteeringDirectives([{ cmd: "control", value: "autonomous" }], src);
assert.strictEqual(_modeState.runControl, "autonomous");
});
test("applyRemoteSteeringDirectives_when_permission_profile_directive_applies", () => {
const src = uniqueSrc();
applyRemoteSteeringDirectives(
[{ cmd: "permission-profile", value: "trusted" }],
src,
);
assert.strictEqual(_modeState.permissionProfile, "trusted");
});
test("applyRemoteSteeringDirectives_when_model_mode_directive_applies", () => {
const src = uniqueSrc();
applyRemoteSteeringDirectives([{ cmd: "model-mode", value: "deep" }], src);
assert.strictEqual(_modeState.modelMode, "deep");
});
test("applyRemoteSteeringDirectives_when_unknown_cmd_returns_applied_false", () => {
const src = uniqueSrc();
const results = applyRemoteSteeringDirectives(
[{ cmd: "notacommand", value: "whatever" }],
src,
);
assert.strictEqual(results[0].applied, false);
assert.ok(results[0].error);
});
test("applyRemoteSteeringDirectives_when_same_source_called_again_immediately_throttles", () => {
// First call should succeed
const src = uniqueSrc();
const first = applyRemoteSteeringDirectives(
[{ cmd: "mode", value: "build" }],
src,
);
assert.strictEqual(first[0].applied, true);
// Second immediate call with the same source must be throttled
const second = applyRemoteSteeringDirectives(
[{ cmd: "mode", value: "review" }],
src,
);
assert.strictEqual(second[0].applied, false);
assert.ok(second[0].error.toLowerCase().includes("throttled"));
});
test("applyRemoteSteeringDirectives_when_different_sources_both_applied", () => {
const first = applyRemoteSteeringDirectives(
[{ cmd: "mode", value: "build" }],
uniqueSrc(),
);
const second = applyRemoteSteeringDirectives(
[{ cmd: "model-mode", value: "deep" }],
uniqueSrc(),
);
assert.strictEqual(first[0].applied, true);
assert.strictEqual(second[0].applied, true);
});
test("applyRemoteSteeringDirectives_when_empty_directives_returns_empty", () => {
const results = applyRemoteSteeringDirectives([], uniqueSrc());
assert.deepStrictEqual(results, []);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Format stage
// ─────────────────────────────────────────────────────────────────────────────
describe("format stage", () => {
test("formatRemoteSteeringResults_when_all_applied_renders_ok_markers", () => {
const results = [
{ cmd: "mode", value: "build", applied: true },
{ cmd: "control", value: "autonomous", applied: true },
];
const text = formatRemoteSteeringResults(results);
assert.ok(text.includes("[ok] /mode build"), `missing ok mode: ${text}`);
assert.ok(text.includes("[ok] /control autonomous"), `missing ok control: ${text}`);
});
test("formatRemoteSteeringResults_when_blocked_renders_blocked_marker_and_error", () => {
const results = [
{ cmd: "mode", value: "review", applied: false, error: "Throttled" },
];
const text = formatRemoteSteeringResults(results);
assert.ok(text.includes("[blocked] /mode review"), `missing blocked: ${text}`);
assert.ok(text.includes("Throttled"), `missing error text: ${text}`);
});
test("formatRemoteSteeringResults_includes_current_mode_summary", () => {
const results = [{ cmd: "mode", value: "build", applied: true }];
const text = formatRemoteSteeringResults(results);
assert.ok(text.includes("Current:"), `missing Current: line: ${text}`);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Full pipeline integration
// ─────────────────────────────────────────────────────────────────────────────
describe("full pipeline: parse → apply → format", () => {
beforeEach(resetMode);
test("pipeline_when_mode_build_directive_ends_with_build_in_current_line", () => {
const src = uniqueSrc();
// Step 1: parse
const parsed = parseRemoteSteeringDirectives({ text: "/mode build" });
assert.strictEqual(parsed.steering, true);
// Step 2: apply
const applied = applyRemoteSteeringDirectives(parsed.directives, src);
assert.ok(applied.every((r) => r.applied));
// Step 3: format
const formatted = formatRemoteSteeringResults(applied);
assert.ok(formatted.includes("build"), `expected 'build' in: ${formatted}`);
assert.ok(formatted.includes("[ok]"), `expected [ok] in: ${formatted}`);
});
test("pipeline_when_all_axes_set_mode_state_reflects_all_changes", () => {
const src = uniqueSrc();
const parsed = parseRemoteSteeringDirectives({
text: "/mode build /control autonomous /permission-profile trusted /model-mode deep",
});
assert.strictEqual(parsed.directives.length, 4);
const applied = applyRemoteSteeringDirectives(parsed.directives, src);
assert.ok(applied.every((r) => r.applied), `Some failed: ${JSON.stringify(applied)}`);
assert.strictEqual(_modeState.workMode, "build");
assert.strictEqual(_modeState.runControl, "autonomous");
assert.strictEqual(_modeState.permissionProfile, "trusted");
assert.strictEqual(_modeState.modelMode, "deep");
});
test("pipeline_when_throttled_apply_returns_blocked_and_format_shows_blocked", () => {
const src = uniqueSrc();
// First steering succeeds
const parsed = parseRemoteSteeringDirectives({ text: "/mode build" });
applyRemoteSteeringDirectives(parsed.directives, src);
// Same source, immediate second steer
const parsed2 = parseRemoteSteeringDirectives({ text: "/mode review" });
const applied2 = applyRemoteSteeringDirectives(parsed2.directives, src);
const formatted = formatRemoteSteeringResults(applied2);
assert.strictEqual(applied2[0].applied, false);
assert.ok(formatted.includes("[blocked]"), `expected blocked: ${formatted}`);
// Mode should not have changed
assert.strictEqual(_modeState.workMode, "build");
});
});

View file

@ -1,217 +0,0 @@
import { randomUUID } from "node:crypto";
import { debugLog } from "../debug-logger.js";
import {
defaultPermissionProfileForRunControl,
resolvePermissionProfile,
resolveRunControlMode,
runControlModeForSession,
} from "../operating-model.js";
import {
isDbAvailable,
recordUokRunExit,
recordUokRunStart,
} from "../sf-db.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
import { setAuditEnvelopeEnabled } from "./audit-toggle.js";
import { writeUokDiagnostics } from "./diagnostic-synthesis.js";
import { resolveUokFlags } from "./flags.js";
import { createTurnObserver } from "./loop-adapter.js";
import {
checkAndDrainMissingExit,
resetParityCommitBlock,
signalKernelEnter,
} from "./parity-diff-capture.js";
import {
hasCurrentParityWarning,
writeParityHeartbeat,
writeParityReport,
} from "./parity-report.js";
function refreshParityReport(basePath) {
try {
return writeParityReport(basePath);
} catch (err) {
debugLog("uok-parity-report-write-failed", {
error: err instanceof Error ? err.message : String(err),
});
return null;
}
}
function resolveKernelPathLabel() {
return "uok-kernel";
}
/**
* Records an abnormal UOK kernel termination in both durable stores.
*
* Purpose: keep the DB-backed UOK run ledger and JSONL parity heartbeat
* symmetrical when autonomous mode exits via signal and bypasses the async kernel
* finally block.
*
* Consumer: auto signal cleanup and UOK parity tests.
*/
export function recordUokKernelTermination({
basePath,
runId,
sessionId,
flags,
runControl,
permissionProfile,
status = "signal",
error,
}) {
const endedAt = new Date().toISOString();
const lifecycleFlags = {
...(flags ?? {}),
...(runControl ? { runControl } : {}),
...(permissionProfile ? { permissionProfile } : {}),
};
if (runId && isDbAvailable()) {
recordUokRunExit({
runId,
sessionId,
path: resolveKernelPathLabel(),
flags: lifecycleFlags,
status,
endedAt,
...(error ? { error } : {}),
});
}
writeParityHeartbeat(basePath, {
ts: endedAt,
...(runId ? { runId } : {}),
sessionId,
path: resolveKernelPathLabel(),
flags: lifecycleFlags,
...(runControl ? { runControl } : {}),
...(permissionProfile ? { permissionProfile } : {}),
phase: "exit",
status,
...(error ? { error } : {}),
});
const report = refreshParityReport(basePath);
try {
writeUokDiagnostics(basePath);
} catch (err) {
debugLog("uok-diagnostics-write-failed", {
error: err instanceof Error ? err.message : String(err),
});
}
return report;
}
export async function runAutoLoopWithUok(args) {
const { ctx, pi, s, deps, runKernelLoop } = args;
const prefs = deps.loadEffectiveSFPreferences()?.preferences;
const flags = { ...resolveUokFlags(prefs), enabled: true };
const runControl = resolveRunControlMode(
args.runControl ?? runControlModeForSession(s),
);
const permissionProfile = resolvePermissionProfile(
args.permissionProfile ??
prefs?.uok?.permission_profile ??
defaultPermissionProfileForRunControl(runControl),
);
// Include workMode and modelMode from session in lifecycle flags
const workMode = s.workMode ?? "chat";
const modelMode = s.modelMode ?? "smart";
const lifecycleFlags = {
...flags,
runControl,
permissionProfile,
workMode,
modelMode,
};
const healthVerdict = writeUokDiagnostics(s.basePath);
debugLog("uok-system-health-verdict", healthVerdict);
const previousReport = refreshParityReport(s.basePath);
const runId = `uok-${randomUUID()}`;
s.currentUokRunId = runId;
resetParityCommitBlock();
if (
previousReport &&
previousReport.missingExitEvents > 0 &&
hasCurrentParityWarning(previousReport)
) {
checkAndDrainMissingExit(
previousReport.enterEvents,
previousReport.exitEvents,
);
}
setAuditEnvelopeEnabled(flags.auditEnvelope);
signalKernelEnter();
const startedAt = new Date().toISOString();
if (isDbAvailable()) {
recordUokRunStart({
runId,
sessionId: ctx.sessionManager?.getSessionId?.(),
path: resolveKernelPathLabel(),
flags: lifecycleFlags,
startedAt,
});
}
writeParityHeartbeat(s.basePath, {
ts: startedAt,
runId,
sessionId: ctx.sessionManager?.getSessionId?.(),
path: resolveKernelPathLabel(),
flags: lifecycleFlags,
runControl,
permissionProfile,
phase: "enter",
});
if (flags.auditEnvelope) {
emitUokAuditEvent(
s.basePath,
buildAuditEnvelope({
traceId: `session:${String(s.autoStartTime || Date.now())}`,
category: "orchestration",
type: "uok-kernel-enter",
payload: {
flags: lifecycleFlags,
runControl,
permissionProfile,
workMode,
modelMode,
sessionId: ctx.sessionManager?.getSessionId?.(),
},
}),
);
}
const decoratedDeps = {
...deps,
uokObserver: createTurnObserver({
basePath: s.basePath,
gitAction: flags.gitopsTurnAction,
gitPush: flags.gitopsTurnPush,
enableAudit: flags.auditEnvelope,
enableGitops: flags.gitops,
enableChaosMonkey: flags.chaosMonkey,
runControl,
permissionProfile,
}),
uokRunControl: runControl,
uokPermissionProfile: permissionProfile,
};
let status = "ok";
let error;
try {
await runKernelLoop(ctx, pi, s, decoratedDeps);
} catch (err) {
status = "error";
error = err instanceof Error ? err.message : String(err);
throw err;
} finally {
recordUokKernelTermination({
basePath: s.basePath,
runId,
sessionId: ctx.sessionManager?.getSessionId?.(),
flags: lifecycleFlags,
runControl,
permissionProfile,
status,
...(error ? { error } : {}),
});
if (s.currentUokRunId === runId) s.currentUokRunId = undefined;
}
}

View file

@ -0,0 +1,321 @@
/**
* uok-kernel.ts Unit-of-Work kernel lifecycle management.
*
* Purpose: wraps the autonomous loop with UOK entry/exit instrumentation
* DB run ledger, parity heartbeat, audit envelope, and diagnostic snapshot
* so every run is durable, observable, and crash-recoverable.
*
* Consumer: auto/dispatch.ts and signal handlers (SIGINT/SIGTERM) in the
* autonomous mode boot path.
*/
import { randomUUID } from "node:crypto";
import { debugLog } from "../debug-logger.js";
import {
defaultPermissionProfileForRunControl,
resolvePermissionProfile,
resolveRunControlMode,
runControlModeForSession,
} from "../operating-model.js";
import {
isDbAvailable,
recordUokRunExit,
recordUokRunStart,
} from "../sf-db.js";
import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
import { setAuditEnvelopeEnabled } from "./audit-toggle.js";
import { writeUokDiagnostics } from "./diagnostic-synthesis.js";
import { resolveUokFlags } from "./flags.js";
import { createTurnObserver } from "./loop-adapter.js";
import {
checkAndDrainMissingExit,
resetParityCommitBlock,
signalKernelEnter,
} from "./parity-diff-capture.js";
import {
hasCurrentParityWarning,
writeParityHeartbeat,
writeParityReport,
} from "./parity-report.js";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** Flags resolved from user preferences for this UOK run. */
interface UokFlags {
auditEnvelope?: boolean;
gitops?: boolean;
gitopsTurnAction?: string;
gitopsTurnPush?: boolean;
chaosMonkey?: boolean;
enabled?: boolean;
[key: string]: unknown;
}
/** Lifecycle flags written to the DB run ledger and parity heartbeat. */
interface LifecycleFlags extends UokFlags {
runControl?: string;
permissionProfile?: string;
workMode?: string;
modelMode?: string;
}
export interface UokKernelTerminationArgs {
basePath: string;
runId?: string;
sessionId?: string | null;
flags?: UokFlags;
runControl?: string;
permissionProfile?: string;
/** Exit status — "ok" | "error" | "signal". Defaults to "signal". */
status?: string;
error?: string;
}
/** Arguments for {@link runAutoLoopWithUok}. */
export interface RunAutoLoopWithUokArgs {
/** Coding-agent context (opaque; provides sessionManager). */
ctx: {
sessionManager?: {
getSessionId?: () => string | undefined;
};
[key: string]: unknown;
};
/** Pi/provider interface (opaque). */
pi: unknown;
/** Mutable session state. */
s: {
basePath: string;
workMode?: string;
modelMode?: string;
autoStartTime?: number;
currentUokRunId?: string;
[key: string]: unknown;
};
/** Injected dependency bag. */
deps: {
loadEffectiveSFPreferences?: () => { preferences?: Record<string, unknown> } | null;
[key: string]: unknown;
};
runControl?: string;
permissionProfile?: string;
/** The inner autonomous loop to execute inside the UOK wrapper. */
runKernelLoop: (
ctx: RunAutoLoopWithUokArgs["ctx"],
pi: unknown,
s: RunAutoLoopWithUokArgs["s"],
deps: Record<string, unknown>,
) => Promise<void>;
}
// ---------------------------------------------------------------------------
// Internals
// ---------------------------------------------------------------------------
function refreshParityReport(basePath: string): ReturnType<typeof writeParityReport> | null {
try {
return writeParityReport(basePath);
} catch (err) {
debugLog("uok-parity-report-write-failed", {
error: err instanceof Error ? err.message : String(err),
});
return null;
}
}
function resolveKernelPathLabel(): string {
return "uok-kernel";
}
// ---------------------------------------------------------------------------
// Exports
// ---------------------------------------------------------------------------
/**
* Records an abnormal UOK kernel termination in both durable stores.
*
* Purpose: keep the DB-backed UOK run ledger and JSONL parity heartbeat
* symmetrical when autonomous mode exits via signal and bypasses the async kernel
* finally block.
*
* Consumer: auto signal cleanup and UOK parity tests.
*/
export function recordUokKernelTermination({
basePath,
runId,
sessionId,
flags,
runControl,
permissionProfile,
status = "signal",
error,
}: UokKernelTerminationArgs): ReturnType<typeof writeParityReport> | null {
const endedAt = new Date().toISOString();
const lifecycleFlags: LifecycleFlags = {
...(flags ?? {}),
...(runControl ? { runControl } : {}),
...(permissionProfile ? { permissionProfile } : {}),
};
if (runId && isDbAvailable()) {
recordUokRunExit({
runId,
sessionId,
path: resolveKernelPathLabel(),
flags: lifecycleFlags,
status,
endedAt,
...(error ? { error } : {}),
});
}
writeParityHeartbeat(basePath, {
ts: endedAt,
...(runId ? { runId } : {}),
sessionId,
path: resolveKernelPathLabel(),
flags: lifecycleFlags,
...(runControl ? { runControl } : {}),
...(permissionProfile ? { permissionProfile } : {}),
phase: "exit",
status,
...(error ? { error } : {}),
});
const report = refreshParityReport(basePath);
try {
writeUokDiagnostics(basePath);
} catch (err) {
debugLog("uok-diagnostics-write-failed", {
error: err instanceof Error ? err.message : String(err),
});
}
return report;
}
/**
* Runs the autonomous loop inside the UOK lifecycle wrapper.
*
* Purpose: instrument every autonomous run with entry/exit DB records, parity
* heartbeats, audit events, and a crash-recovery diagnostic snapshot so that
* incomplete runs can be detected and drained on the next startup.
*
* Consumer: auto/dispatch.ts called once per autonomous session activation.
*/
export async function runAutoLoopWithUok(args: RunAutoLoopWithUokArgs): Promise<void> {
const { ctx, pi, s, deps, runKernelLoop } = args;
const prefs = deps.loadEffectiveSFPreferences?.()?.preferences as Record<string, unknown> | undefined;
const flags: UokFlags = { ...resolveUokFlags(prefs), enabled: true };
const runControl: string = resolveRunControlMode(
args.runControl ?? runControlModeForSession(s),
);
const permissionProfile: string = resolvePermissionProfile(
args.permissionProfile ??
(prefs?.uok as Record<string, unknown> | undefined)?.permission_profile ??
defaultPermissionProfileForRunControl(runControl),
);
// Include workMode and modelMode from session in lifecycle flags
const workMode: string = (s.workMode as string | undefined) ?? "chat";
const modelMode: string = (s.modelMode as string | undefined) ?? "smart";
const lifecycleFlags: LifecycleFlags = {
...flags,
runControl,
permissionProfile,
workMode,
modelMode,
};
const healthVerdict = writeUokDiagnostics(s.basePath);
debugLog("uok-system-health-verdict", healthVerdict);
const previousReport = refreshParityReport(s.basePath);
const runId = `uok-${randomUUID()}`;
s.currentUokRunId = runId;
resetParityCommitBlock();
if (
previousReport &&
(previousReport as { missingExitEvents?: number }).missingExitEvents != null &&
(previousReport as { missingExitEvents: number }).missingExitEvents > 0 &&
hasCurrentParityWarning(previousReport)
) {
checkAndDrainMissingExit(
(previousReport as { enterEvents: number }).enterEvents,
(previousReport as { exitEvents: number }).exitEvents,
);
}
setAuditEnvelopeEnabled(flags.auditEnvelope ?? false);
signalKernelEnter();
const startedAt = new Date().toISOString();
const sessionId = ctx.sessionManager?.getSessionId?.();
if (isDbAvailable()) {
recordUokRunStart({
runId,
sessionId,
path: resolveKernelPathLabel(),
flags: lifecycleFlags,
startedAt,
});
}
writeParityHeartbeat(s.basePath, {
ts: startedAt,
runId,
sessionId,
path: resolveKernelPathLabel(),
flags: lifecycleFlags,
runControl,
permissionProfile,
phase: "enter",
});
if (flags.auditEnvelope) {
emitUokAuditEvent(
s.basePath,
buildAuditEnvelope({
traceId: `session:${String(s.autoStartTime || Date.now())}`,
category: "orchestration",
type: "uok-kernel-enter",
payload: {
flags: lifecycleFlags,
runControl,
permissionProfile,
workMode,
modelMode,
sessionId,
},
}),
);
}
const decoratedDeps = {
...deps,
uokObserver: createTurnObserver({
basePath: s.basePath,
gitAction: flags.gitopsTurnAction,
gitPush: flags.gitopsTurnPush,
enableAudit: flags.auditEnvelope,
enableGitops: flags.gitops,
enableChaosMonkey: flags.chaosMonkey,
runControl,
permissionProfile,
}),
uokRunControl: runControl,
uokPermissionProfile: permissionProfile,
};
let status = "ok";
let error: string | undefined;
try {
await runKernelLoop(ctx, pi, s, decoratedDeps);
} catch (err) {
status = "error";
error = err instanceof Error ? err.message : String(err);
throw err;
} finally {
recordUokKernelTermination({
basePath: s.basePath,
runId,
sessionId,
flags: lifecycleFlags,
runControl,
permissionProfile,
status,
...(error ? { error } : {}),
});
if (s.currentUokRunId === runId) s.currentUokRunId = undefined;
}
}

View file

@ -1,8 +0,0 @@
export default function clearCommand(pi) {
pi.registerCommand("clear", {
description: "Alias for /new — start a new session",
async handler(_args, ctx) {
await ctx.newSession();
},
});
}

View file

@ -1,11 +0,0 @@
{
"id": "slash-commands",
"name": "Slash Commands",
"version": "1.0.0",
"description": "Boilerplate generators for slash commands, extensions, and audit tools",
"tier": "bundled",
"requires": { "platform": ">=2.29.0" },
"provides": {
"commands": ["create-slash-command", "create-extension", "audit", "clear"]
}
}

View file

@ -1,12 +0,0 @@
{
"id": "vectordrive",
"name": "VectorDrive",
"version": "1.0.0",
"description": "Native vector database integration via vectordrive (Rust-based, in-process)",
"tier": "bundled",
"requires": { "platform": ">=2.71.0" },
"provides": {
"tools": ["vectordrive_info", "vectordrive_store", "vectordrive_search"],
"hooks": ["session_start", "session_shutdown"]
}
}

View file

@ -1,24 +0,0 @@
/**
* VectorDrive Extension for Singularity Forge
*
* Integrates the native Rust vectordrive vector database for semantic
* memory and code search. Works offline with no external services.
*/
import { VectordriveManager } from "./manager.js";
import { registerVectordriveInfoTool } from "./tool-info.js";
import { registerVectordriveSearchTool } from "./tool-search.js";
import { registerVectordriveStoreTool } from "./tool-store.js";
export default function (pi) {
registerVectordriveInfoTool(pi);
registerVectordriveStoreTool(pi);
registerVectordriveSearchTool(pi);
// Pre-warm the connection on session start
pi.on("session_start", async () => {
const manager = VectordriveManager.getInstance();
await manager.getStatus();
});
pi.on("session_shutdown", async () => {
const manager = VectordriveManager.getInstance();
await manager.close();
});
}

View file

@ -1,160 +0,0 @@
/**
* VectorDrive Manager Singleton wrapping the native vectordrive VectorDb.
*
* Loads the `vectordrive` npm package dynamically (optional dependency),
* creates a persisted VectorDb in `.sf/vectordrive/`, and exposes status
* and search/store operations with graceful degradation.
*/
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
const DB_DIR = ".sf/vectordrive";
const DB_PATH = `${DB_DIR}/forge.vectors`;
const DIMENSIONS = 384;
function getDbPath() {
const home = process.env.HOME || process.env.USERPROFILE || ".";
return `${home}/${DB_PATH}`;
}
function ensureDir(path) {
try {
mkdirSync(dirname(path), { recursive: true });
} catch {
// ignore
}
}
/** Simple text→vector fallback when no embedding model is available. */
export function textToVector(text, dimensions = DIMENSIONS) {
const vec = new Array(dimensions).fill(0);
const normalized = text.toLowerCase().trim();
for (let i = 0; i < normalized.length; i++) {
vec[i % dimensions] += normalized.charCodeAt(i) / 65535;
}
const mag = Math.sqrt(vec.reduce((s, v) => s + v * v, 0));
return mag > 0 ? vec.map((v) => v / mag) : vec;
}
export class VectordriveManager {
static instance;
status = null;
initPromise = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
db = null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vd = null;
static getInstance() {
if (!VectordriveManager.instance) {
VectordriveManager.instance = new VectordriveManager();
}
return VectordriveManager.instance;
}
async getStatus() {
if (this.status?.initialized) return this.status;
if (this.initPromise) return this.initPromise;
this.initPromise = this.probe();
return this.initPromise;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async getDb() {
const status = await this.getStatus();
if (status.backend !== "vectordrive") return null;
return this.db;
}
async probe() {
const dbPath = getDbPath();
let vectordrive = null;
try {
const modName = "vectordrive";
vectordrive = await import(modName);
} catch (err) {
this.status = {
backend: "none",
version: null,
implementation: null,
initialized: true,
vectorCount: 0,
error: `vectordrive package not installed: ${err instanceof Error ? err.message : String(err)}`,
dbPath: null,
};
return this.status;
}
try {
this.vd = vectordrive;
ensureDir(dbPath);
const VectorDb = vectordrive.VectorDb || vectordrive.VectorDB;
if (typeof VectorDb !== "function") {
throw new Error("vectordrive package does not export VectorDb");
}
this.db = new VectorDb({
dimensions: DIMENSIONS,
storagePath: dbPath,
distanceMetric: "cosine",
});
const count = await this.db.len();
const version = vectordrive.getVersion?.() ?? null;
const impl = vectordrive.getImplementationType?.() ?? "unknown";
this.status = {
backend: "vectordrive",
version: version?.version ?? null,
implementation: impl,
initialized: true,
vectorCount: count,
error: null,
dbPath,
};
return this.status;
} catch (err) {
this.status = {
backend: "none",
version: null,
implementation: null,
initialized: true,
vectorCount: 0,
error: err instanceof Error ? err.message : String(err),
dbPath: null,
};
return this.status;
}
}
async store(entry) {
const db = await this.getDb();
if (!db) return false;
try {
await db.insert({
id: entry.id,
vector: entry.vector,
metadata: entry.metadata,
});
return true;
} catch {
return false;
}
}
async search(vector, k) {
const db = await this.getDb();
if (!db) return [];
try {
const results = await db.search({ vector, k });
return results.map((r) => ({
id: String(r.id),
score: Number(r.score),
metadata: r.metadata,
}));
} catch {
return [];
}
}
async delete(id) {
const db = await this.getDb();
if (!db) return false;
try {
return await db.delete(id);
} catch {
return false;
}
}
async close() {
this.db = null;
this.vd = null;
this.status = null;
this.initPromise = null;
}
}

View file

@ -1,54 +0,0 @@
/**
* VectorDrive Info Tool
*
* Introspects the vectordrive native package status, version, implementation
* type (native vs wasm), and vector count.
*/
import { Type } from "@sinclair/typebox";
import { VectordriveManager } from "./manager.js";
export function registerVectordriveInfoTool(pi) {
pi.registerTool({
name: "vectordrive_info",
label: "VectorDrive Info",
description:
"Check VectorDrive native vector database status. " +
"Returns implementation type (native Rust or WASM), version, " +
"vector count, and database path.",
promptSnippet: "Check VectorDrive database status and capabilities",
parameters: Type.Object({
refresh: Type.Optional(
Type.Boolean({
default: false,
description: "Force re-probe instead of using cached status",
}),
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
const manager = VectordriveManager.getInstance();
if (params.refresh) {
await manager.close();
}
const status = await manager.getStatus();
const lines = [];
lines.push(`# VectorDrive Status`);
lines.push("");
lines.push(`- **Backend**: ${status.backend}`);
lines.push(`- **Implementation**: ${status.implementation ?? "n/a"}`);
lines.push(`- **Version**: ${status.version ?? "n/a"}`);
lines.push(`- **Vectors**: ${status.vectorCount}`);
lines.push(`- **Initialized**: ${status.initialized}`);
if (status.dbPath) {
lines.push(`- **DB Path**: ${status.dbPath}`);
}
if (status.error) {
lines.push(`- **Error**: ${status.error}`);
}
const text = lines.join("\n");
return {
content: [{ type: "text", text }],
details: { status },
isError: status.backend === "none",
};
},
});
}

View file

@ -1,105 +0,0 @@
/**
* VectorDrive Search Tool
*
* Semantic search over stored vectors. Accepts a pre-computed query vector
* or raw text (auto-embedded). Falls back to metadata keyword matching
* when vectordrive is offline.
*/
import { Type } from "@sinclair/typebox";
import { textToVector, VectordriveManager } from "./manager.js";
export function registerVectordriveSearchTool(pi) {
pi.registerTool({
name: "vectordrive_search",
label: "VectorDrive Search",
description:
"Search VectorDrive by vector similarity or text query. " +
"Returns the most relevant stored entries with similarity scores. " +
"When no embedding model is available, a simple hash embedding is used — " +
"for best results provide pre-computed vectors via vectordrive_store.",
promptSnippet: "Search VectorDrive memories or code chunks",
promptGuidelines: [
"Use vectordrive_search to find previously stored memories, code chunks, or documents.",
"Be specific with queries for better results.",
"If you stored code with metadata.file_path, results will include the source location.",
],
parameters: Type.Object({
query: Type.String({
description: "Text query to search for (auto-converted to embedding)",
}),
vector: Type.Optional(
Type.Array(Type.Number(), {
description:
"Optional pre-computed query vector. If provided, overrides 'query' text.",
}),
),
limit: Type.Optional(
Type.Number({
default: 10,
description: "Maximum results (1-50)",
minimum: 1,
maximum: 50,
}),
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
const manager = VectordriveManager.getInstance();
const status = await manager.getStatus();
const limit = Math.min(Math.max(params.limit ?? 10, 1), 50);
if (status.backend === "none") {
return {
content: [
{
type: "text",
text: `VectorDrive is unavailable: ${status.error ?? "unknown error"}\n\nInstall with: npm install vectordrive`,
},
],
details: { status },
isError: true,
};
}
const queryVector =
params.vector && params.vector.length > 0
? params.vector
: textToVector(params.query);
const results = await manager.search(queryVector, limit);
if (results.length === 0) {
return {
content: [
{
type: "text",
text: `No results found in VectorDrive for query: "${params.query}"`,
},
],
details: { query: params.query, count: 0 },
isError: false,
};
}
const lines = [];
lines.push(`# VectorDrive Search Results`);
lines.push(`Query: "${params.query}"`);
lines.push("");
for (const r of results) {
const meta = r.metadata ?? {};
const preview = meta.text_preview ?? "";
lines.push(`## ${r.id} (score: ${r.score.toFixed(4)})`);
if (preview) {
lines.push("```");
lines.push(String(preview).slice(0, 400));
lines.push("```");
}
const metaLines = Object.entries(meta)
.filter(([k]) => k !== "text_preview" && k !== "stored_at")
.map(([k, v]) => `- ${k}: ${v}`);
if (metaLines.length > 0) {
lines.push(...metaLines);
}
lines.push("");
}
return {
content: [{ type: "text", text: lines.join("\n") }],
details: { results, count: results.length },
isError: false,
};
},
});
}

View file

@ -1,108 +0,0 @@
/**
* VectorDrive Store Tool
*
* Store a vector with metadata in the native VectorDb.
*/
import { Type } from "@sinclair/typebox";
import { textToVector, VectordriveManager } from "./manager.js";
export function registerVectordriveStoreTool(pi) {
pi.registerTool({
name: "vectordrive_store",
label: "VectorDrive Store",
description:
"Store a vector entry in VectorDrive. Accepts either a pre-computed " +
"vector array or raw text (a simple hash embedding is generated automatically). " +
"Metadata is stored as JSON and returned in search results.",
promptSnippet: "Store a memory or code chunk in VectorDrive",
parameters: Type.Object({
id: Type.String({
description:
"Unique identifier for this entry (e.g. file-path:line-range)",
}),
text: Type.Optional(
Type.String({
description:
"Raw text content to store. A simple embedding is auto-generated if 'vector' is not provided.",
}),
),
vector: Type.Optional(
Type.Array(Type.Number(), {
description:
"Pre-computed embedding vector (384 dimensions). Overrides 'text' if provided.",
}),
),
metadata: Type.Optional(
Type.Record(Type.String(), Type.Unknown(), {
description:
"Optional metadata object (e.g. { file_path, line_start, language })",
}),
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
const manager = VectordriveManager.getInstance();
const status = await manager.getStatus();
if (status.backend === "none") {
return {
content: [
{
type: "text",
text: `VectorDrive is unavailable: ${status.error ?? "unknown error"}\n\nInstall with: npm install vectordrive`,
},
],
details: { status },
isError: true,
};
}
const id = params.id.trim();
if (!id) {
return {
content: [{ type: "text", text: "Error: id is required." }],
details: { error: "missing_id" },
isError: true,
};
}
let vector;
if (params.vector && params.vector.length > 0) {
vector = params.vector;
} else if (params.text) {
vector = textToVector(params.text);
} else {
return {
content: [
{
type: "text",
text: "Error: either 'text' or 'vector' must be provided.",
},
],
details: { error: "missing_content" },
isError: true,
};
}
const metadata = {
...(params.metadata ?? {}),
stored_at: new Date().toISOString(),
};
if (params.text) {
metadata.text_preview = params.text.slice(0, 200);
}
const ok = await manager.store({ id, vector, metadata });
if (!ok) {
return {
content: [{ type: "text", text: "Error: failed to store entry." }],
details: { error: "store_failed" },
isError: true,
};
}
return {
content: [
{
type: "text",
text: `Stored ${id} (${vector.length} dims).`,
},
],
details: { id, dimensions: vector.length, metadata },
isError: false,
};
},
});
}

View file

@ -197,7 +197,6 @@ test("loader sets all 4 SF_ env vars and PI_PACKAGE_DIR", async (_t) => {
"sf",
"bg-shell",
"browser-tools",
"subagent",
"search-the-web",
]) {
assert.ok(
@ -326,7 +325,6 @@ test("initResources syncs extensions, agents, and skills to target dir", async (
assertExtensionIndexExists(fakeAgentDir, "browser-tools");
assertExtensionIndexExists(fakeAgentDir, "search-the-web");
assertExtensionIndexExists(fakeAgentDir, "context7");
assertExtensionIndexExists(fakeAgentDir, "subagent");
// Agents synced
assert.ok(

View file

@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, test } from "vitest";
@ -138,4 +138,49 @@ describe("discoverExtensionEntryPaths", () => {
"cmux should not be discovered",
);
});
test("deduplicates directories when a symlink points to an already-discovered real dir", (_t) => {
const root = makeTempDir();
afterEach(() => rmSync(root, { recursive: true, force: true }));
// Real extension directory
const extDir = join(root, "sf");
mkdirSync(extDir);
writeFileSync(join(extDir, "index.js"), "export default function() {}");
// Symlink pointing to the same directory under a different name
const linkDir = join(root, "sf-link");
symlinkSync(extDir, linkDir, "dir");
const paths = discoverExtensionEntryPaths(root);
// Should only return index.js once, not twice
const indexJsPaths = paths.filter((p) => p.endsWith("index.js"));
assert.equal(
indexJsPaths.length,
1,
`Expected 1 index.js entry but got ${indexJsPaths.length}: ${JSON.stringify(indexJsPaths)}`,
);
});
test("deduplicates top-level file extensions when a symlink aliases a real .js file", (_t) => {
const root = makeTempDir();
afterEach(() => rmSync(root, { recursive: true, force: true }));
// Real top-level extension file
writeFileSync(join(root, "ask-user-questions.js"), "export {}");
// Symlink to the same file under a different name
symlinkSync(
join(root, "ask-user-questions.js"),
join(root, "ask-user-questions-alias.js"),
);
const paths = discoverExtensionEntryPaths(root);
// Both filenames resolve to the same inode → deduplicated to 1
assert.equal(
paths.length,
1,
`Expected 1 entry after dedup but got ${paths.length}: ${JSON.stringify(paths)}`,
);
});
});

View file

@ -18,17 +18,14 @@ test("features inventory generator surfaces expected SF native tool, extension,
const searchProviders = parseSearchProviders();
const knownProviders = parseKnownProviders();
assert.ok(sfNativeTools.includes("sf_plan_milestone"));
assert.ok(sfNativeTools.includes("sf_task_complete"));
assert.ok(sfNativeTools.includes("sf_validate_milestone"));
assert.ok(sfNativeTools.includes("plan_milestone"));
assert.ok(sfNativeTools.includes("complete_task"));
assert.ok(sfNativeTools.includes("validate_milestone"));
assert.ok(!sfNativeTools.includes("capture_thought"));
assert.ok(extensions.includes("sf"));
assert.ok(extensions.includes("search-the-web"));
assert.ok(extensions.includes("subagent"));
assert.ok(extensions.includes("guardrails"));
assert.ok(extensions.includes("sf-permissions"));
assert.ok(extensions.includes("sf-inturn-guard"));
assert.deepEqual(searchProviders, [
"brave",
@ -63,7 +60,7 @@ test("features inventory generator injects a rendered appendix between markers",
assert.match(updated, /### Search Providers/);
assert.match(updated, /### Known Model Providers/);
assert.match(updated, /- `search-the-web` — \[extension-manifest\.json]/);
assert.match(updated, /- `sf_task_complete`/);
assert.match(updated, /- `plan_milestone`/);
assert.match(updated, /- `brave`/);
assert.match(updated, /- `xiaomi`/);
assert.ok(updated.includes(generated));

View file

@ -1278,8 +1278,8 @@ test("resolveSearchProvider prefers tavily over minimax in autonomous mode", asy
process.env.TAVILY_API_KEY = "test-tavily-key";
process.env.MINIMAX_API_KEY = "test-minimax-key";
// Tavily should be preferred in autonomous mode
const result = resolveSearchProvider();
// In auto mode (no explicit preference), tavily wins by registry order.
const result = resolveSearchProvider("auto");
assert.equal(
result,
"tavily",

View file

@ -8,7 +8,9 @@
"declaration": false,
"declarationMap": false,
"sourceMap": false,
"tsBuildInfoFile": "dist/.tsbuildinfo/tsconfig.resources.tsbuildinfo"
"tsBuildInfoFile": "dist/.tsbuildinfo/tsconfig.resources.tsbuildinfo",
"allowJs": true,
"checkJs": false
},
"include": ["src/resources/**/*.ts"],
"exclude": [