Initial commit: shiro-neko 0.1.0-beta.1

Agentic coding CLI on Bun, Ink, and the AI SDK.

Core: streamText loop with SDK-level tool approval so a denied call provably never executes; endpoint fallback for OpenAI reasoning models; retry with backoff.

Tools: read/write/edit/glob/grep/bash, path-jailed, gitignore-aware, ripgrep with a JS fallback, binary rejection, live bash streaming.

Agents: five variants crossing thinking level with tool restriction; plan and review withhold mutating tools from the model.

Extensibility: frontmatter skills with on-demand bodies, plugin host with blocking hooks, MCP stdio and HTTP, read-only subagents.

State: durable per-project memory, session task lists, session persistence, compaction that repairs provider-item dependencies.

Distribution: five-platform cross-compiled binaries with checksums, install scripts, CI on three operating systems.

404 tests, typecheck clean.
This commit is contained in:
Muhammad Zakir Ramadhan
2026-09-02 17:30:18 +07:00
commit 5b8503fcd9
93 changed files with 12775 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
import type { ToolSet } from 'ai';
export type ToolCallContext = {
toolName: string;
input: unknown;
cwd: string;
};
/** Returning a string blocks the call; the string is handed to the model as the reason. */
export type BeforeToolCall = (ctx: ToolCallContext) => string | undefined | Promise<string | undefined>;
export type Plugin = {
name: string;
description: string;
/** Extra tools contributed by this plugin. */
tools?: ToolSet;
/** Tool names that should never prompt for approval. */
autoApprove?: readonly string[];
beforeToolCall?: BeforeToolCall;
afterTurn?: () => void | Promise<void>;
/** Text appended to the system prompt. */
appendix?: string;
};
export type PluginHost = {
plugins: Plugin[];
tools: ToolSet;
autoApprove: string[];
appendix: string;
/** Runs every beforeToolCall hook; the first block wins. */
guard: BeforeToolCall;
afterTurn: () => Promise<void>;
errors: { plugin: string; message: string }[];
};
export function createHost(plugins: Plugin[], errors: PluginHost['errors'] = []): PluginHost {
const tools: ToolSet = {};
const autoApprove: string[] = [];
const appendices: string[] = [];
for (const p of plugins) {
for (const [name, t] of Object.entries(p.tools ?? {})) tools[name] = t;
autoApprove.push(...(p.autoApprove ?? []));
if (p.appendix) appendices.push(p.appendix);
}
return {
plugins,
tools,
autoApprove,
appendix: appendices.length > 0 ? `\n${appendices.join('\n')}` : '',
errors,
guard: async (ctx) => {
for (const p of plugins) {
if (!p.beforeToolCall) continue;
try {
const blocked = await p.beforeToolCall(ctx);
if (blocked) return `Blocked by the ${p.name} plugin: ${blocked}`;
} catch (e) {
// A broken hook must not take the agent down, but it must not silently
// allow the call either: treat a throwing guard as a block.
return `The ${p.name} plugin failed while checking this call: ${e instanceof Error ? e.message : String(e)}`;
}
}
return undefined;
},
afterTurn: async () => {
for (const p of plugins) {
try {
await p.afterTurn?.();
} catch {
continue;
}
}
},
};
}