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
+143
View File
@@ -0,0 +1,143 @@
import { Text, useInput } from 'ink';
import React, { useEffect, useState } from 'react';
export type PromptInputProps = {
value: string;
onChange: (value: string) => void;
onSubmit: (value: string) => void;
placeholder?: string;
focus?: boolean;
mask?: string;
/** Newest-last list of previously submitted prompts, walked by up/down. */
history?: readonly string[];
/** Intercept a key before the input consumes it. Return true to swallow it. */
onKey?: (input: string, key: KeyLike) => boolean;
};
type KeyLike = {
upArrow: boolean;
downArrow: boolean;
leftArrow: boolean;
rightArrow: boolean;
return: boolean;
escape: boolean;
tab: boolean;
backspace: boolean;
delete: boolean;
ctrl: boolean;
meta: boolean;
home?: boolean;
end?: boolean;
};
const INVERSE_ON = '\u001B[7m';
const INVERSE_OFF = '\u001B[27m';
const invert = (s: string) => `${INVERSE_ON}${s}${INVERSE_OFF}`;
/**
* Text input with a real cursor and shell-style history recall.
*
* ink-text-input cannot do this: it discards up/down before its own handler and
* only ever shrinks its internal cursor offset, so an externally driven value
* leaves the cursor stranded. Owning the cursor here also gives us home/end and
* ctrl-a/e/k/u/w for free.
*/
export function PromptInput({
value,
onChange,
onSubmit,
placeholder = '',
focus = true,
mask,
history = [],
onKey,
}: PromptInputProps) {
const [cursor, setCursor] = useState(value.length);
// -1 means "editing a fresh line"; 0+ indexes back from the newest entry.
const [recall, setRecall] = useState(-1);
const [stash, setStash] = useState('');
useEffect(() => {
setCursor((c) => Math.min(c, value.length));
}, [value]);
const set = (next: string, nextCursor = next.length) => {
onChange(next);
setCursor(Math.max(0, Math.min(nextCursor, next.length)));
};
useInput(
(input, key) => {
if (onKey?.(input, key as KeyLike)) return;
if (key.return) {
setRecall(-1);
setStash('');
setCursor(0);
onSubmit(value);
return;
}
if (key.upArrow || key.downArrow) {
if (history.length === 0) return;
if (key.upArrow) {
const next = Math.min(recall + 1, history.length - 1);
if (recall === -1) setStash(value);
setRecall(next);
set(history[history.length - 1 - next] ?? value);
} else {
const next = recall - 1;
setRecall(next);
set(next < 0 ? stash : (history[history.length - 1 - next] ?? ''));
}
return;
}
if (key.leftArrow) return setCursor((c) => Math.max(0, c - 1));
if (key.rightArrow) return setCursor((c) => Math.min(value.length, c + 1));
if (key.home || (key.ctrl && input === 'a')) return setCursor(0);
if (key.end || (key.ctrl && input === 'e')) return setCursor(value.length);
if (key.ctrl && input === 'k') return set(value.slice(0, cursor), cursor);
if (key.ctrl && input === 'u') return set(value.slice(cursor), 0);
if (key.ctrl && input === 'w') {
const upto = value.slice(0, cursor);
const trimmed = upto.replace(/\S+\s*$/, '');
return set(trimmed + value.slice(cursor), trimmed.length);
}
if (key.backspace || key.delete) {
if (cursor === 0) return;
return set(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
}
// Ignore remaining control sequences; a paste arrives as one multi-char input.
if (!input || key.tab || key.escape || key.meta || key.ctrl) return;
set(value.slice(0, cursor) + input + value.slice(cursor), cursor + input.length);
},
{ isActive: focus },
);
if (value.length === 0) {
if (!placeholder) return <Text>{focus ? invert(' ') : ' '}</Text>;
return (
<Text dimColor>
{focus ? invert(placeholder.slice(0, 1)) : placeholder.slice(0, 1)}
{placeholder.slice(1)}
</Text>
);
}
const shown = mask ? mask.repeat(value.length) : value;
if (!focus) return <Text>{shown}</Text>;
return (
<Text>
{shown.slice(0, cursor)}
{invert(shown.slice(cursor, cursor + 1) || ' ')}
{shown.slice(cursor + 1)}
</Text>
);
}
export type { KeyLike };