81c3b1b44a
Adds `aish -p "<text>"` for Unix-pipeline composability:
tail app.log | aish -p "any anomalies?"
aish -p "summarize: $(curl -sS https://...)"
The flag bypasses repl.lua entirely. On invocation:
1. Stdin: when not a TTY, read to EOF and prepend to the prompt as a
fenced block. ffi.libc.isatty(0) gates the read so interactive
`aish -p "..."` (no pipe) doesn't hang.
2. Resolve config.models[config.default_model].
3. Stream broker.chat_stream replies to stdout; finalize with newline.
4. Exit 0 on success, 1 on broker error, 2 on arg / config error.
Behavior NOT in -p mode (kept simple per the issue's "no repl.lua
involvement"):
- No MCP, no tool loop, no Norris, no routing, no memory injection.
- "CMD:" lines in the reply are printed verbatim, NOT executed —
callers can grep / pipe them as they wish.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
4.3 KiB
Lua
138 lines
4.3 KiB
Lua
-- main.lua — entry point
|
|
-- Phase 0: arg parsing, config load, REPL start.
|
|
-- See docs/PHASE0.md §4, §10. -p one-shot mode lands per issue #4.
|
|
|
|
-- Make project modules and the vendored dkjson resolvable from the repo root.
|
|
-- Run aish with the repo root as cwd; PTY-relative resolution lands later.
|
|
package.path = "./?.lua;./vendor/?.lua;" .. package.path
|
|
|
|
local USAGE = [[
|
|
aish — AI-augmented conversational shell.
|
|
|
|
Usage:
|
|
luajit main.lua [--config <path>] [--help] -- interactive REPL
|
|
luajit main.lua -p "<prompt>" [--config <path>] -- one-shot, print + exit
|
|
|
|
In -p mode, if stdin is not a TTY it's read as additional context and
|
|
prepended to the prompt as a fenced block — composes with Unix pipes:
|
|
tail app.log | aish -p "any anomalies?"
|
|
|
|
Config resolution order (PHASE0.md §10):
|
|
1. --config <path>
|
|
2. $AISH_CONFIG
|
|
3. ~/.config/aish/config.lua
|
|
4. ./config.lua
|
|
]]
|
|
|
|
local function parse_args(argv)
|
|
local out = {}
|
|
local i = 1
|
|
while i <= #argv do
|
|
local a = argv[i]
|
|
if a == "--config" then
|
|
out.config = argv[i + 1]
|
|
i = i + 2
|
|
elseif a == "--help" or a == "-h" then
|
|
out.help = true
|
|
i = i + 1
|
|
elseif a == "-p" or a == "--prompt" then
|
|
out.prompt = argv[i + 1]
|
|
if not out.prompt then
|
|
io.stderr:write("aish: -p requires a prompt argument\n")
|
|
os.exit(2)
|
|
end
|
|
i = i + 2
|
|
else
|
|
io.stderr:write("aish: unrecognized argument: " .. a .. "\n")
|
|
os.exit(2)
|
|
end
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function load_config(opts)
|
|
-- --config is explicit: use exactly that path or fail. No silent fallback.
|
|
if opts.config then
|
|
local f = io.open(opts.config, "r")
|
|
if not f then
|
|
error("aish: --config " .. opts.config .. ": cannot open")
|
|
end
|
|
f:close()
|
|
return dofile(opts.config), opts.config
|
|
end
|
|
|
|
local home = os.getenv("HOME") or ""
|
|
local candidates = {}
|
|
local function push(p) if p and p ~= "" then candidates[#candidates + 1] = p end end
|
|
push(os.getenv("AISH_CONFIG"))
|
|
push(home .. "/.config/aish/config.lua")
|
|
push("./config.lua")
|
|
|
|
for _, path in ipairs(candidates) do
|
|
local f = io.open(path, "r")
|
|
if f then f:close(); return dofile(path), path end
|
|
end
|
|
error("aish: no config.lua found (tried: "
|
|
.. table.concat(candidates, ", ") .. ")")
|
|
end
|
|
|
|
-- One-shot mode: read non-TTY stdin (if any), compose prompt, stream
|
|
-- broker reply to stdout, exit. Bypasses repl.lua entirely — no REPL,
|
|
-- no MCP, no tool loop, no Norris. The model's reply is printed
|
|
-- verbatim (including any "CMD:" lines, which are NOT executed in
|
|
-- this mode by design — the user can pipe-grep them as they wish).
|
|
local function run_one_shot(config, user_prompt)
|
|
local libc = require("ffi.libc")
|
|
local broker = require("broker")
|
|
|
|
local composed = user_prompt
|
|
if not libc.isatty(0) then
|
|
local piped = io.read("*a") or ""
|
|
if piped ~= "" then
|
|
composed = "```\n" .. piped .. "\n```\n\n" .. user_prompt
|
|
end
|
|
end
|
|
|
|
local model_name = config.default_model
|
|
local model_cfg = config.models and config.models[model_name]
|
|
if not model_cfg then
|
|
io.stderr:write(("aish: default_model '%s' not found in models{}\n")
|
|
:format(tostring(model_name)))
|
|
os.exit(2)
|
|
end
|
|
|
|
local messages = { { role = "user", content = composed } }
|
|
local got_any = false
|
|
local ok, err = broker.chat_stream(model_cfg, messages,
|
|
function(kind, payload)
|
|
if kind == "text" and payload and payload ~= "" then
|
|
io.write(payload); io.flush()
|
|
got_any = true
|
|
end
|
|
end)
|
|
if not ok then
|
|
if got_any then io.write("\n") end
|
|
io.stderr:write("aish: broker error: " .. tostring(err) .. "\n")
|
|
os.exit(1)
|
|
end
|
|
if got_any then io.write("\n") end
|
|
end
|
|
|
|
local function main(argv)
|
|
local opts = parse_args(argv or {})
|
|
if opts.help then io.write(USAGE); return end
|
|
|
|
local config, config_path = load_config(opts)
|
|
io.stderr:write(("aish: loaded config from %s\n"):format(config_path))
|
|
|
|
if opts.prompt then
|
|
run_one_shot(config, opts.prompt)
|
|
return
|
|
end
|
|
|
|
local repl = require("repl")
|
|
repl.run(config)
|
|
end
|
|
|
|
main(arg)
|