Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Follow these rules when editing code in this project.
- `src/cli.ts` — entry point. Ensures dirs + config exist, loads plugins, builds a `PluginContext` (`buildPluginContext`), and dispatches commands (`search`, `fetch`, `ask`, `setup`, `help`/`--help`/`-h`, `version`/`--version`). `search`, `fetch`, and `ask` are wired up via the async `handleSearch`/`handleFetch`/`handleAsk` helpers (awaited by `main`), each passing the context as the last arg to the selected plugin's `fn`. The `fetch` command prints the fetch plugin's output directly — the CLI doesn't dispatch a separate `parse` step, but a fetch plugin may itself run the configured parse plugin via `context.configuredPlugins.parse` (`builtin-brightdata-fetch`, `builtin-crawl4ai-fetch`, and `builtin-alterlab-fetch` do; `builtin-firecrawl-fetch` does only in its raw-HTML mode; `builtin-exa-fetch` returns content as-is). The `ask` command (`sibyl ask <url> <question>`) passes the URL as the ask plugin's first arg; analogously, an ask plugin may itself fetch that URL via `context.configuredPlugins.fetch` before answering (`builtin-ai-ask` does). The `setup` command is handled synchronously by `runSetup(rest)` (`src/setup-command.ts`) and uses neither plugins nor the `PluginContext` (see The `setup` command). `main` is exported and only auto-runs when the file is the actual CLI entry (`import.meta.url` vs `process.argv[1]` guard), so tests can import it without side effects.
- `src/setup.ts` — ensures `~/.sibyl` and `~/.sibyl/plugins` exist, and loads/creates/validates `~/.sibyl/config.json` (all on every invocation).
- `src/setup-command.ts` — implements the `setup` command (`runSetup`): installs the instructions doc into agent tools' global instruction files (see The `setup` command). Distinct from `setup.ts`, which is config bootstrap.
- `src/instructions.ts` — the bundled `SIBYL.md` content (`SIBYL_INSTRUCTIONS`) plus the import line, marker, and notice constants used by `setup-command.ts`. Inlined as a string constant (not read from disk) so it ships in `dist`.
- `src/instructions.ts` — the bundled `SIBYL.md` content (`buildSibylInstructions(subagentModel)`, which interpolates a per-target cheap model into the "run subagents with …" line) plus the import line, marker, and notice constants used by `setup-command.ts`. Inlined as a string builder (not read from disk) so it ships in `dist`.
- `src/plugin-loader.ts` — assembles the active plugin set: builtin plugins + external (on-disk) plugins; validates the external ones.
- `src/plugins/config.ts` — `getBuiltinPlugins()`, the in-repo builtin plugin registry.
- `src/utils.ts` — pure helpers: `isValidHttpUrl`, `stripSearchResultDatePrefix` (strips localized SERP date prefixes), `collapseBlankLines`, and the search-setting readers `getSearchResultsLimit` / `shouldShowSearchDescription` (see Conventions).
Expand Down Expand Up @@ -91,7 +91,7 @@ Shape: `SibylConfig` (`src/@types/sibyl-config.ts`) — `{ plugins: Partial<Reco
- `--antigravity` → embeds the content into `~/.gemini/GEMINI.md`.
- `--other <file>` (repeatable; `--other=<file>` also accepted) → embeds the content into an arbitrary file.

Two mechanisms: Claude and opencode **reference** a `SIBYL.md` file each target owns; Codex, Antigravity, and `--other` **embed** the content between `<!-- SIBYL:START -->` / `<!-- SIBYL:END -->` markers, followed by a do-not-edit notice line. Everything is idempotent and refreshes in place on re-run — doc files are overwritten, the marker block (and its trailing notice) is regex-replaced, and the `@SIBYL.md` line / opencode entry are added at most once. The content is inlined in `src/instructions.ts` (a bundled string constant) because the `tsc`-only build has no asset-copy step, so a raw `.md` would never reach `dist`.
Two mechanisms: Claude and opencode **reference** a `SIBYL.md` file each target owns; Codex, Antigravity, and `--other` **embed** the content between `<!-- SIBYL:START -->` / `<!-- SIBYL:END -->` markers, followed by a do-not-edit notice line. Everything is idempotent and refreshes in place on re-run — doc files are overwritten, the marker block (and its trailing notice) is regex-replaced, and the `@SIBYL.md` line / opencode entry are added at most once. The content is inlined in `src/instructions.ts` (a bundled string builder) because the `tsc`-only build has no asset-copy step, so a raw `.md` would never reach `dist`. Each target renders the doc with a cheap model for its own tool (`SUBAGENT_MODEL` in `setup-command.ts`: `claude` → Claude Haiku, `codex` → GPT-5 Mini, `antigravity` → Gemini Flash; `opencode` and `--other` use the generic "a cheap model").

## Conventions

Expand Down
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ Extensible and lightweight by design 🕷️

---

## Status

⚠️ Currently in development.

## Quickstart

Sibyl uses **SearXNG** for web search and **Crawl4AI** for webpage fetching by default. Both run
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@postapsis/sibyl",
"version": "0.1.0",
"version": "0.1.1",
"description": "Give your AI Agent the web, without the bloat — extensible and lightweight by design 🕷️",
"type": "module",
"packageManager": "pnpm@11.5.1",
Expand Down
56 changes: 30 additions & 26 deletions src/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,36 @@

// The bundled `SIBYL.md` instructions doc installed by `sibyl setup`. Inlined here (rather
// than read from disk) because the build is `tsc`-only with no asset-copy step, so a raw
// `.md` file would never reach `dist`. Assembled as a line array joined with newlines so the
// fenced code block below doesn't need its backticks escaped inside a template literal.
export const SIBYL_INSTRUCTIONS = [
"# Search the web using `sibyl`",
"Use `sibyl` CLI for searching google/fetching webpages.",
"",
"## Usage",
"```",
"Commands:",
" search <query> Search the web",
" fetch <url> Fetch the content of a URL",
" help Show this help",
" version Show version",
"",
"Examples:",
' sibyl search "react vite bootstrap"',
" sibyl fetch https://vite.dev/guide",
"```",
"",
"Always search the web first when installing a new library/framework to get the latest docs. Also search when you want to find out information/API DOC etc.",
"Search more often than not. Conduct multiple search and fetches using subagents (with Haiku) and then proceed.",
"Use your judgment when interpreting web search results. Do not rely only on the first result you find.",
"If the information is incomplete, unclear, or insufficient to make a confident decision, perform a few more searches or fetch additional sources before making changes.",
"",
"Collect all the necessary information thoroughly before proceeding with creating/editing code.",
].join("\n");
// `.md` file would never reach `dist`. Built per target so `subagentModel` names a cheap model
// the installing tool can actually run (Claude → Claude Haiku, Codex → GPT-5 Mini, etc.), rather than
// hardcoding one tool's model. Assembled as a line array joined with newlines so the fenced
// code block below doesn't need its backticks escaped inside a template literal.
export function buildSibylInstructions(subagentModel: string): string {
return [
"# Search the web using `sibyl`",
"Use `sibyl` CLI for searching google/fetching webpages.",
"",
"## Usage",
"```",
"Commands:",
" search <query> Search the web",
" fetch <url> Fetch the content of a URL",
" help Show this help",
" version Show version",
"",
"Examples:",
' sibyl search "react vite bootstrap"',
" sibyl fetch https://vite.dev/guide",
"```",
"",
"Always search the web first when installing a new library/framework to get the latest docs. Also search when you want to find out information/API DOC etc.",
`Search more often than not. Conduct multiple search and fetches using subagents (with ${subagentModel}) and then proceed.`,
"Use your judgment when interpreting web search results. Do not rely only on the first result you find.",
"If the information is incomplete, unclear, or insufficient to make a confident decision, perform a few more searches or fetch additional sources before making changes.",
"",
"Collect all the necessary information thoroughly before proceeding with creating/editing code.",
].join("\n");
}

// Filename for the standalone doc that reference-based targets (Claude, opencode) point at.
export const SIBYL_DOC_FILENAME = "SIBYL.md";
Expand Down
36 changes: 34 additions & 2 deletions src/setup-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
SIBYL_BLOCK_END,
SIBYL_BLOCK_NOTICE,
SIBYL_BLOCK_START,
SIBYL_INSTRUCTIONS,
buildSibylInstructions,
} from "./instructions.ts";

let home: string;
Expand Down Expand Up @@ -66,7 +66,7 @@ describe("claude target", () => {
it("writes SIBYL.md and the @SIBYL.md import, leaving other tools untouched", () => {
runSetup(["--claude"]);

expect(read(".claude", "SIBYL.md").trimEnd()).toBe(SIBYL_INSTRUCTIONS);
expect(read(".claude", "SIBYL.md").trimEnd()).toBe(buildSibylInstructions("Claude Haiku"));
expect(read(".claude", "CLAUDE.md")).toContain("@SIBYL.md");

expect(fs.existsSync(path.join(home, ".config", "opencode"))).toBe(false);
Expand Down Expand Up @@ -165,3 +165,35 @@ describe("embed targets", () => {
expect(content.split(SIBYL_BLOCK_NOTICE)).toHaveLength(2);
});
});

describe("subagent model per target", () => {
it("names Claude's cheap model (Claude Haiku) in the claude doc", () => {
runSetup(["--claude"]);
expect(read(".claude", "SIBYL.md")).toContain("subagents (with Claude Haiku)");
});

it("uses the generic phrasing for opencode (model-agnostic)", () => {
runSetup(["--opencode"]);
expect(read(".config", "opencode", "SIBYL.md")).toContain("subagents (with a cheap model)");
});

it("names GPT-5 Mini for codex and drops the Claude-specific Haiku", () => {
runSetup(["--codex"]);

const content = read(".codex", "AGENTS.md");
expect(content).toContain("subagents (with GPT-5 Mini)");
expect(content).not.toContain("Haiku");
});

it("names Gemini Flash for antigravity", () => {
runSetup(["--antigravity"]);
expect(read(".gemini", "GEMINI.md")).toContain("subagents (with Gemini Flash)");
});

it("uses the generic phrasing for --other targets", () => {
const target = path.join(home, "notes.md");

runSetup(["--other", target]);
expect(fs.readFileSync(target, "utf8")).toContain("subagents (with a cheap model)");
});
});
31 changes: 21 additions & 10 deletions src/setup-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,22 @@ import {
SIBYL_BLOCK_START,
SIBYL_DOC_FILENAME,
SIBYL_IMPORT_LINE,
SIBYL_INSTRUCTIONS,
buildSibylInstructions,
} from "./instructions.ts";

type SetupTarget = "claude" | "opencode" | "codex" | "antigravity";

// Cheap/fast model each target's agent should spawn subagents with, interpolated into the
// instructions doc so every tool references a model it can actually run. opencode is
// provider-agnostic (no canonical cheap model), so it falls back to the generic phrasing.
const GENERIC_SUBAGENT_MODEL = "a cheap model";
const SUBAGENT_MODEL: Record<SetupTarget, string> = {
claude: "Claude Haiku",
opencode: GENERIC_SUBAGENT_MODEL,
codex: "GPT-5 Mini",
antigravity: "Gemini Flash",
};

// Path opencode stores in its `instructions[]` array. Kept in tilde form (opencode expands
// `~`) so it stays portable, while the doc itself is written to the expanded path.
const OPENCODE_INSTRUCTION_REF = `~/.config/opencode/${SIBYL_DOC_FILENAME}`;
Expand All @@ -37,25 +48,25 @@ export function runSetup(args: string[]): void {
const home = os.homedir();

if (targets.has("claude")) {
writeDoc(path.join(home, ".claude", SIBYL_DOC_FILENAME));
writeDoc(path.join(home, ".claude", SIBYL_DOC_FILENAME), SUBAGENT_MODEL.claude);
addImportLine(path.join(home, ".claude", "CLAUDE.md"));
}

if (targets.has("opencode")) {
writeDoc(path.join(home, ".config", "opencode", SIBYL_DOC_FILENAME));
writeDoc(path.join(home, ".config", "opencode", SIBYL_DOC_FILENAME), SUBAGENT_MODEL.opencode);
addOpencodeInstruction(path.join(home, ".config", "opencode", "opencode.json"));
}

if (targets.has("codex")) {
embedBlock(path.join(home, ".codex", "AGENTS.md"));
embedBlock(path.join(home, ".codex", "AGENTS.md"), SUBAGENT_MODEL.codex);
}

if (targets.has("antigravity")) {
embedBlock(path.join(home, ".gemini", "GEMINI.md"));
embedBlock(path.join(home, ".gemini", "GEMINI.md"), SUBAGENT_MODEL.antigravity);
}

for (const other of otherPaths) {
embedBlock(path.resolve(other));
embedBlock(path.resolve(other), GENERIC_SUBAGENT_MODEL);
}
} catch (error) {
console.error(`Error running setup: ${error}`);
Expand Down Expand Up @@ -113,9 +124,9 @@ function usageError(message: string): never {
}

// Writes the standalone SIBYL.md doc, overwriting any existing copy (refresh in place).
function writeDoc(file: string): void {
function writeDoc(file: string, subagentModel: string): void {
ensureDir(file);
fs.writeFileSync(file, `${SIBYL_INSTRUCTIONS}\n`);
fs.writeFileSync(file, `${buildSibylInstructions(subagentModel)}\n`);
console.log(`Wrote instructions doc: ${file}`);
}

Expand Down Expand Up @@ -159,11 +170,11 @@ function addOpencodeInstruction(jsonPath: string): void {

// Embeds the instructions inside sentinel markers followed by a do-not-edit notice. On
// re-run the existing block (and its trailing notice) is replaced in place, not duplicated.
function embedBlock(file: string): void {
function embedBlock(file: string, subagentModel: string): void {
ensureDir(file);
const current = readIfExists(file);

const block = `${SIBYL_BLOCK_START}\n${SIBYL_INSTRUCTIONS}\n${SIBYL_BLOCK_END}\n${SIBYL_BLOCK_NOTICE}`;
const block = `${SIBYL_BLOCK_START}\n${buildSibylInstructions(subagentModel)}\n${SIBYL_BLOCK_END}\n${SIBYL_BLOCK_NOTICE}`;
const blockRe = new RegExp(
`${escapeRegExp(SIBYL_BLOCK_START)}[\\s\\S]*?${escapeRegExp(SIBYL_BLOCK_END)}` +
`(?:\\n${escapeRegExp(SIBYL_BLOCK_NOTICE)})?`,
Expand Down
Loading