chore: commit current workspace state

This commit is contained in:
Mikael Hugo 2026-05-05 14:46:18 +02:00
parent f11c877224
commit 00a118ea71
822 changed files with 3525 additions and 4215 deletions

View file

@ -27,7 +27,16 @@
"rules": { "rules": {
"recommended": true, "recommended": true,
"correctness": { "correctness": {
"noUnreachable": "off" "noUnreachable": "off",
"useExhaustiveDependencies": "off"
},
"a11y": {
"noLabelWithoutControl": "off",
"noStaticElementInteractions": "off",
"noSvgWithoutTitle": "off",
"useAriaPropsSupportedByRole": "off",
"useKeyWithClickEvents": "off",
"useSemanticElements": "off"
}, },
"style": { "style": {
"noNonNullAssertion": "off", "noNonNullAssertion": "off",
@ -35,7 +44,9 @@
}, },
"suspicious": { "suspicious": {
"noAssignInExpressions": "off", "noAssignInExpressions": "off",
"noArrayIndexKey": "off",
"noControlCharactersInRegex": "off", "noControlCharactersInRegex": "off",
"noDocumentCookie": "off",
"noDuplicateTestHooks": "off", "noDuplicateTestHooks": "off",
"noExplicitAny": "off", "noExplicitAny": "off",
"noImplicitAnyLet": "off", "noImplicitAnyLet": "off",

View file

@ -15,7 +15,7 @@ is left alone.
This is correct for protecting user content but wrong for everything else: This is correct for protecting user content but wrong for everything else:
1. **No drift signal.** When SF adds a new template (e.g. `harness/AGENTS.md`, 1. **No drift signal.** When SF adds a new template (e.g. `.sf/harness/AGENTS.md`,
`ARCHITECTURE.md`) or improves an existing one (e.g. tightens `ARCHITECTURE.md`) or improves an existing one (e.g. tightens
`RELIABILITY.md`), existing projects never notice. Only newly-bootstrapped `RELIABILITY.md`), existing projects never notice. Only newly-bootstrapped
projects benefit. projects benefit.
@ -95,7 +95,7 @@ Apply markers to every entry in `SCAFFOLD_FILES`. Categories:
|----------|------------------|------------------| |----------|------------------|------------------|
| Markdown docs | `AGENTS.md`, `ARCHITECTURE.md`, `docs/RELIABILITY.md`, `docs/SECURITY.md`, `docs/DESIGN.md`, `docs/QUALITY_SCORE.md`, `docs/RECORDS_KEEPER.md`, all `*/AGENTS.md` | HTML comment on line 1 | | Markdown docs | `AGENTS.md`, `ARCHITECTURE.md`, `docs/RELIABILITY.md`, `docs/SECURITY.md`, `docs/DESIGN.md`, `docs/QUALITY_SCORE.md`, `docs/RECORDS_KEEPER.md`, all `*/AGENTS.md` | HTML comment on line 1 |
| Frontmatter docs | `.sf/PREFERENCES.md` | Frontmatter fields: `last_synced_with_sf`, `sf_template_state`, `sf_template_hash` (extends prior art in `preferences-template-upgrade.ts`) | | Frontmatter docs | `.sf/PREFERENCES.md` | Frontmatter fields: `last_synced_with_sf`, `sf_template_state`, `sf_template_hash` (extends prior art in `preferences-template-upgrade.ts`) |
| Templates / specs | `docs/design-docs/ADR-TEMPLATE.md`, `harness/specs/bootstrap.md`, `harness/AGENTS.md`, `harness/specs/AGENTS.md`, `harness/evals/AGENTS.md`, `harness/graders/AGENTS.md` | HTML comment on line 1 | | Templates / specs | `docs/design-docs/ADR-TEMPLATE.md`, `.sf/harness/specs/bootstrap.md`, `.sf/harness/AGENTS.md`, `.sf/harness/specs/AGENTS.md`, `.sf/harness/evals/AGENTS.md`, `.sf/harness/graders/AGENTS.md` | HTML comment on line 1 |
| Reference slot text files | `docs/references/*-llms.txt` | HTML comment on line 1 (Markdown comments are valid in plain text consumed by LLMs) | | Reference slot text files | `docs/references/*-llms.txt` | HTML comment on line 1 (Markdown comments are valid in plain text consumed by LLMs) |
| `.siftignore` and similar non-Markdown configs | `.siftignore` | Skip versioning. Sibling file `.sf/scaffold-manifest.json` records the applied version. (Rationale: hash-based legacy match is sufficient; markers in dotfiles fight tooling.) | | `.siftignore` and similar non-Markdown configs | `.siftignore` | Skip versioning. Sibling file `.sf/scaffold-manifest.json` records the applied version. (Rationale: hash-based legacy match is sufficient; markers in dotfiles fight tooling.) |

View file

@ -1,20 +0,0 @@
# Bootstrap Spec: Agent Legibility
Verifies that the SF repo is minimally agent-legible.
## Criteria
- [ ] `AGENTS.md` exists at repo root and is non-empty.
- [ ] `ARCHITECTURE.md` exists at repo root and describes the system.
- [ ] `docs/exec-plans/active/index.md` exists.
- [ ] `docs/exec-plans/tech-debt-tracker.md` exists.
- [ ] `docs/design-docs/ADR-TEMPLATE.md` exists.
- [ ] `harness/specs/` exists with at least this file.
## Verification command
```bash
for f in AGENTS.md ARCHITECTURE.md docs/exec-plans/active/index.md docs/exec-plans/tech-debt-tracker.md docs/design-docs/ADR-TEMPLATE.md harness/specs/bootstrap.md; do [ -s "$f" ] && echo "OK: $f" || echo "MISSING: $f"; done
```
All lines should start with `OK:` for this spec to pass.

View file

@ -72,11 +72,12 @@ spec name:
printf "# {{name}}\n\n## Job to be done\n\n## Workflow\n\n## Edge cases\n\n## Non-goals\n\n## Verification\n\n\`\`\`bash\n# command that proves this spec passes\n\`\`\`\n" > "${dest}" printf "# {{name}}\n\n## Job to be done\n\n## Workflow\n\n## Edge cases\n\n## Non-goals\n\n## Verification\n\n\`\`\`bash\n# command that proves this spec passes\n\`\`\`\n" > "${dest}"
echo "Created: ${dest}" echo "Created: ${dest}"
# Create a new harness spec (usage: just harness-spec "behavior-name") # Create a new SF-local harness spec (usage: just harness-spec "behavior-name")
harness-spec name: harness-spec name:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
dest="harness/specs/{{name}}.md" dest=".sf/harness/specs/{{name}}.md"
if [ -f "${dest}" ]; then echo "Already exists: ${dest}"; exit 1; fi if [ -f "${dest}" ]; then echo "Already exists: ${dest}"; exit 1; fi
mkdir -p "$(dirname "${dest}")"
printf "# Harness Spec: {{name}}\n\n## Behavior\n\n## Verification command\n\n\`\`\`bash\n\n\`\`\`\n\n## Pass criteria\n\n" > "${dest}" printf "# Harness Spec: {{name}}\n\n## Behavior\n\n## Verification command\n\n\`\`\`bash\n\n\`\`\`\n\n## Pass criteria\n\n" > "${dest}"
echo "Created: ${dest}" echo "Created: ${dest}"

View file

@ -98,8 +98,10 @@
"typecheck:extensions": "npm run check:versioned-json && tsc --noEmit --project tsconfig.extensions.json", "typecheck:extensions": "npm run check:versioned-json && tsc --noEmit --project tsconfig.extensions.json",
"check:sf-inventory": "node scripts/check-sf-extension-inventory.mjs", "check:sf-inventory": "node scripts/check-sf-extension-inventory.mjs",
"check:versioned-json": "node scripts/check-versioned-json.mjs && npm run check:sf-inventory", "check:versioned-json": "node scripts/check-versioned-json.mjs && npm run check:sf-inventory",
"lint": "npm run check:versioned-json && biome lint src/", "format": "biome format --write .",
"lint:fix": "npm run check:versioned-json && biome lint src/ --write", "format:check": "biome format .",
"lint": "npm run check:versioned-json && biome check .",
"lint:fix": "npm run check:versioned-json && biome check --write .",
"pipeline:version-stamp": "node scripts/version-stamp.mjs", "pipeline:version-stamp": "node scripts/version-stamp.mjs",
"release:changelog": "node scripts/generate-changelog.mjs", "release:changelog": "node scripts/generate-changelog.mjs",
"release:bump": "node scripts/bump-version.mjs", "release:bump": "node scripts/bump-version.mjs",

View file

@ -6,12 +6,12 @@
*/ */
import { import {
ChannelType,
PermissionFlagsBits,
type Guild,
type CategoryChannel, type CategoryChannel,
type TextChannel, ChannelType,
type Guild,
type GuildBasedChannel, type GuildBasedChannel,
PermissionFlagsBits,
type TextChannel,
} from "discord.js"; } from "discord.js";
import type { Logger } from "./logger.js"; import type { Logger } from "./logger.js";

View file

@ -1,9 +1,9 @@
import { parseArgs } from "node:util";
import { resolve } from "node:path"; import { resolve } from "node:path";
import { resolveConfigPath, loadConfig } from "./config.js"; import { parseArgs } from "node:util";
import { Logger } from "./logger.js"; import { loadConfig, resolveConfigPath } from "./config.js";
import { Daemon } from "./daemon.js"; import { Daemon } from "./daemon.js";
import { install, uninstall, status } from "./launchd.js"; import { install, status, uninstall } from "./launchd.js";
import { Logger } from "./logger.js";
export const COMMAND_NAME = "sf-server"; export const COMMAND_NAME = "sf-server";

View file

@ -6,13 +6,13 @@
*/ */
import { import {
SlashCommandBuilder, type REST,
REST,
Routes,
type RESTPostAPIChatInputApplicationCommandsJSONBody, type RESTPostAPIChatInputApplicationCommandsJSONBody,
Routes,
SlashCommandBuilder,
} from "discord.js"; } from "discord.js";
import type { ManagedSession } from "./types.js";
import type { Logger } from "./logger.js"; import type { Logger } from "./logger.js";
import type { ManagedSession } from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Command definitions // Command definitions

View file

@ -1,4 +1,4 @@
import { readFileSync, existsSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { resolve } from "node:path"; import { resolve } from "node:path";
import { parse as parseYaml } from "yaml"; import { parse as parseYaml } from "yaml";
@ -54,7 +54,7 @@ export function validateConfig(raw: unknown): DaemonConfig {
const obj = raw as Record<string, unknown>; const obj = raw as Record<string, unknown>;
// --- discord --- // --- discord ---
let discord: DaemonConfig["discord"] = undefined; let discord: DaemonConfig["discord"];
if (obj["discord"] != null && typeof obj["discord"] === "object") { if (obj["discord"] != null && typeof obj["discord"] === "object") {
const d = obj["discord"] as Record<string, unknown>; const d = obj["discord"] as Record<string, unknown>;
discord = { discord = {

View file

@ -1,22 +1,21 @@
import { describe, it, afterEach, beforeAll, afterAll } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { execFileSync, spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import { import {
mkdtempSync,
writeFileSync,
readFileSync,
rmSync,
existsSync, existsSync,
mkdirSync, mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs"; } from "node:fs";
import { join } from "node:path"; import { homedir, tmpdir } from "node:os";
import { tmpdir, homedir } from "node:os"; import { dirname, join } from "node:path";
import { randomUUID } from "node:crypto";
import { execFileSync, spawn } from "node:child_process";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { dirname } from "node:path"; import { afterAll, afterEach, beforeAll, describe, it } from "vitest";
import { resolveConfigPath, loadConfig, validateConfig } from "./config.js"; import { loadConfig, resolveConfigPath, validateConfig } from "./config.js";
import { Logger } from "./logger.js";
import { Daemon } from "./daemon.js"; import { Daemon } from "./daemon.js";
import { Logger } from "./logger.js";
import { SessionManager } from "./session-manager.js"; import { SessionManager } from "./session-manager.js";
import type { DaemonConfig, LogEntry } from "./types.js"; import type { DaemonConfig, LogEntry } from "./types.js";

View file

@ -1,10 +1,10 @@
import type { DaemonConfig, ProjectInfo } from "./types.js";
import type { Logger } from "./logger.js";
import { SessionManager } from "./session-manager.js";
import { scanForProjects } from "./project-scanner.js";
import { DiscordBot, validateDiscordConfig } from "./discord-bot.js"; import { DiscordBot, validateDiscordConfig } from "./discord-bot.js";
import { EventBridge } from "./event-bridge.js"; import { EventBridge } from "./event-bridge.js";
import type { Logger } from "./logger.js";
import { Orchestrator } from "./orchestrator.js"; import { Orchestrator } from "./orchestrator.js";
import { scanForProjects } from "./project-scanner.js";
import { SessionManager } from "./session-manager.js";
import type { DaemonConfig, ProjectInfo } from "./types.js";
/** /**
* Core daemon class ties config + logger together with lifecycle management. * Core daemon class ties config + logger together with lifecycle management.

View file

@ -1,17 +1,17 @@
import { describe, it, afterEach } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { ChannelType } from "discord.js"; import { ChannelType } from "discord.js";
import { isAuthorized, validateDiscordConfig } from "./discord-bot.js"; import { afterEach, describe, it } from "vitest";
import { sanitizeChannelName, ChannelManager } from "./channel-manager.js"; import { ChannelManager, sanitizeChannelName } from "./channel-manager.js";
import { buildCommands, formatSessionStatus } from "./commands.js"; import { buildCommands, formatSessionStatus } from "./commands.js";
import { Daemon } from "./daemon.js";
import { Logger } from "./logger.js";
import { validateConfig } from "./config.js"; import { validateConfig } from "./config.js";
import type { DaemonConfig, LogEntry, ManagedSession } from "./types.js"; import { Daemon } from "./daemon.js";
import { isAuthorized, validateDiscordConfig } from "./discord-bot.js";
import { Logger } from "./logger.js";
import type { DaemonConfig, ManagedSession } from "./types.js";
// ---------- helpers ---------- // ---------- helpers ----------
@ -315,7 +315,7 @@ describe("ChannelManager", () => {
name: string; name: string;
type: number; type: number;
parentId: string | null; parentId: string | null;
edit?: Function; edit?: (editOpts: { parent?: string }) => Promise<unknown>;
} }
>(); >();
let createCounter = 0; let createCounter = 0;
@ -592,7 +592,7 @@ describe("formatSessionStatus", () => {
describe("command dispatch", () => { describe("command dispatch", () => {
// Minimal mock of a ChatInputCommandInteraction // Minimal mock of a ChatInputCommandInteraction
function mockInteraction(commandName: string, userId: string = "owner-1") { function _mockInteraction(commandName: string, userId: string = "owner-1") {
let replied = false; let replied = false;
let replyContent = ""; let replyContent = "";
@ -611,8 +611,8 @@ describe("command dispatch", () => {
} }
// Minimal mock of a non-command interaction // Minimal mock of a non-command interaction
function mockNonCommandInteraction(userId: string = "owner-1") { function _mockNonCommandInteraction(userId: string = "owner-1") {
let replied = false; const replied = false;
return { return {
user: { id: userId }, user: { id: userId },
type: 3, // InteractionType.MessageComponent type: 3, // InteractionType.MessageComponent

View file

@ -7,26 +7,25 @@
*/ */
import { import {
ActionRowBuilder,
Client, Client,
ComponentType,
GatewayIntentBits, GatewayIntentBits,
type Interaction,
REST, REST,
StringSelectMenuBuilder, StringSelectMenuBuilder,
ActionRowBuilder,
ComponentType,
type Interaction,
type Guild,
type StringSelectMenuInteraction, type StringSelectMenuInteraction,
} from "discord.js"; } from "discord.js";
import type { DaemonConfig, VerbosityLevel, ProjectInfo } from "./types.js";
import type { Logger } from "./logger.js";
import type { SessionManager } from "./session-manager.js";
import { ChannelManager } from "./channel-manager.js"; import { ChannelManager } from "./channel-manager.js";
import { import {
buildCommands, buildCommands,
registerGuildCommands,
formatSessionStatus, formatSessionStatus,
registerGuildCommands,
} from "./commands.js"; } from "./commands.js";
import type { EventBridge } from "./event-bridge.js"; import type { EventBridge } from "./event-bridge.js";
import type { Logger } from "./logger.js";
import type { SessionManager } from "./session-manager.js";
import type { DaemonConfig, ProjectInfo, VerbosityLevel } from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Pure helpers — exported for testability // Pure helpers — exported for testability

View file

@ -6,22 +6,22 @@
* blocker handling, conversation relay, and cleanup. * blocker handling, conversation relay, and cleanup.
*/ */
import { vi, describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
import { EventBridge } from "./event-bridge.js";
import type { EventBridgeOptions, BridgeClient } from "./event-bridge.js";
import type { import type {
PendingBlocker,
ManagedSession,
DaemonConfig,
SessionStatus,
} from "./types.js";
import type {
SdkAgentEvent,
RpcClient, RpcClient,
RpcExtensionUIRequest, RpcExtensionUIRequest,
SdkAgentEvent,
} from "@singularity-forge/rpc-client"; } from "@singularity-forge/rpc-client";
import { describe, it, vi } from "vitest";
import type { BridgeClient, EventBridgeOptions } from "./event-bridge.js";
import { EventBridge } from "./event-bridge.js";
import type {
DaemonConfig,
ManagedSession,
PendingBlocker,
SessionStatus,
} from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Mock factories // Mock factories
@ -263,7 +263,7 @@ describe("EventBridge", () => {
}); });
it("filters events based on verbosity", async () => { it("filters events based on verbosity", async () => {
const { bridge, sessionManager, channelManager, logger } = buildBridge(); const { bridge, sessionManager, logger } = buildBridge();
bridge.start(); bridge.start();
sessionManager.emit("session:started", { sessionManager.emit("session:started", {
sessionId: "sess-1", sessionId: "sess-1",
@ -470,7 +470,7 @@ describe("EventBridge", () => {
}); });
await tick(); await tick();
const collectorCalls = mockFn( const _collectorCalls = mockFn(
channelManager._channel.createMessageComponentCollector, channelManager._channel.createMessageComponentCollector,
).mock.calls; ).mock.calls;
const collector = mockFn( const collector = mockFn(
@ -518,7 +518,7 @@ describe("EventBridge", () => {
}); });
await tick(); await tick();
const collectorCalls = mockFn( const _collectorCalls = mockFn(
channelManager._channel.createMessageComponentCollector, channelManager._channel.createMessageComponentCollector,
).mock.calls; ).mock.calls;
const collector = mockFn( const collector = mockFn(

View file

@ -10,28 +10,27 @@
* - DM backup owner gets DM on blocker when dm_on_blocker configured * - DM backup owner gets DM on blocker when dm_on_blocker configured
*/ */
import type {
Client,
Message,
TextChannel,
MessageComponentInteraction,
} from "discord.js";
import { EmbedBuilder, ComponentType } from "discord.js";
import type { SdkAgentEvent } from "@singularity-forge/rpc-client"; import type { SdkAgentEvent } from "@singularity-forge/rpc-client";
import type { Logger } from "./logger.js"; import type {
import type { DaemonConfig, PendingBlocker } from "./types.js"; Message,
import type { SessionManager } from "./session-manager.js"; MessageComponentInteraction,
TextChannel,
} from "discord.js";
import { ComponentType, EmbedBuilder } from "discord.js";
import type { ChannelManager } from "./channel-manager.js"; import type { ChannelManager } from "./channel-manager.js";
import { MessageBatcher } from "./message-batcher.js";
import { VerbosityManager } from "./verbosity.js";
import {
formatEvent,
formatBlocker,
formatSessionStarted,
formatError,
formatCompletion,
} from "./event-formatter.js";
import { isAuthorized } from "./discord-bot.js"; import { isAuthorized } from "./discord-bot.js";
import {
formatBlocker,
formatCompletion,
formatError,
formatEvent,
formatSessionStarted,
} from "./event-formatter.js";
import type { Logger } from "./logger.js";
import { MessageBatcher } from "./message-batcher.js";
import type { SessionManager } from "./session-manager.js";
import type { DaemonConfig, PendingBlocker } from "./types.js";
import { VerbosityManager } from "./verbosity.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types

View file

@ -1,22 +1,20 @@
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { EmbedBuilder, ActionRowBuilder, ButtonBuilder } from "discord.js";
import type { SdkAgentEvent } from "@singularity-forge/rpc-client"; import type { SdkAgentEvent } from "@singularity-forge/rpc-client";
import type { PendingBlocker, FormattedEvent } from "./types.js"; import { describe, it } from "vitest";
import type { RpcExtensionUIRequest } from "@singularity-forge/rpc-client";
import { import {
formatToolStart,
formatToolEnd,
formatMessage,
formatBlocker, formatBlocker,
formatCompletion, formatCompletion,
formatError,
formatCostUpdate, formatCostUpdate,
formatError,
formatEvent,
formatGenericEvent,
formatMessage,
formatSessionStarted, formatSessionStarted,
formatTaskTransition, formatTaskTransition,
formatGenericEvent, formatToolEnd,
formatEvent, formatToolStart,
} from "./event-formatter.js"; } from "./event-formatter.js";
import type { FormattedEvent, PendingBlocker } from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers

View file

@ -10,14 +10,13 @@
* grey = tool / generic * grey = tool / generic
*/ */
import type { SdkAgentEvent } from "@singularity-forge/rpc-client";
import { import {
EmbedBuilder,
ActionRowBuilder, ActionRowBuilder,
ButtonBuilder, ButtonBuilder,
ButtonStyle, ButtonStyle,
EmbedBuilder,
} from "discord.js"; } from "discord.js";
import type { SdkAgentEvent } from "@singularity-forge/rpc-client";
import type { RpcExtensionUIRequest } from "@singularity-forge/rpc-client";
import type { FormattedEvent, PendingBlocker } from "./types.js"; import type { FormattedEvent, PendingBlocker } from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -190,7 +189,7 @@ export function formatBlocker(
const chunks = chunkArray(options.slice(0, 25), 5); const chunks = chunkArray(options.slice(0, 25), 5);
for (const chunk of chunks) { for (const chunk of chunks) {
const row = new ActionRowBuilder<ButtonBuilder>(); const row = new ActionRowBuilder<ButtonBuilder>();
chunk.forEach((opt, i) => { chunk.forEach((opt, _i) => {
const globalIndex = options.indexOf(opt); const globalIndex = options.indexOf(opt);
row.addComponents( row.addComponents(
new ButtonBuilder() new ButtonBuilder()
@ -414,7 +413,7 @@ export function formatGenericEvent(event: SdkAgentEvent): FormattedEvent {
*/ */
export function formatEvent( export function formatEvent(
event: SdkAgentEvent, event: SdkAgentEvent,
ownerId?: string, _ownerId?: string,
): FormattedEvent { ): FormattedEvent {
const type = str(event.type); const type = str(event.type);

View file

@ -1,72 +1,72 @@
export type {
DaemonConfig,
LogLevel,
LogEntry,
SessionStatus,
ManagedSession,
PendingBlocker,
CostAccumulator,
ProjectInfo,
ProjectMarker,
StartSessionOptions,
FormattedEvent,
VerbosityLevel,
} from "./types.js";
export { MAX_EVENTS, INIT_TIMEOUT_MS } from "./types.js";
export { resolveConfigPath, loadConfig, validateConfig } from "./config.js";
export { Logger } from "./logger.js";
export type { LoggerOptions } from "./logger.js";
export { Daemon } from "./daemon.js";
export { scanForProjects } from "./project-scanner.js";
export { SessionManager } from "./session-manager.js";
export {
DiscordBot,
isAuthorized,
validateDiscordConfig,
} from "./discord-bot.js";
export type { DiscordBotOptions } from "./discord-bot.js";
export { ChannelManager, sanitizeChannelName } from "./channel-manager.js";
export type { ChannelManagerOptions } from "./channel-manager.js"; export type { ChannelManagerOptions } from "./channel-manager.js";
export { ChannelManager, sanitizeChannelName } from "./channel-manager.js";
export { export {
buildCommands, buildCommands,
formatSessionStatus, formatSessionStatus,
registerGuildCommands, registerGuildCommands,
} from "./commands.js"; } from "./commands.js";
export { EventBridge } from "./event-bridge.js"; export { loadConfig, resolveConfigPath, validateConfig } from "./config.js";
export type { BridgeClient, EventBridgeOptions } from "./event-bridge.js"; export { Daemon } from "./daemon.js";
export { Orchestrator } from "./orchestrator.js"; export type { DiscordBotOptions } from "./discord-bot.js";
export type { export {
OrchestratorConfig, DiscordBot,
OrchestratorDeps, isAuthorized,
DiscordMessageLike, validateDiscordConfig,
} from "./orchestrator.js"; } from "./discord-bot.js";
export { MessageBatcher } from "./message-batcher.js"; export type { BridgeClient, EventBridgeOptions } from "./event-bridge.js";
export type { export { EventBridge } from "./event-bridge.js";
SendPayload,
SendFn,
BatcherLogger,
BatcherOptions,
} from "./message-batcher.js";
export { VerbosityManager, shouldShowAtLevel } from "./verbosity.js";
export { export {
formatToolStart,
formatToolEnd,
formatMessage,
formatBlocker, formatBlocker,
formatCompletion, formatCompletion,
formatError,
formatCostUpdate, formatCostUpdate,
formatError,
formatEvent,
formatGenericEvent,
formatMessage,
formatSessionStarted, formatSessionStarted,
formatTaskTransition, formatTaskTransition,
formatGenericEvent, formatToolEnd,
formatEvent, formatToolStart,
} from "./event-formatter.js"; } from "./event-formatter.js";
export type { LaunchdStatus, PlistOptions, RunCommandFn } from "./launchd.js";
export { export {
escapeXml, escapeXml,
generatePlist, generatePlist,
getPlistPath, getPlistPath,
install as installLaunchAgent, install as installLaunchAgent,
uninstall as uninstallLaunchAgent,
status as launchAgentStatus, status as launchAgentStatus,
uninstall as uninstallLaunchAgent,
} from "./launchd.js"; } from "./launchd.js";
export type { PlistOptions, LaunchdStatus, RunCommandFn } from "./launchd.js"; export type { LoggerOptions } from "./logger.js";
export { Logger } from "./logger.js";
export type {
BatcherLogger,
BatcherOptions,
SendFn,
SendPayload,
} from "./message-batcher.js";
export { MessageBatcher } from "./message-batcher.js";
export type {
DiscordMessageLike,
OrchestratorConfig,
OrchestratorDeps,
} from "./orchestrator.js";
export { Orchestrator } from "./orchestrator.js";
export { scanForProjects } from "./project-scanner.js";
export { SessionManager } from "./session-manager.js";
export type {
CostAccumulator,
DaemonConfig,
FormattedEvent,
LogEntry,
LogLevel,
ManagedSession,
PendingBlocker,
ProjectInfo,
ProjectMarker,
SessionStatus,
StartSessionOptions,
VerbosityLevel,
} from "./types.js";
export { INIT_TIMEOUT_MS, MAX_EVENTS } from "./types.js";
export { shouldShowAtLevel, VerbosityManager } from "./verbosity.js";

View file

@ -1,30 +1,22 @@
import { describe, it, beforeEach, afterEach } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import {
mkdtempSync,
existsSync,
readFileSync,
writeFileSync,
rmSync,
mkdirSync,
statSync,
} from "node:fs";
import { join, dirname } from "node:path";
import { tmpdir, homedir } from "node:os";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, it } from "vitest";
import type { PlistOptions, RunCommandFn } from "./launchd.js";
import { import {
escapeXml, escapeXml,
generatePlist, generatePlist,
getPlistPath, getPlistPath,
install, install,
uninstall,
status, status,
uninstall,
} from "./launchd.js"; } from "./launchd.js";
import type { PlistOptions, RunCommandFn, LaunchdStatus } from "./launchd.js";
// ---------- helpers ---------- // ---------- helpers ----------
function tmpDir(): string { function _tmpDir(): string {
return mkdtempSync( return mkdtempSync(
join(tmpdir(), `launchd-test-${randomUUID().slice(0, 8)}-`), join(tmpdir(), `launchd-test-${randomUUID().slice(0, 8)}-`),
); );
@ -184,8 +176,8 @@ describe("getPlistPath", () => {
// ---------- install ---------- // ---------- install ----------
describe("install", () => { describe("install", () => {
let tmp: string; let _tmp: string;
let fakePlistPath: string; let _fakePlistPath: string;
// We can't mock getPlistPath directly, but we can verify the commands // We can't mock getPlistPath directly, but we can verify the commands
// issued and the plist content by intercepting runCommand and filesystem ops. // issued and the plist content by intercepting runCommand and filesystem ops.
@ -209,7 +201,7 @@ describe("install", () => {
} }
const loadCalls = calls.filter((c) => c.startsWith("launchctl load")); const loadCalls = calls.filter((c) => c.startsWith("launchctl load"));
const listCalls = calls.filter((c) => c.startsWith("launchctl list")); const _listCalls = calls.filter((c) => c.startsWith("launchctl list"));
// Should have at least attempted launchctl load // Should have at least attempted launchctl load
assert.ok( assert.ok(
loadCalls.length > 0 || calls.length > 0, loadCalls.length > 0 || calls.length > 0,
@ -243,7 +235,7 @@ describe("install", () => {
} }
// The second install should have tried to unload first // The second install should have tried to unload first
const unloadCalls = calls.filter((c) => c.startsWith("launchctl unload")); const _unloadCalls = calls.filter((c) => c.startsWith("launchctl unload"));
// If the plist path exists, we expect at least one unload attempt on second call // If the plist path exists, we expect at least one unload attempt on second call
// This is a command-level check; filesystem existence depends on environment // This is a command-level check; filesystem existence depends on environment
}); });

View file

@ -1,14 +1,13 @@
import {
writeFileSync,
unlinkSync,
existsSync,
chmodSync,
mkdirSync,
} from "node:fs";
import { resolve } from "node:path";
import { homedir } from "node:os";
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { dirname } from "node:path"; import {
chmodSync,
existsSync,
mkdirSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, resolve } from "node:path";
// --------------- types --------------- // --------------- types ---------------

View file

@ -1,6 +1,6 @@
import { createWriteStream, mkdirSync, type WriteStream } from "node:fs"; import { createWriteStream, mkdirSync, type WriteStream } from "node:fs";
import { dirname } from "node:path"; import { dirname } from "node:path";
import type { LogLevel, LogEntry } from "./types.js"; import type { LogEntry, LogLevel } from "./types.js";
const LEVEL_ORDER: Record<LogLevel, number> = { const LEVEL_ORDER: Record<LogLevel, number> = {
debug: 0, debug: 0,

View file

@ -1,7 +1,7 @@
import { vi, describe, it, beforeEach, afterEach } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, it, vi } from "vitest";
import type { BatcherLogger, SendPayload } from "./message-batcher.js";
import { MessageBatcher } from "./message-batcher.js"; import { MessageBatcher } from "./message-batcher.js";
import type { SendPayload, BatcherLogger } from "./message-batcher.js";
import type { FormattedEvent } from "./types.js"; import type { FormattedEvent } from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -265,9 +265,9 @@ describe("MessageBatcher", () => {
describe("error handling", () => { describe("error handling", () => {
it("logs error and continues when send throws", async () => { it("logs error and continues when send throws", async () => {
let attempt = 0; let _attempt = 0;
const sendFn = async () => { const sendFn = async () => {
attempt++; _attempt++;
throw new Error("Discord rate limit"); throw new Error("Discord rate limit");
}; };
const { logger, errors, warns } = createLogger(); const { logger, errors, warns } = createLogger();

View file

@ -5,25 +5,20 @@
* allowing tool execution and conversation flow testing without real API calls. * allowing tool execution and conversation flow testing without real API calls.
*/ */
import { describe, it, afterEach } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { mkdtempSync, rmSync, existsSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, it } from "vitest";
import { Logger } from "./logger.js";
import { import {
type DiscordMessageLike,
Orchestrator, Orchestrator,
type OrchestratorConfig, type OrchestratorConfig,
type OrchestratorDeps, type OrchestratorDeps,
type DiscordMessageLike,
} from "./orchestrator.js"; } from "./orchestrator.js";
import { Logger } from "./logger.js"; import type { ManagedSession, ProjectInfo, SessionStatus } from "./types.js";
import type {
ManagedSession,
ProjectInfo,
SessionStatus,
CostAccumulator,
} from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers

View file

@ -11,20 +11,20 @@
* at the tool execution layer. * at the tool execution layer.
*/ */
import { z } from "zod";
import type Anthropic from "@anthropic-ai/sdk"; import type Anthropic from "@anthropic-ai/sdk";
import type { import type {
MessageParam,
ContentBlockParam, ContentBlockParam,
MessageParam,
TextBlock,
Tool, Tool,
ToolResultBlockParam, ToolResultBlockParam,
ToolUseBlock, ToolUseBlock,
TextBlock,
} from "@anthropic-ai/sdk/resources/messages/messages"; } from "@anthropic-ai/sdk/resources/messages/messages";
import type { SessionManager } from "./session-manager.js"; import { z } from "zod";
import type { ChannelManager } from "./channel-manager.js"; import type { ChannelManager } from "./channel-manager.js";
import type { ProjectInfo, ManagedSession } from "./types.js";
import type { Logger } from "./logger.js"; import type { Logger } from "./logger.js";
import type { SessionManager } from "./session-manager.js";
import type { ManagedSession, ProjectInfo } from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// API key resolution — requires ANTHROPIC_API_KEY env var // API key resolution — requires ANTHROPIC_API_KEY env var

View file

@ -2,19 +2,19 @@
* Tests for the project scanner module. * Tests for the project scanner module.
*/ */
import { describe, it, afterEach } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import {
mkdtempSync,
mkdirSync,
writeFileSync,
rmSync,
existsSync,
chmodSync,
} from "node:fs";
import { join } from "node:path";
import { tmpdir, platform } from "node:os";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
rmSync,
writeFileSync,
} from "node:fs";
import { platform, tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, it } from "vitest";
import { scanForProjects } from "./project-scanner.js"; import { scanForProjects } from "./project-scanner.js";
// ---------- helpers ---------- // ---------- helpers ----------

View file

@ -3,8 +3,8 @@
* marker files/directories. Reads one level deep (immediate children only). * marker files/directories. Reads one level deep (immediate children only).
*/ */
import { readdir, stat, access } from "node:fs/promises"; import { readdir, stat } from "node:fs/promises";
import { join, basename } from "node:path"; import { basename, join } from "node:path";
import type { ProjectInfo, ProjectMarker } from "./types.js"; import type { ProjectInfo, ProjectMarker } from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -6,17 +6,15 @@
* and cleanup without spawning real SF processes. * and cleanup without spawning real SF processes.
*/ */
import { describe, it, beforeEach, afterEach } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { resolve, basename } from "node:path"; import { mkdtempSync, rmSync } from "node:fs";
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { basename, join, resolve } from "node:path";
import { afterEach, describe, it } from "vitest";
import { SessionManager } from "./session-manager.js";
import { MAX_EVENTS } from "./types.js";
import type { ManagedSession, PendingBlocker } from "./types.js";
import { Logger } from "./logger.js"; import { Logger } from "./logger.js";
import { SessionManager } from "./session-manager.js";
import type { ManagedSession, PendingBlocker } from "./types.js";
import { MAX_EVENTS } from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Mock RpcClient (duck-typed to match RpcClient interface) // Mock RpcClient (duck-typed to match RpcClient interface)
@ -843,7 +841,7 @@ describe("SessionManager", () => {
it("logger receives structured calls during lifecycle", async () => { it("logger receives structured calls during lifecycle", async () => {
const { manager, spy } = createManager(); const { manager, spy } = createManager();
const sessionId = await manager.startSession({ const _sessionId = await manager.startSession({
projectDir: "/tmp/log-test", projectDir: "/tmp/log-test",
}); });

View file

@ -14,23 +14,23 @@
*/ */
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { basename, resolve } from "node:path";
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
import { RpcClient } from "@singularity-forge/rpc-client"; import { basename, resolve } from "node:path";
import type { import type {
SdkAgentEvent,
RpcInitResult,
RpcCostUpdateEvent, RpcCostUpdateEvent,
RpcExtensionUIRequest, RpcExtensionUIRequest,
RpcInitResult,
SdkAgentEvent,
} from "@singularity-forge/rpc-client"; } from "@singularity-forge/rpc-client";
import { RpcClient } from "@singularity-forge/rpc-client";
import type { Logger } from "./logger.js";
import type { import type {
ManagedSession, ManagedSession,
StartSessionOptions,
PendingBlocker, PendingBlocker,
RuntimeHeartbeat, RuntimeHeartbeat,
StartSessionOptions,
} from "./types.js"; } from "./types.js";
import { MAX_EVENTS, INIT_TIMEOUT_MS } from "./types.js"; import { INIT_TIMEOUT_MS, MAX_EVENTS } from "./types.js";
import type { Logger } from "./logger.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Inlined detection logic (from headless-events.ts — no internal package imports) // Inlined detection logic (from headless-events.ts — no internal package imports)

View file

@ -1,7 +1,7 @@
import type { import type {
RpcClient, RpcClient,
SdkAgentEvent,
RpcExtensionUIRequest, RpcExtensionUIRequest,
SdkAgentEvent,
} from "@singularity-forge/rpc-client"; } from "@singularity-forge/rpc-client";
/** /**

View file

@ -1,6 +1,6 @@
import { describe, it, beforeEach } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { VerbosityManager, shouldShowAtLevel } from "./verbosity.js"; import { beforeEach, describe, it } from "vitest";
import { shouldShowAtLevel, VerbosityManager } from "./verbosity.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// VerbosityManager // VerbosityManager

View file

@ -34,7 +34,7 @@ const DEFAULT_SHOWN: ReadonlySet<string> = new Set([
]); ]);
/** Event types shown only at verbose level. */ /** Event types shown only at verbose level. */
const VERBOSE_ONLY: ReadonlySet<string> = new Set([ const _VERBOSE_ONLY: ReadonlySet<string> = new Set([
"cost_update", "cost_update",
"state_update", "state_update",
"status", "status",

View file

@ -5,8 +5,8 @@
* Cursor, and other MCP-compatible clients. * Cursor, and other MCP-compatible clients.
*/ */
import { SessionManager } from "./session-manager.js";
import { createMcpServer } from "./server.js"; import { createMcpServer } from "./server.js";
import { SessionManager } from "./session-manager.js";
import { loadStoredCredentialEnvKeys } from "./tool-credentials.js"; import { loadStoredCredentialEnvKeys } from "./tool-credentials.js";
const MCP_PKG = "@modelcontextprotocol/sdk"; const MCP_PKG = "@modelcontextprotocol/sdk";

View file

@ -1,5 +1,5 @@
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, it } from "vitest";
import { z } from "zod"; import { z } from "zod";
import { validateToolArguments } from "../../pi-ai/src/utils/validation.ts"; import { validateToolArguments } from "../../pi-ai/src/utils/validation.ts";

View file

@ -1,29 +1,29 @@
// @singularity-forge/mcp-server — Tests for env-writer utilities // @singularity-forge/mcp-server — Tests for env-writer utilities
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net> // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { describe, it, afterEach } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { import {
mkdtempSync,
mkdirSync, mkdirSync,
rmSync, mkdtempSync,
writeFileSync,
readFileSync, readFileSync,
realpathSync, realpathSync,
rmSync,
symlinkSync, symlinkSync,
writeFileSync,
} from "node:fs"; } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { afterEach, describe, it } from "vitest";
import { import {
applySecrets,
checkExistingEnvKeys, checkExistingEnvKeys,
detectDestination, detectDestination,
writeEnvKey,
applySecrets,
isSafeEnvVarKey, isSafeEnvVarKey,
isSupportedDeploymentEnvironment, isSupportedDeploymentEnvironment,
resolveProjectEnvFilePath, resolveProjectEnvFilePath,
shellEscapeSingle, shellEscapeSingle,
writeEnvKey,
} from "./env-writer.js"; } from "./env-writer.js";
function makeTempDir(prefix: string): string { function makeTempDir(prefix: string): string {

View file

@ -5,7 +5,6 @@
// destinations, and checking existing keys. Used by secure_env_collect // destinations, and checking existing keys. Used by secure_env_collect
// MCP tool. No TUI dependencies — pure filesystem + process.env operations. // MCP tool. No TUI dependencies — pure filesystem + process.env operations.
import { open, readFile, rename, rm } from "node:fs/promises";
import { import {
constants, constants,
existsSync, existsSync,
@ -13,6 +12,7 @@ import {
realpathSync, realpathSync,
statSync, statSync,
} from "node:fs"; } from "node:fs";
import { open, readFile, rename, rm } from "node:fs/promises";
import { import {
basename, basename,
dirname, dirname,

View file

@ -1,6 +1,7 @@
// SF — Regression tests for importLocalModule candidate resolution (#3954) // SF — Regression tests for importLocalModule candidate resolution (#3954)
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, it } from "vitest";
import { _buildImportCandidates } from "./workflow-tools.js"; import { _buildImportCandidates } from "./workflow-tools.js";

View file

@ -10,23 +10,17 @@
* 4. Testing CLI path resolution via static method * 4. Testing CLI path resolution via static method
*/ */
import { describe, it, beforeEach, afterEach } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { resolve } from "node:path"; import { resolve } from "node:path";
import { EventEmitter } from "node:events"; import { afterEach, beforeEach, describe, it } from "vitest";
import { SessionManager } from "./session-manager.js";
import { import {
buildAskUserQuestionsElicitRequest, buildAskUserQuestionsElicitRequest,
createMcpServer, createMcpServer,
formatAskUserQuestionsElicitResult, formatAskUserQuestionsElicitResult,
} from "./server.js"; } from "./server.js";
import { SessionManager } from "./session-manager.js";
import type { ManagedSession } from "./types.js";
import { MAX_EVENTS } from "./types.js"; import { MAX_EVENTS } from "./types.js";
import type {
ManagedSession,
CostAccumulator,
PendingBlocker,
} from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Mock RpcClient (duck-typed to match RpcClient interface) // Mock RpcClient (duck-typed to match RpcClient interface)

View file

@ -1,8 +1,8 @@
// SF MCP Server — captures reader // SF MCP Server — captures reader
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net> // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { readFileSync, existsSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { resolveSFRoot, resolveRootFile } from "./paths.js"; import { resolveRootFile, resolveSFRoot } from "./paths.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types

View file

@ -1,16 +1,16 @@
// SF MCP Server — lightweight structural health checks // SF MCP Server — lightweight structural health checks
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net> // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { existsSync, readFileSync } from "node:fs"; import { existsSync } from "node:fs";
import { import {
resolveSFRoot,
resolveRootFile,
findMilestoneIds, findMilestoneIds,
resolveMilestoneFile,
resolveMilestoneDir,
findSliceIds, findSliceIds,
resolveSliceFile,
findTaskFiles, findTaskFiles,
resolveMilestoneDir,
resolveMilestoneFile,
resolveRootFile,
resolveSFRoot,
resolveSliceFile,
} from "./paths.js"; } from "./paths.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -1,8 +1,8 @@
// SF MCP Server — knowledge base reader // SF MCP Server — knowledge base reader
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net> // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { readFileSync, existsSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { resolveSFRoot, resolveRootFile } from "./paths.js"; import { resolveRootFile, resolveSFRoot } from "./paths.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types

View file

@ -1,8 +1,8 @@
// SF MCP Server — metrics/history reader // SF MCP Server — metrics/history reader
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net> // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { readFileSync, existsSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { resolveSFRoot, resolveRootFile } from "./paths.js"; import { resolveRootFile, resolveSFRoot } from "./paths.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Types // Types

View file

@ -1,9 +1,9 @@
// SF MCP Server — .sf/ directory resolution // SF MCP Server — .sf/ directory resolution
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net> // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { existsSync, statSync, readdirSync } from "node:fs";
import { join, resolve, dirname, basename } from "node:path";
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import { existsSync, readdirSync, statSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
/** /**
* Resolve the .sf/ root directory for a project. * Resolve the .sf/ root directory for a project.

View file

@ -1,19 +1,18 @@
// SF MCP Server — reader tests // SF MCP Server — reader tests
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net> // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { describe, it, beforeAll, afterAll } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { readProgress } from "./state.js"; import { tmpdir } from "node:os";
import { readRoadmap } from "./roadmap.js"; import { join } from "node:path";
import { readHistory } from "./metrics.js"; import { afterAll, beforeAll, describe, it } from "vitest";
import { readCaptures } from "./captures.js"; import { readCaptures } from "./captures.js";
import { readKnowledge } from "./knowledge.js";
import { runDoctorLite } from "./doctor-lite.js"; import { runDoctorLite } from "./doctor-lite.js";
import { readKnowledge } from "./knowledge.js";
import { readHistory } from "./metrics.js";
import { readRoadmap } from "./roadmap.js";
import { readProgress } from "./state.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Test fixture helpers // Test fixture helpers

View file

@ -1,14 +1,14 @@
// SF MCP Server — roadmap structure reader // SF MCP Server — roadmap structure reader
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net> // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { readFileSync, existsSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { import {
resolveSFRoot,
findMilestoneIds, findMilestoneIds,
resolveMilestoneFile,
findSliceIds, findSliceIds,
resolveSliceFile,
findTaskFiles, findTaskFiles,
resolveMilestoneFile,
resolveSFRoot,
resolveSliceFile,
} from "./paths.js"; } from "./paths.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -1,15 +1,13 @@
// SF MCP Server — project state reader // SF MCP Server — project state reader
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net> // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
import { readFileSync, existsSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { import {
resolveSFRoot,
resolveRootFile,
findMilestoneIds, findMilestoneIds,
resolveMilestoneDir,
resolveMilestoneFile,
findSliceIds, findSliceIds,
findTaskFiles, findTaskFiles,
resolveRootFile,
resolveSFRoot,
} from "./paths.js"; } from "./paths.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -2,91 +2,18 @@
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net> // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
// //
// Tests the secure_env_collect tool registered in createMcpServer. // Tests the secure_env_collect tool registered in createMcpServer.
// Uses a mock MCP server to intercept tool registration and elicitInput calls.
import { describe, it, beforeEach } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
mkdtempSync,
mkdirSync,
rmSync,
writeFileSync,
readFileSync,
} from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { describe, it } from "vitest";
import { createMcpServer } from "./server.js"; import { createMcpServer } from "./server.js";
import { SessionManager } from "./session-manager.js"; import { SessionManager } from "./session-manager.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Mock infrastructure // Test helpers
// ---------------------------------------------------------------------------
/**
* We intercept McpServer construction by monkey-patching the dynamic import.
* Instead, we'll test the tool handler indirectly through the exported
* createMcpServer function capturing the registered tool handlers.
*
* Since createMcpServer dynamically imports McpServer, we need to test at
* a level that exercises the tool handler logic. We do this by extracting
* the tool handler through the server.tool() calls.
*/
interface RegisteredTool {
name: string;
description: string;
params: Record<string, unknown>;
handler: (args: Record<string, unknown>) => Promise<unknown>;
}
interface ToolResult {
content?: Array<{ type: string; text: string }>;
isError?: boolean;
}
/**
* Mock McpServer that captures tool registrations and provides
* a controllable elicitInput response.
*/
class MockMcpServer {
registeredTools: RegisteredTool[] = [];
elicitResponse: { action: string; content?: Record<string, unknown> } = {
action: "accept",
content: {},
};
server = {
elicitInput: async (_params: unknown) => {
return this.elicitResponse;
},
};
tool(
name: string,
description: string,
params: Record<string, unknown>,
handler: (args: Record<string, unknown>) => Promise<unknown>,
) {
this.registeredTools.push({ name, description, params, handler });
}
async connect(_transport: unknown) {
/* no-op */
}
async close() {
/* no-op */
}
getToolHandler(
name: string,
): ((args: Record<string, unknown>) => Promise<unknown>) | undefined {
return this.registeredTools.find((t) => t.name === name)?.handler;
}
}
// ---------------------------------------------------------------------------
// Helper to create a mock MCP server with secure_env_collect registered
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/** /**

View file

@ -8,21 +8,19 @@
import { execSync } from "node:child_process"; import { execSync } from "node:child_process";
import { resolve } from "node:path"; import { resolve } from "node:path";
import { RpcClient } from "@singularity-forge/rpc-client";
import type { import type {
SdkAgentEvent,
RpcInitResult,
RpcCostUpdateEvent, RpcCostUpdateEvent,
RpcExtensionUIRequest, RpcExtensionUIRequest,
RpcInitResult,
SdkAgentEvent,
} from "@singularity-forge/rpc-client"; } from "@singularity-forge/rpc-client";
import { RpcClient } from "@singularity-forge/rpc-client";
import type { import type {
ManagedSession,
ExecuteOptions, ExecuteOptions,
ManagedSession,
PendingBlocker, PendingBlocker,
CostAccumulator,
SessionStatus,
} from "./types.js"; } from "./types.js";
import { MAX_EVENTS, INIT_TIMEOUT_MS } from "./types.js"; import { INIT_TIMEOUT_MS, MAX_EVENTS } from "./types.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Inlined detection logic (from headless-events.ts — no internal package imports) // Inlined detection logic (from headless-events.ts — no internal package imports)

View file

@ -1,8 +1,8 @@
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { describe, it } from "vitest";
import { import {
loadStoredCredentialEnvKeys, loadStoredCredentialEnvKeys,

View file

@ -4,9 +4,8 @@
import type { import type {
RpcClient, RpcClient,
SdkAgentEvent,
RpcCostUpdateEvent,
RpcExtensionUIRequest, RpcExtensionUIRequest,
SdkAgentEvent,
} from "@singularity-forge/rpc-client"; } from "@singularity-forge/rpc-client";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -1,8 +1,8 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,8 +1,8 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,10 +1,10 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import * as fs from "node:fs";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import * as fs from "node:fs"; import { describe, test } from "vitest";
import * as os from "node:os";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,10 +1,10 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import * as fs from "node:fs";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import * as fs from "node:fs"; import { describe, test } from "vitest";
import * as os from "node:os";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,10 +1,10 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import * as fs from "node:fs";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import * as fs from "node:fs"; import { describe, test } from "vitest";
import * as os from "node:os";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,10 +1,10 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import * as fs from "node:fs";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import * as fs from "node:fs"; import { describe, test } from "vitest";
import * as os from "node:os";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,8 +1,8 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,8 +1,8 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,9 +1,9 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { deflateSync } from "node:zlib"; import { deflateSync } from "node:zlib";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,8 +1,8 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -7,11 +7,11 @@
* declared "type": "module" and strict ESM resolution was enforced. * declared "type": "module" and strict ESM resolution was enforced.
*/ */
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkgPath = path.resolve(__dirname, "..", "..", "package.json"); const pkgPath = path.resolve(__dirname, "..", "..", "package.json");

View file

@ -1,9 +1,9 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { spawn } from "node:child_process"; import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,5 +1,5 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, test } from "vitest";
import { processStreamChunk } from "../stream-process/index.ts"; import { processStreamChunk } from "../stream-process/index.ts";
describe("processStreamChunk", () => { describe("processStreamChunk", () => {

View file

@ -10,13 +10,13 @@
* npx vitest run packages/native/src/__tests__/symbol.test.mjs --config vitest.config.ts * npx vitest run packages/native/src/__tests__/symbol.test.mjs --config vitest.config.ts
*/ */
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import * as fs from "node:fs";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as os from "node:os";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import * as fs from "node:fs"; import { describe, test } from "vitest";
import * as os from "node:os";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
@ -147,14 +147,14 @@ describe("native symbol: replaceSymbol()", () => {
test("replaces an arrow function declaration", ({ onTestFinished }) => { test("replaces an arrow function declaration", ({ onTestFinished }) => {
const { dir, file } = tempTsFile( const { dir, file } = tempTsFile(
"const greet = (name: string) => { return `Hello ${name}`; }\n", "const greet = (name: string) => { return `Hello " + "$" + "{name}`; }\n",
); );
onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true })); onTestFinished(() => fs.rmSync(dir, { recursive: true, force: true }));
const result = native.replaceSymbol( const result = native.replaceSymbol(
file, file,
"greet", "greet",
"const greet = (name: string) => { return `Hi ${name}!`; }", "const greet = (name: string) => { return `Hi " + "$" + "{name}!`; }",
{ fsync: false }, { fsync: false },
); );

View file

@ -1,8 +1,8 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,8 +1,8 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,8 +1,8 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import * as path from "node:path"; import * as path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);

View file

@ -1,9 +1,9 @@
import { describe, test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import * as fs from "node:fs"; import * as fs from "node:fs";
import * as os from "node:os"; import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, test } from "vitest";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const { watchTree } = await import( const { watchTree } = await import(

View file

@ -1,6 +1,6 @@
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { xxHash32, xxHash32Fallback } from "@singularity-forge/native/xxhash"; import { xxHash32, xxHash32Fallback } from "@singularity-forge/native/xxhash";
import { describe, it } from "vitest";
/** /**
* Reference values computed from the pure-JS xxHash32 implementation * Reference values computed from the pure-JS xxHash32 implementation

View file

@ -1,12 +1,12 @@
import { native } from "../native.js"; import { native } from "../native.js";
import type { import type {
AstFindMatch,
AstFindOptions, AstFindOptions,
AstFindResult, AstFindResult,
AstReplaceOptions,
AstReplaceResult,
AstFindMatch,
AstReplaceChange, AstReplaceChange,
AstReplaceFileChange, AstReplaceFileChange,
AstReplaceOptions,
AstReplaceResult,
} from "./types.js"; } from "./types.js";
export type { export type {
@ -20,11 +20,13 @@ export type {
}; };
export function astGrep(options: AstFindOptions): AstFindResult { export function astGrep(options: AstFindOptions): AstFindResult {
return (native as Record<string, Function>).astGrep(options) as AstFindResult; return (native as Record<string, (...args: unknown[]) => unknown>).astGrep(
options,
) as AstFindResult;
} }
export function astEdit(options: AstReplaceOptions): AstReplaceResult { export function astEdit(options: AstReplaceOptions): AstReplaceResult {
return (native as Record<string, Function>).astEdit( return (native as Record<string, (...args: unknown[]) => unknown>).astEdit(
options, options,
) as AstReplaceResult; ) as AstReplaceResult;
} }

View file

@ -18,9 +18,9 @@ export type { DiffResult, FuzzyMatchResult };
* - Special Unicode spaces to regular space * - Special Unicode spaces to regular space
*/ */
export function normalizeForFuzzyMatch(text: string): string { export function normalizeForFuzzyMatch(text: string): string {
return (native as Record<string, Function>).normalizeForFuzzyMatch( return (
text, native as Record<string, (...args: unknown[]) => unknown>
) as string; ).normalizeForFuzzyMatch(text) as string;
} }
/** /**
@ -33,10 +33,9 @@ export function fuzzyFindText(
content: string, content: string,
oldText: string, oldText: string,
): FuzzyMatchResult { ): FuzzyMatchResult {
return (native as Record<string, Function>).fuzzyFindText( return (
content, native as Record<string, (...args: unknown[]) => unknown>
oldText, ).fuzzyFindText(content, oldText) as FuzzyMatchResult;
) as FuzzyMatchResult;
} }
/** /**
@ -53,9 +52,7 @@ export function generateDiff(
newContent: string, newContent: string,
contextLines?: number, contextLines?: number,
): DiffResult { ): DiffResult {
return (native as Record<string, Function>).generateDiff( return (
oldContent, native as Record<string, (...args: unknown[]) => unknown>
newContent, ).generateDiff(oldContent, newContent, contextLines) as DiffResult;
contextLines,
) as DiffResult;
} }

View file

@ -8,8 +8,8 @@
* need to commit them to disk as one native operation. * need to commit them to disk as one native operation.
*/ */
import { native } from "../native.js";
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
import { native } from "../native.js";
import type { import type {
ApplyEditsOptions, ApplyEditsOptions,
ApplyEditsResult, ApplyEditsResult,

View file

@ -43,9 +43,9 @@ export type {
* of the parsed frontmatter key-value pairs. Parse it with `JSON.parse()`. * of the parsed frontmatter key-value pairs. Parse it with `JSON.parse()`.
*/ */
export function parseFrontmatter(content: string): FrontmatterResult { export function parseFrontmatter(content: string): FrontmatterResult {
return (native as Record<string, Function>).parseFrontmatter( return (
content, native as Record<string, (...args: unknown[]) => unknown>
) as FrontmatterResult; ).parseFrontmatter(content) as FrontmatterResult;
} }
/** /**
@ -60,11 +60,9 @@ export function extractSection(
heading: string, heading: string,
level?: number, level?: number,
): SectionResult { ): SectionResult {
return (native as Record<string, Function>).extractSection( return (
content, native as Record<string, (...args: unknown[]) => unknown>
heading, ).extractSection(content, heading, level) as SectionResult;
level,
) as SectionResult;
} }
/** /**
@ -74,10 +72,9 @@ export function extractSection(
* Parse with `JSON.parse()`. * Parse with `JSON.parse()`.
*/ */
export function extractAllSections(content: string, level?: number): string { export function extractAllSections(content: string, level?: number): string {
return (native as Record<string, Function>).extractAllSections( return (
content, native as Record<string, (...args: unknown[]) => unknown>
level, ).extractAllSections(content, level) as string;
) as string;
} }
/** /**
@ -87,9 +84,9 @@ export function extractAllSections(content: string, level?: number): string {
* Each file gets frontmatter parsing and section extraction. * Each file gets frontmatter parsing and section extraction.
*/ */
export function batchParseSfFiles(directory: string): BatchParseResult { export function batchParseSfFiles(directory: string): BatchParseResult {
return (native as Record<string, Function>).batchParseSfFiles( return (
directory, native as Record<string, (...args: unknown[]) => unknown>
) as BatchParseResult; ).batchParseSfFiles(directory) as BatchParseResult;
} }
/** /**
@ -99,16 +96,16 @@ export function batchParseSfFiles(directory: string): BatchParseResult {
* and boundary map entries. * and boundary map entries.
*/ */
export function parseRoadmapFile(content: string): NativeRoadmap { export function parseRoadmapFile(content: string): NativeRoadmap {
return (native as Record<string, Function>).parseRoadmapFile( return (
content, native as Record<string, (...args: unknown[]) => unknown>
) as NativeRoadmap; ).parseRoadmapFile(content) as NativeRoadmap;
} }
/** /**
* Scan a `.sf/` directory tree. * Scan a `.sf/` directory tree.
*/ */
export function scanSfTree(directory: string): SfTreeEntry[] { export function scanSfTree(directory: string): SfTreeEntry[] {
return (native as Record<string, Function>).scanSfTree( return (native as Record<string, (...args: unknown[]) => unknown>).scanSfTree(
directory, directory,
) as SfTreeEntry[]; ) as SfTreeEntry[];
} }
@ -121,27 +118,25 @@ export function parseJsonlTail(
maxBytes?: number, maxBytes?: number,
maxEntries?: number, maxEntries?: number,
): JsonlParseResult { ): JsonlParseResult {
return (native as Record<string, Function>).parseJsonlTail( return (
filePath, native as Record<string, (...args: unknown[]) => unknown>
maxBytes, ).parseJsonlTail(filePath, maxBytes, maxEntries) as JsonlParseResult;
maxEntries,
) as JsonlParseResult;
} }
/** /**
* Parse a task plan markdown file into structured data. * Parse a task plan markdown file into structured data.
*/ */
export function parsePlanFile(content: string): NativePlan { export function parsePlanFile(content: string): NativePlan {
return (native as Record<string, Function>).parsePlanFile( return (
content, native as Record<string, (...args: unknown[]) => unknown>
) as NativePlan; ).parsePlanFile(content) as NativePlan;
} }
/** /**
* Parse a summary markdown file into structured data. * Parse a summary markdown file into structured data.
*/ */
export function parseSummaryFile(content: string): NativeSummary { export function parseSummaryFile(content: string): NativeSummary {
return (native as Record<string, Function>).parseSummaryFile( return (
content, native as Record<string, (...args: unknown[]) => unknown>
) as NativeSummary; ).parseSummaryFile(content) as NativeSummary;
} }

View file

@ -1,5 +1,5 @@
/** File type classification for filesystem entries. */ /** File type classification for filesystem entries. */
export const enum FileType { export enum FileType {
/** Regular file. */ /** Regular file. */
File = 1, File = 1,
/** Directory. */ /** Directory. */

View file

@ -8,8 +8,8 @@ import { native } from "../native.js";
import type { NativeImageHandle } from "./types.js"; import type { NativeImageHandle } from "./types.js";
import { ImageFormat, SamplingFilter } from "./types.js"; import { ImageFormat, SamplingFilter } from "./types.js";
export { ImageFormat, SamplingFilter };
export type { NativeImageHandle }; export type { NativeImageHandle };
export { ImageFormat, SamplingFilter };
const NativeImageClass = (native as Record<string, unknown>) const NativeImageClass = (native as Record<string, unknown>)
.NativeImage as NativeImageConstructor; .NativeImage as NativeImageConstructor;

View file

@ -14,47 +14,6 @@
for autocomplete and @-mention resolution for autocomplete and @-mention resolution
*/ */
export {
copyToClipboard,
readTextFromClipboard,
readImageFromClipboard,
} from "./clipboard/index.js";
export type { ClipboardImage } from "./clipboard/index.js";
export {
highlightCode,
supportsLanguage,
getSupportedLanguages,
} from "./highlight/index.js";
export type { HighlightColors } from "./highlight/index.js";
export { searchContent, grep } from "./grep/index.js";
export type {
ContextLine,
GrepMatch,
GrepOptions,
GrepResult,
SearchMatch,
SearchOptions,
SearchResult,
} from "./grep/index.js";
export {
killTree,
listDescendants,
processGroupId,
killProcessGroup,
} from "./ps/index.js";
export { glob, invalidateFsScanCache } from "./glob/index.js";
export type {
FileType,
GlobMatch,
GlobOptions,
GlobResult,
} from "./glob/index.js";
export { astGrep, astEdit } from "./ast/index.js";
export type { export type {
AstFindMatch, AstFindMatch,
AstFindOptions, AstFindOptions,
@ -64,35 +23,19 @@ export type {
AstReplaceOptions, AstReplaceOptions,
AstReplaceResult, AstReplaceResult,
} from "./ast/index.js"; } from "./ast/index.js";
export { astEdit, astGrep } from "./ast/index.js";
export { htmlToMarkdown } from "./html/index.js"; export type { ClipboardImage } from "./clipboard/index.js";
export type { HtmlToMarkdownOptions } from "./html/index.js";
export { export {
wrapTextWithAnsi, copyToClipboard,
truncateToWidth, readImageFromClipboard,
sliceWithWidth, readTextFromClipboard,
extractSegments, } from "./clipboard/index.js";
sanitizeText, export type { DiffResult, FuzzyMatchResult } from "./diff/index.js";
visibleWidth,
EllipsisKind,
} from "./text/index.js";
export type { SliceResult, ExtractSegmentsResult } from "./text/index.js";
export { export {
normalizeForFuzzyMatch,
fuzzyFindText, fuzzyFindText,
generateDiff, generateDiff,
normalizeForFuzzyMatch,
} from "./diff/index.js"; } from "./diff/index.js";
export type { FuzzyMatchResult, DiffResult } from "./diff/index.js";
export {
applyEdits,
applyWorkspaceEdit,
insertAroundSymbol,
replaceSymbol,
watchTree,
} from "./edit/index.js";
export type { export type {
ApplyEditsOptions, ApplyEditsOptions,
ApplyEditsResult, ApplyEditsResult,
@ -112,49 +55,19 @@ export type {
WatchOptions, WatchOptions,
WorkspaceEditLike, WorkspaceEditLike,
} from "./edit/index.js"; } from "./edit/index.js";
export {
export { fuzzyFind } from "./fd/index.js"; applyEdits,
applyWorkspaceEdit,
insertAroundSymbol,
replaceSymbol,
watchTree,
} from "./edit/index.js";
export type { export type {
FuzzyFindMatch, FuzzyFindMatch,
FuzzyFindOptions, FuzzyFindOptions,
FuzzyFindResult, FuzzyFindResult,
} from "./fd/index.js"; } from "./fd/index.js";
export { fuzzyFind } from "./fd/index.js";
export { parseImage, ImageFormat, SamplingFilter } from "./image/index.js";
export type { NativeImageHandle } from "./image/index.js";
export { xxHash32, xxHash32Fallback } from "./xxhash/index.js";
export {
ttsrCompileRules,
ttsrCheckBuffer,
ttsrFreeRules,
} from "./ttsr/index.js";
export type { TtsrHandle, TtsrRuleInput } from "./ttsr/index.js";
export {
parseJson,
parsePartialJson,
parseStreamingJson,
} from "./json-parse/index.js";
export {
processStreamChunk,
stripAnsiNative,
sanitizeBinaryOutputNative,
} from "./stream-process/index.js";
export type { StreamState, StreamChunkResult } from "./stream-process/index.js";
export {
parseFrontmatter,
extractSection,
extractSection as nativeExtractSection,
extractAllSections,
batchParseSfFiles,
parseRoadmapFile,
scanSfTree,
parseJsonlTail,
parsePlanFile,
parseSummaryFile,
} from "./forge-parser/index.js";
export type { export type {
BatchParseResult, BatchParseResult,
FrontmatterResult, FrontmatterResult,
@ -172,10 +85,83 @@ export type {
SectionResult, SectionResult,
SfTreeEntry, SfTreeEntry,
} from "./forge-parser/index.js"; } from "./forge-parser/index.js";
export { export {
truncateTail, batchParseSfFiles,
extractAllSections,
extractSection,
extractSection as nativeExtractSection,
parseFrontmatter,
parseJsonlTail,
parsePlanFile,
parseRoadmapFile,
parseSummaryFile,
scanSfTree,
} from "./forge-parser/index.js";
export type {
FileType,
GlobMatch,
GlobOptions,
GlobResult,
} from "./glob/index.js";
export { glob, invalidateFsScanCache } from "./glob/index.js";
export type {
ContextLine,
GrepMatch,
GrepOptions,
GrepResult,
SearchMatch,
SearchOptions,
SearchResult,
} from "./grep/index.js";
export { grep, searchContent } from "./grep/index.js";
export type { HighlightColors } from "./highlight/index.js";
export {
getSupportedLanguages,
highlightCode,
supportsLanguage,
} from "./highlight/index.js";
export type { HtmlToMarkdownOptions } from "./html/index.js";
export { htmlToMarkdown } from "./html/index.js";
export type { NativeImageHandle } from "./image/index.js";
export { ImageFormat, parseImage, SamplingFilter } from "./image/index.js";
export {
parseJson,
parsePartialJson,
parseStreamingJson,
} from "./json-parse/index.js";
export {
killProcessGroup,
killTree,
listDescendants,
processGroupId,
} from "./ps/index.js";
export type { StreamChunkResult, StreamState } from "./stream-process/index.js";
export {
processStreamChunk,
sanitizeBinaryOutputNative,
stripAnsiNative,
} from "./stream-process/index.js";
export type { ExtractSegmentsResult, SliceResult } from "./text/index.js";
export {
EllipsisKind,
extractSegments,
sanitizeText,
sliceWithWidth,
truncateToWidth,
visibleWidth,
wrapTextWithAnsi,
} from "./text/index.js";
export type { TruncateOutputResult, TruncateResult } from "./truncate/index.js";
export {
truncateHead, truncateHead,
truncateOutput, truncateOutput,
truncateTail,
} from "./truncate/index.js"; } from "./truncate/index.js";
export type { TruncateResult, TruncateOutputResult } from "./truncate/index.js"; export type { TtsrHandle, TtsrRuleInput } from "./ttsr/index.js";
export {
ttsrCheckBuffer,
ttsrCompileRules,
ttsrFreeRules,
} from "./ttsr/index.js";
export { xxHash32, xxHash32Fallback } from "./xxhash/index.js";

View file

@ -75,7 +75,7 @@ function loadNative(): Record<string, unknown> {
errors.push(`${devPath}: ${message}`); errors.push(`${devPath}: ${message}`);
} }
const details = errors.map((e) => ` - ${e}`).join("\n"); const _details = errors.map((e) => ` - ${e}`).join("\n");
const supportedPlatforms = Object.keys(platformPackageMap); const supportedPlatforms = Object.keys(platformPackageMap);
// Graceful fallback: on unsupported platforms (e.g., win32-arm64), return a // Graceful fallback: on unsupported platforms (e.g., win32-arm64), return a

View file

@ -38,10 +38,9 @@ export function processStreamChunk(
} }
: undefined; : undefined;
const result = (native as Record<string, Function>).processStreamChunk( const result = (
chunk, native as Record<string, (...args: unknown[]) => unknown>
napiState, ).processStreamChunk(chunk, napiState) as {
) as {
text: string; text: string;
state: { utf8Pending: Buffer; ansiPending: Buffer }; state: { utf8Pending: Buffer; ansiPending: Buffer };
}; };
@ -59,7 +58,9 @@ export function processStreamChunk(
* Strip ANSI escape sequences from a string. * Strip ANSI escape sequences from a string.
*/ */
export function stripAnsiNative(text: string): string { export function stripAnsiNative(text: string): string {
return (native as Record<string, Function>).stripAnsiNative(text) as string; return (
native as Record<string, (...args: unknown[]) => unknown>
).stripAnsiNative(text) as string;
} }
/** /**
@ -69,7 +70,7 @@ export function stripAnsiNative(text: string): string {
* characters, Unicode format characters (U+FFF9-U+FFFB), and lone surrogates. * characters, Unicode format characters (U+FFF9-U+FFFB), and lone surrogates.
*/ */
export function sanitizeBinaryOutputNative(text: string): string { export function sanitizeBinaryOutputNative(text: string): string {
return (native as Record<string, Function>).sanitizeBinaryOutputNative( return (
text, native as Record<string, (...args: unknown[]) => unknown>
) as string; ).sanitizeBinaryOutputNative(text) as string;
} }

View file

@ -8,8 +8,8 @@
import { native } from "../native.js"; import { native } from "../native.js";
import type { ExtractSegmentsResult, SliceResult } from "./types.js"; import type { ExtractSegmentsResult, SliceResult } from "./types.js";
export type { ExtractSegmentsResult, SliceResult };
export { EllipsisKind } from "./types.js"; export { EllipsisKind } from "./types.js";
export type { ExtractSegmentsResult, SliceResult };
/** /**
* Word-wrap text to a visible width, preserving ANSI escape codes across * Word-wrap text to a visible width, preserving ANSI escape codes across
@ -24,11 +24,9 @@ export function wrapTextWithAnsi(
width: number, width: number,
tabWidth?: number, tabWidth?: number,
): string[] { ): string[] {
return (native as Record<string, Function>).wrapTextWithAnsi( return (
text, native as Record<string, (...args: unknown[]) => unknown>
width, ).wrapTextWithAnsi(text, width, tabWidth) as string[];
tabWidth,
) as string[];
} }
/** /**
@ -47,13 +45,9 @@ export function truncateToWidth(
pad: boolean, pad: boolean,
tabWidth?: number, tabWidth?: number,
): string { ): string {
return (native as Record<string, Function>).truncateToWidth( return (
text, native as Record<string, (...args: unknown[]) => unknown>
maxWidth, ).truncateToWidth(text, maxWidth, ellipsisKind, pad, tabWidth) as string;
ellipsisKind,
pad,
tabWidth,
) as string;
} }
/** /**
@ -69,13 +63,9 @@ export function sliceWithWidth(
strict: boolean, strict: boolean,
tabWidth?: number, tabWidth?: number,
): SliceResult { ): SliceResult {
return (native as Record<string, Function>).sliceWithWidth( return (
line, native as Record<string, (...args: unknown[]) => unknown>
startCol, ).sliceWithWidth(line, startCol, length, strict, tabWidth) as SliceResult;
length,
strict,
tabWidth,
) as SliceResult;
} }
/** /**
@ -92,7 +82,9 @@ export function extractSegments(
strictAfter: boolean, strictAfter: boolean,
tabWidth?: number, tabWidth?: number,
): ExtractSegmentsResult { ): ExtractSegmentsResult {
return (native as Record<string, Function>).extractSegments( return (
native as Record<string, (...args: unknown[]) => unknown>
).extractSegments(
line, line,
beforeEnd, beforeEnd,
afterStart, afterStart,
@ -109,7 +101,9 @@ export function extractSegments(
* Returns the original string when no changes are needed (zero-copy). * Returns the original string when no changes are needed (zero-copy).
*/ */
export function sanitizeText(text: string): string { export function sanitizeText(text: string): string {
return (native as Record<string, Function>).sanitizeText(text) as string; return (
native as Record<string, (...args: unknown[]) => unknown>
).sanitizeText(text) as string;
} }
/** /**
@ -118,8 +112,7 @@ export function sanitizeText(text: string): string {
* Tabs count as `tabWidth` cells (default 3). * Tabs count as `tabWidth` cells (default 3).
*/ */
export function visibleWidth(text: string, tabWidth?: number): number { export function visibleWidth(text: string, tabWidth?: number): number {
return (native as Record<string, Function>).visibleWidth( return (
text, native as Record<string, (...args: unknown[]) => unknown>
tabWidth, ).visibleWidth(text, tabWidth) as number;
) as number;
} }

View file

@ -24,20 +24,18 @@ export interface TruncateOutputResult {
* Keep the first `maxBytes` worth of complete lines. * Keep the first `maxBytes` worth of complete lines.
*/ */
export function truncateTail(text: string, maxBytes: number): TruncateResult { export function truncateTail(text: string, maxBytes: number): TruncateResult {
return (native as Record<string, Function>).truncateTail( return (
text, native as Record<string, (...args: unknown[]) => unknown>
maxBytes, ).truncateTail(text, maxBytes) as TruncateResult;
) as TruncateResult;
} }
/** /**
* Keep the last `maxBytes` worth of complete lines. * Keep the last `maxBytes` worth of complete lines.
*/ */
export function truncateHead(text: string, maxBytes: number): TruncateResult { export function truncateHead(text: string, maxBytes: number): TruncateResult {
return (native as Record<string, Function>).truncateHead( return (
text, native as Record<string, (...args: unknown[]) => unknown>
maxBytes, ).truncateHead(text, maxBytes) as TruncateResult;
) as TruncateResult;
} }
/** /**
@ -48,9 +46,7 @@ export function truncateOutput(
maxBytes: number, maxBytes: number,
mode?: string, mode?: string,
): TruncateOutputResult { ): TruncateOutputResult {
return (native as Record<string, Function>).truncateOutput( return (
text, native as Record<string, (...args: unknown[]) => unknown>
maxBytes, ).truncateOutput(text, maxBytes, mode) as TruncateOutputResult;
mode,
) as TruncateOutputResult;
} }

View file

@ -3,16 +3,16 @@
// and that the footer reads activeInferenceModel instead of state.model. // and that the footer reads activeInferenceModel instead of state.model.
// Regression test for https://github.com/singularity-forge/sf-run/issues/1844 Bug 2 // Regression test for https://github.com/singularity-forge/sf-run/issues/1844 Bug 2
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { join, dirname } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { Agent } from "./agent.ts";
import { import {
getModel,
type AssistantMessageEventStream, type AssistantMessageEventStream,
getModel,
} from "@singularity-forge/pi-ai"; } from "@singularity-forge/pi-ai";
import { describe, it } from "vitest";
import { Agent } from "./agent.ts";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));

View file

@ -22,10 +22,6 @@ import type {
AgentMessage, AgentMessage,
AgentState, AgentState,
AgentTool, AgentTool,
BeforeToolCallContext,
BeforeToolCallResult,
AfterToolCallContext,
AfterToolCallResult,
StreamFn, StreamFn,
ThinkingLevel, ThinkingLevel,
} from "./types.js"; } from "./types.js";

View file

@ -91,7 +91,7 @@ export interface ProxyStreamOptions extends SimpleStreamOptions {
* }); * });
* ``` * ```
*/ */
function streamProxy( function _streamProxy(
model: Model<any>, model: Model<any>,
context: Context, context: Context,
options: ProxyStreamOptions, options: ProxyStreamOptions,

View file

@ -1,3 +1,4 @@
import type { Static, TSchema } from "@sinclair/typebox";
import type { import type {
AssistantMessage, AssistantMessage,
AssistantMessageEvent, AssistantMessageEvent,
@ -10,7 +11,6 @@ import type {
Tool, Tool,
ToolResultMessage, ToolResultMessage,
} from "@singularity-forge/pi-ai"; } from "@singularity-forge/pi-ai";
import type { Static, TSchema } from "@sinclair/typebox";
/** Stream function - can return sync or Promise for async config lookup */ /** Stream function - can return sync or Promise for async config lookup */
export type StreamFn = ( export type StreamFn = (
@ -255,9 +255,8 @@ export type ThinkingLevel =
* } * }
* ``` * ```
*/ */
export interface CustomAgentMessages { // biome-ignore lint/suspicious/noEmptyInterface: extension point for downstream declaration merging
// Empty by default - apps extend via declaration merging export interface CustomAgentMessages {}
}
/** /**
* AgentMessage: Union of LLM messages + custom messages. * AgentMessage: Union of LLM messages + custom messages.

View file

@ -1,8 +1,8 @@
#!/usr/bin/env tsx #!/usr/bin/env tsx
import { writeFileSync } from "fs"; import { writeFileSync } from "node:fs";
import { join } from "path"; import { join } from "node:path";
import { Api, KnownProvider, Model } from "../src/types.js"; import type { Api, KnownProvider, Model } from "../src/types.js";
const packageRoot = join(import.meta.dirname, ".."); const packageRoot = join(import.meta.dirname, "..");
@ -69,7 +69,7 @@ async function fetchOpenRouterModels(): Promise<Model<any>[]> {
if (!model.supported_parameters?.includes("tools")) continue; if (!model.supported_parameters?.includes("tools")) continue;
// Parse provider from model ID // Parse provider from model ID
let provider: KnownProvider = "openrouter"; const provider: KnownProvider = "openrouter";
let modelKey = model.id; let modelKey = model.id;
modelKey = model.id; // Keep full ID for OpenRouter modelKey = model.id; // Keep full ID for OpenRouter
@ -197,7 +197,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
const m = model as ModelsDevModel; const m = model as ModelsDevModel;
if (m.tool_call !== true) continue; if (m.tool_call !== true) continue;
let id = modelId; const id = modelId;
if (id.startsWith("ai21.jamba")) { if (id.startsWith("ai21.jamba")) {
// These models doesn't support tool use in streaming mode // These models doesn't support tool use in streaming mode
@ -568,7 +568,7 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
if (m.status === "deprecated") continue; if (m.status === "deprecated") continue;
// Claude 4.x models route to Anthropic Messages API // Claude 4.x models route to Anthropic Messages API
const isCopilotClaude4 = /^claude-(haiku|sonnet|opus)-4([.\-]|$)/.test( const isCopilotClaude4 = /^claude-(haiku|sonnet|opus)-4([.-]|$)/.test(
modelId, modelId,
); );
// gpt-5 models require responses API, others use completions // gpt-5 models require responses API, others use completions

View file

@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import { existsSync, readFileSync, writeFileSync } from "fs"; import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { createInterface } from "readline"; import { createInterface } from "node:readline";
import { getOAuthProvider, getOAuthProviders } from "./utils/oauth/index.js"; import { getOAuthProvider, getOAuthProviders } from "./utils/oauth/index.js";
import type { OAuthCredentials, OAuthProviderId } from "./utils/oauth/types.js"; import type { OAuthCredentials, OAuthProviderId } from "./utils/oauth/types.js";

View file

@ -37,6 +37,6 @@ export type {
OAuthProviderInterface, OAuthProviderInterface,
} from "./utils/oauth/types.js"; } from "./utils/oauth/types.js";
export * from "./utils/overflow.js"; export * from "./utils/overflow.js";
export * from "./utils/typebox-helpers.js";
export * from "./utils/repair-tool-json.js"; export * from "./utils/repair-tool-json.js";
export * from "./utils/typebox-helpers.js";
export * from "./utils/validation.js"; export * from "./utils/validation.js";

View file

@ -1,5 +1,5 @@
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, it } from "vitest";
import { MODELS } from "./models.generated.js"; import { MODELS } from "./models.generated.js";
import { getModel, getModels, getProviders } from "./models.js"; import { getModel, getModels, getProviders } from "./models.js";

View file

@ -1,11 +1,11 @@
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, it } from "vitest";
import { import {
getProviders,
getModels,
getModel,
supportsXhigh,
applyCapabilityPatches, applyCapabilityPatches,
getModel,
getModels,
getProviders,
supportsXhigh,
} from "./models.js"; } from "./models.js";
import type { Api, Model } from "./types.js"; import type { Api, Model } from "./types.js";
@ -178,7 +178,7 @@ describe("model registry — kimi-coding provider", () => {
const providers = getProviders(); const providers = getProviders();
assert.ok( assert.ok(
providers.includes("kimi-coding"), providers.includes("kimi-coding"),
`Expected \"kimi-coding\" in providers, got: ${providers.join(", ")}`, `Expected "kimi-coding" in providers, got: ${providers.join(", ")}`,
); );
}); });

View file

@ -1,5 +1,5 @@
import { MODELS } from "./models.generated.js";
import { CUSTOM_MODELS } from "./models.custom.js"; import { CUSTOM_MODELS } from "./models.custom.js";
import { MODELS } from "./models.generated.js";
import type { import type {
Api, Api,
KnownProvider, KnownProvider,

View file

@ -7,17 +7,16 @@
* Related: #4392 (opus-4-7 adaptive thinking not recognised on Bedrock) * Related: #4392 (opus-4-7 adaptive thinking not recognised on Bedrock)
* #4352 (pre-existing: only opus-4-6 / sonnet-4-6 whitelisted) * #4352 (pre-existing: only opus-4-6 / sonnet-4-6 whitelisted)
*/ */
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, it } from "vitest";
import {
supportsAdaptiveThinking,
mapThinkingLevelToEffort,
buildAdditionalModelRequestFields,
type BedrockOptions,
} from "./amazon-bedrock.js";
import type { Model } from "../types.js"; import type { Model } from "../types.js";
import {
type BedrockOptions,
buildAdditionalModelRequestFields,
mapThinkingLevelToEffort,
supportsAdaptiveThinking,
} from "./amazon-bedrock.js";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Helpers // Helpers

View file

@ -1,12 +1,12 @@
import { test } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { test } from "vitest";
import { import {
usesAnthropicBearerAuth,
resolveAnthropicBaseUrl, resolveAnthropicBaseUrl,
usesAnthropicBearerAuth,
} from "./anthropic.js"; } from "./anthropic.js";
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));

View file

@ -1,5 +1,5 @@
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, it } from "vitest";
import { convertTools, mapStopReason } from "./anthropic-shared.js"; import { convertTools, mapStopReason } from "./anthropic-shared.js";
const makeTool = (name: string) => const makeTool = (name: string) =>

View file

@ -33,6 +33,7 @@ import type {
/** API types that use the Anthropic Messages protocol */ /** API types that use the Anthropic Messages protocol */
export type AnthropicApi = "anthropic-messages" | "anthropic-vertex"; export type AnthropicApi = "anthropic-messages" | "anthropic-vertex";
import type { AssistantMessageEventStream } from "../utils/event-stream.js"; import type { AssistantMessageEventStream } from "../utils/event-stream.js";
import { parseAnthropicSSE } from "../utils/event-stream.js"; import { parseAnthropicSSE } from "../utils/event-stream.js";
import { parseStreamingJson } from "../utils/json-parse.js"; import { parseStreamingJson } from "../utils/json-parse.js";
@ -233,7 +234,7 @@ export function isTransientNetworkError(error: unknown): boolean {
export function extractRetryAfterMs( export function extractRetryAfterMs(
headers: Headers | { get(name: string): string | null }, headers: Headers | { get(name: string): string | null },
errorText = "", _errorText = "",
): number | undefined { ): number | undefined {
const normalizeDelay = (ms: number): number | undefined => const normalizeDelay = (ms: number): number | undefined =>
ms > 0 ? Math.ceil(ms + 1000) : undefined; ms > 0 ? Math.ceil(ms + 1000) : undefined;

View file

@ -10,18 +10,18 @@ import type {
StreamFunction, StreamFunction,
} from "../types.js"; } from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js";
import {
adjustMaxTokensForThinking,
buildBaseOptions,
isAutoReasoning,
resolveReasoningLevel,
} from "./simple-options.js";
import { import {
type AnthropicOptions, type AnthropicOptions,
mapThinkingLevelToEffort, mapThinkingLevelToEffort,
processAnthropicStream, processAnthropicStream,
supportsAdaptiveThinking, supportsAdaptiveThinking,
} from "./anthropic-shared.js"; } from "./anthropic-shared.js";
import {
adjustMaxTokensForThinking,
buildBaseOptions,
isAutoReasoning,
resolveReasoningLevel,
} from "./simple-options.js";
let _AnthropicVertexClass: typeof AnthropicVertex | undefined; let _AnthropicVertexClass: typeof AnthropicVertex | undefined;
let _AnthropicSdkClass: typeof Anthropic | undefined; let _AnthropicSdkClass: typeof Anthropic | undefined;

View file

@ -9,7 +9,14 @@ import type {
StreamFunction, StreamFunction,
} from "../types.js"; } from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js";
import {
type AnthropicEffort,
type AnthropicOptions,
extractRetryAfterMs,
mapThinkingLevelToEffort,
processAnthropicStream,
supportsAdaptiveThinking,
} from "./anthropic-shared.js";
import { import {
buildCopilotDynamicHeaders, buildCopilotDynamicHeaders,
hasCopilotVisionInput, hasCopilotVisionInput,
@ -20,14 +27,6 @@ import {
isAutoReasoning, isAutoReasoning,
resolveReasoningLevel, resolveReasoningLevel,
} from "./simple-options.js"; } from "./simple-options.js";
import {
type AnthropicEffort,
type AnthropicOptions,
extractRetryAfterMs,
mapThinkingLevelToEffort,
processAnthropicStream,
supportsAdaptiveThinking,
} from "./anthropic-shared.js";
// Re-export types used by other modules // Re-export types used by other modules
export type { AnthropicEffort, AnthropicOptions }; export type { AnthropicEffort, AnthropicOptions };

View file

@ -1,5 +1,5 @@
import { describe, it } from "vitest";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { describe, it } from "vitest";
import { sanitizeSchemaForGoogle } from "./google-shared.js"; import { sanitizeSchemaForGoogle } from "./google-shared.js";
// ═══════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════

View file

@ -1,9 +1,9 @@
// Lazy-loaded: Google GenAI SDK is imported on first use, not at startup. // Lazy-loaded: Google GenAI SDK is imported on first use, not at startup.
// This avoids penalizing users who don't use Google Vertex models. // This avoids penalizing users who don't use Google Vertex models.
import type { GoogleGenAI } from "@google/genai";
import type { import type {
GenerateContentConfig, GenerateContentConfig,
GenerateContentParameters, GenerateContentParameters,
GoogleGenAI,
ThinkingConfig, ThinkingConfig,
} from "@google/genai"; } from "@google/genai";
import { calculateCost } from "../models.js"; import { calculateCost } from "../models.js";

View file

@ -15,6 +15,7 @@ async function getGoogleGenAIClass(): Promise<typeof GoogleGenAI> {
} }
return _GoogleGenAIClass; return _GoogleGenAIClass;
} }
import { getEnvApiKey } from "../env-api-keys.js"; import { getEnvApiKey } from "../env-api-keys.js";
import { calculateCost } from "../models.js"; import { calculateCost } from "../models.js";
import type { import type {

View file

@ -13,16 +13,16 @@ import type {
} from "../types.js"; } from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { parseStreamingJson } from "../utils/json-parse.js"; import { parseStreamingJson } from "../utils/json-parse.js";
import {
type CodexAppServerNotification,
getCodexAppServerClient,
} from "./codex-app-server-client.js";
import { convertResponsesMessages } from "./openai-responses-shared.js"; import { convertResponsesMessages } from "./openai-responses-shared.js";
import { import {
buildBaseOptions, buildBaseOptions,
clampReasoning, clampReasoning,
resolveReasoningLevel, resolveReasoningLevel,
} from "./simple-options.js"; } from "./simple-options.js";
import {
getCodexAppServerClient,
type CodexAppServerNotification,
} from "./codex-app-server-client.js";
export interface OpenAICodexResponsesOptions extends StreamOptions { export interface OpenAICodexResponsesOptions extends StreamOptions {
reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh"; reasoningEffort?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
@ -657,7 +657,7 @@ function readString(value: unknown): string | undefined {
function readErrorMessage(value: unknown): string | undefined { function readErrorMessage(value: unknown): string | undefined {
const object = asObject(value); const object = asObject(value);
const error = asObject(object?.error); const _error = asObject(object?.error);
return readNestedCodexErrorMessage(object) ?? readString(object?.message); return readNestedCodexErrorMessage(object) ?? readString(object?.message);
} }

View file

@ -15,7 +15,6 @@ import type { FunctionParameters } from "openai/resources/shared.js";
import { getEnvApiKey } from "../env-api-keys.js"; import { getEnvApiKey } from "../env-api-keys.js";
import { calculateCost, supportsXhigh } from "../models.js"; import { calculateCost, supportsXhigh } from "../models.js";
import type { import type {
AssistantMessage,
Context, Context,
ImageContent, ImageContent,
Message, Message,
@ -34,12 +33,6 @@ import type {
import { AssistantMessageEventStream } from "../utils/event-stream.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { parseStreamingJson } from "../utils/json-parse.js"; import { parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { sanitizeToolCallArgumentsForSerialization } from "./sanitize-tool-arguments.js";
import {
buildBaseOptions,
clampReasoning,
resolveReasoningLevel,
} from "./simple-options.js";
import { import {
assertStreamSuccess, assertStreamSuccess,
buildInitialOutput, buildInitialOutput,
@ -47,6 +40,12 @@ import {
finalizeStream, finalizeStream,
handleStreamError, handleStreamError,
} from "./openai-shared.js"; } from "./openai-shared.js";
import { sanitizeToolCallArgumentsForSerialization } from "./sanitize-tool-arguments.js";
import {
buildBaseOptions,
clampReasoning,
resolveReasoningLevel,
} from "./simple-options.js";
import { transformMessagesWithReport } from "./transform-messages.js"; import { transformMessagesWithReport } from "./transform-messages.js";
/** /**

Some files were not shown because too many files have changed in this diff Show more