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 committed 2026-09-02 17:30:18 +07:00
commit 5b8503fcd9
93 files changed
+12775

No files matched your search

+111
View File
@@ -0,0 +1,111 @@
import { tool } from 'ai';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { z } from 'zod';
import { BUILTIN_SKILLS } from './skills-builtin';
export type SkillOrigin = 'builtin' | 'user' | 'project';
export type Skill = {
name: string;
description: string;
origin: SkillOrigin;
path?: string;
body: string;
};
const MAX_BODY = 20_000;
/**
* Minimal YAML frontmatter reader: `name` and `description` only.
* A real YAML parser would be a dependency for two string fields.
*/
export function parseSkill(source: string, origin: SkillOrigin, path?: string): Skill | undefined {
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(source.trimStart());
if (!match) return undefined;
const meta: Record<string, string> = {};
for (const line of match[1]!.split(/\r?\n/)) {
const kv = /^([A-Za-z_-]+)\s*:\s*(.*)$/.exec(line.trim());
if (kv) meta[kv[1]!.toLowerCase()] = kv[2]!.replace(/^["']|["']$/g, '').trim();
}
const name = meta['name'];
const description = meta['description'];
if (!name || !description) return undefined;
return { name, description, origin, ...(path ? { path } : {}), body: match[2]!.trim().slice(0, MAX_BODY) };
}
const skillDirs = (cwd: string) => [
{ dir: join(process.env['SHIRO_HOME'] ?? homedir(), '.shiro-neko', 'skills'), origin: 'user' as const },
{ dir: join(cwd, '.shiro', 'skills'), origin: 'project' as const },
];
/**
* Builtin, then user, then project. Later wins, so a project can override a
* bundled skill by using the same name.
*/
export async function loadSkills(cwd = process.cwd()): Promise<Skill[]> {
const byName = new Map<string, Skill>();
for (const { name, source } of BUILTIN_SKILLS) {
const skill = parseSkill(source, 'builtin');
if (skill) byName.set(skill.name, skill);
else byName.delete(name);
}
for (const { dir, origin } of skillDirs(cwd)) {
let files: string[] = [];
try {
for await (const f of new Bun.Glob('*.md').scan({ cwd: dir, onlyFiles: true })) files.push(f);
} catch {
continue;
}
for (const file of files.sort()) {
const path = join(dir, file);
try {
const skill = parseSkill(await Bun.file(path).text(), origin, path);
if (skill) byName.set(skill.name, skill);
} catch {
continue;
}
}
}
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
}
/**
* Catalogue for the system prompt: names and one-line descriptions only.
* Bodies stay out of context until the model asks, which is the point.
*/
export function renderSkills(skills: Skill[]): string {
if (skills.length === 0) return '';
const lines = skills.map((s) => `- ${s.name}: ${s.description}`);
return [
'',
'Skills available through the skill tool. Load one when its description matches the task,',
'before you start working, and follow it as if the user had written it:',
...lines,
].join('\n');
}
export function createSkillTool(skills: Skill[]) {
const names = skills.map((s) => s.name);
return tool({
description:
'Load a skill: detailed instructions for one kind of task. Call it as soon as a skill description matches ' +
`what you are about to do, then follow what it says. Available: ${names.join(', ') || 'none'}.`,
inputSchema: z.object({
name: z.string().describe('Skill name from the list in your instructions'),
}),
execute: async ({ name }) => {
const skill = skills.find((s) => s.name === name.trim().toLowerCase());
if (!skill) throw new Error(`No skill named "${name}". Available: ${names.join(', ') || 'none'}`);
return `Skill "${skill.name}" (${skill.origin}). Follow these instructions for this task.\n\n${skill.body}`;
},
});
}
export { skillDirs };