@sema-agent/core
Version:
Stateless, task-oriented AI agent core
221 lines (220 loc) • 11.3 kB
JavaScript
import { literalHeredocBody, literalTextOf } from "../../shell/lex.js";
import { programBasename, readProgramPosition } from "./bash-program-position.js";
import { implementationRejects, implementationsOf, programRow, readArgRoles, selectedOptions, targetUserOf } from "./program-read-model.js";
import { namesStandardInput } from "./safety.js";
export function codeBodies(basename, args) {
const row = programRow(basename);
switch (row?.body) {
case "first-operand": return firstOperandBody(row, args);
case "assignment-values": return aliasValues(args);
case "option-values": return shellTextOptionValues(row, args);
case undefined: return [];
}
}
function firstOperandBody(row, args) {
const first = readArgRoles(row, args).find((e) => e.kind === "operand");
const action = first === undefined ? undefined : args[first.at];
return action === undefined || action === "" || action === "-" ? [] : [action];
}
function aliasValues(args) {
const values = [];
for (const a of args) {
const eq = a.indexOf("=");
if (eq > 0)
values.push(a.slice(eq + 1));
}
return values;
}
function shellTextOptionValues(row, args) {
const callbacks = [];
for (const e of optionSection(readArgRoles(row, args))) {
const text = shellTextOf(e, args);
if (text !== undefined)
callbacks.push(text);
}
return callbacks;
}
function optionSection(events) {
const end = events.findIndex((e) => e.kind === "operand" || e.kind === "dash-word-operand" || e.kind === "end-of-options");
return end === -1 ? [...events] : events.slice(0, end);
}
function shellTextOf(e, args) {
if (e.kind === "value" && e.role?.kind === "shell-text")
return args[e.at];
if (e.kind === "option" && typeof e.reading === "object" && e.reading.role?.kind === "shell-text")
return e.reading.attached;
return undefined;
}
export function reentryText(basename, args, stdin) {
const row = programRow(basename);
const r = row?.reentry;
if (row === undefined || r?.kind !== "shell")
return undefined;
const text = argvText(row, r.text, args);
if (text !== undefined || stdin === undefined)
return text;
const source = scriptSourceOf(row, args);
return (r.stdin === "script" && source.stdin) || (source.file !== undefined && namesStandardInput(args[source.file])) ? stdin : undefined;
}
function argvText(row, at, args) {
if (at === "words")
return args.length === 0 ? undefined : args.join(" ");
if (at === "non-option-words")
return args.filter((a) => !a.startsWith("-")).join(" ") || undefined;
if (at === "file-operand")
return undefined;
for (const e of optionSection(readArgRoles(row, args))) {
const text = shellTextOf(e, args);
if (text !== undefined)
return text;
}
return undefined;
}
export function launchedCommands(argv, pos = readProgramPosition(argv)) {
const runs = pos.refusal?.kind === "control_syntax_at_program_position" ? pos.refusal.runsShell : undefined;
if (runs === undefined)
return [];
const row = programRow(programBasename(argv[runs.launcher].folded));
const words = argv.slice(runs.from).map((w) => w.folded);
const at = (k) => runs.from + k;
if (row?.launcher?.shellText === "user")
return userCommands(row, words, at);
const user = row === undefined ? undefined : targetUserOf(row, argv.slice(runs.launcher + 1, runs.from).map((w) => w.folded));
const as = user !== undefined ? { as: user } : {};
if (words.length === 0 || row?.launcher === undefined)
return [{ args: [], ...as }];
if (row.launcher.shellText === "joined") {
const texts = new Map();
for (const view of implementationsOf(row)) {
const events = readArgRoles(view, words);
const first = events.find((e) => e.kind === "operand" || e.kind === "dash-word-operand")?.at ?? words.length;
if (implementationRejects(row, view, words.slice(0, first)))
continue;
const joined = words.slice(first).map((_, k) => at(first + k));
const values = events.flatMap((e) => (e.kind === "option" || (e.kind === "value" && e.at < first) ? roleValue(e, "shell-text", words, at) ?? [] : []));
for (const t of [...(joined.length === 0 ? [] : [{ joined }]), ...values])
texts.set(JSON.stringify(t), t);
}
return texts.size === 0 ? [{ args: [], ...as }] : [...texts.values()].map((t) => ({ args: [{ fixed: "-c" }, t], ...as }));
}
return [{ args: [{ fixed: "-c" }, { joined: words.map((_, k) => at(k)), escaped: true }], ...as }];
}
function roleValue(e, kind, words, at) {
if (e.kind === "value" && e.role?.kind === kind)
return { at: at(e.at) };
if (e.kind === "option" && typeof e.reading === "object" && e.reading.role?.kind === kind)
return { at: at(e.at), from: words[e.at].length - e.reading.attached.length };
return undefined;
}
function userCommands(row, words, at) {
const out = [];
const valued = (e, kind) => roleValue(e, kind, words, at);
for (const view of implementationsOf(row)) {
const events = readArgRoles(view, words);
const operands = events.filter((e) => e.kind === "operand");
const user = (operands[0]?.dash !== undefined ? operands[1] : operands[0])?.at;
const end = user ?? words.length;
const stops = view.grammars[0]?.afterFirstOperand === "operands";
const own = stops ? events.filter((e) => e.at < end) : events;
const named = own.flatMap((e) => valued(e, "command") ?? []).at(-1);
const fromEnvironment = selectedOptions(view, stops ? words.slice(0, end) : words, "one-implementation").some((o) => o.mode === "names-program");
const program = named !== undefined ? { program: named } : fromEnvironment ? { program: { spelled: "\"$SHELL\"" } } : {};
const target = targetUserOf(view, stops ? words.slice(0, end) : words.filter((_, k) => k !== user), { name: user === undefined ? "root" : words[user], dash: operands[0]?.dash !== undefined });
const runs = { ...program, ...(target !== undefined ? { as: target } : {}) };
if (stops) {
out.push({ ...runs, args: user === undefined ? [] : words.slice(user + 1).map((_, k) => ({ at: at(user + 1 + k) })) });
continue;
}
const rest = operands.filter((e) => user !== undefined && e.at > user).map((e) => ({ at: at(e.at) }));
const texts = own.flatMap((e) => valued(e, "shell-text") ?? []);
if (texts.length === 0)
out.push({ ...runs, args: rest });
for (const text of texts)
out.push({ ...runs, args: [{ fixed: "-c" }, text, ...rest] });
}
return out;
}
export function launchedReadings(argv, command) {
const p = command.program;
if (p === undefined)
return { shell: "sh" };
const raw = (k, from = 0) => {
if (from === 0)
return argv[k].raw;
const literal = literalTextOf(argv[k].pieces);
return literal === undefined ? argv[k].folded.slice(from) : `'${literal.slice(from).replace(/'/g, "'\\''")}'`;
};
const line = [("at" in p ? raw(p.at, p.from) : p.spelled), ...command.args.map((a) => ("fixed" in a ? a.fixed : "at" in a ? raw(a.at, a.from) : a.joined.map((k) => raw(k)).join(" ")))].join(" ");
const literal = "at" in p ? literalTextOf(argv[p.at].pieces)?.slice(p.from ?? 0) : undefined;
if (literal === undefined)
return { shell: "sh", line };
const basename = programBasename(literal);
const reentry = programRow(basename)?.reentry;
return reentry?.kind === "shell" && reentry.text === "c-option" ? { shell: basename } : { line };
}
export function launchedInput(argv, pos, spell) {
const spelled = (word) => (args) => spellLaunched(argv, args, { word: (k, from) => word(k).slice(from), values: spell.values });
const asTexts = spelled(spell.text);
const asArgs = spelled(spell.folded);
const lines = new Set();
const programWords = [];
const shells = launchedCommands(argv, pos).flatMap((c) => {
const { shell, line } = launchedReadings(argv, c);
if (line !== undefined)
lines.add(line);
if (shell === undefined)
return [];
if (line === undefined && c.program !== undefined && "at" in c.program && c.program.from === undefined)
programWords.push(c.program.at);
const words = c.args.map((x) => ("at" in x && x.from === undefined ? x.at : undefined));
return [{ shell, texts: asTexts(c.args), args: asArgs(c.args), words, ...(c.as !== undefined ? { as: c.as } : {}) }];
});
return { shells, programWords, lines: [...lines] };
}
function sudoEscaped(field) {
return field.replace(/[^A-Za-z0-9_$-]/gu, (c) => `\\${c}`);
}
function launchedSpellingOf(v) {
return v.glob === true ? v.parts.map((p) => (p.pattern ? p.text : sudoEscaped(p.text))).join("") : sudoEscaped(v.spelled);
}
export function spellLaunched(argv, args, spell) {
const field = (k) => {
const literal = literalTextOf(argv[k].pieces);
if (literal !== undefined)
return sudoEscaped(literal);
const values = spell.values?.(k);
return values?.length === 1 ? launchedSpellingOf(values[0]) : argv[k].raw;
};
return args.map((a) => ("fixed" in a ? a.fixed : "at" in a ? spell.word(a.at, a.from ?? 0) : a.joined.map((k) => (a.escaped === true ? field(k) : spell.word(k, 0))).join(" ")));
}
export function scriptFileAt(basename, args) {
const row = programRow(basename);
return row?.reentry?.kind === "shell" && argvText(row, row.reentry.text, args) === undefined ? scriptSourceOf(row, args).file : undefined;
}
function scriptSourceOf(row, args) {
const events = readArgRoles(row, args);
const options = optionSection(events);
const end = events[options.length]?.at ?? args.length;
if (selectedOptions(row, args.slice(0, end), "one-implementation").some((o) => o.mode === "interactive"))
return { stdin: true };
const first = events.find((e) => e.kind === "operand");
if (first?.kind !== "operand" || first.dash !== undefined)
return { stdin: true };
return { file: first.at, stdin: options.some((e) => e.kind === "option" && e.reading === "speculative") };
}
export function stdinTextsOf(segment, fed) {
const out = [];
let replacesPipe = false;
for (const t of segment.tokens) {
if (t.kind === "heredoc") {
const literal = t.quoted ? t.body : literalHeredocBody(t.body);
out.push(literal === undefined ? { from: t.body } : { from: t.body, literal });
}
else if (t.kind === "redirect" && (t.fd ?? 0) === 0 && (t.op === "<<<" || t.op === "<" || (t.op === "<&" && t.dup !== "-"))) {
const literal = t.op === "<<<" && t.target !== undefined ? literalTextOf(t.target.pieces) : undefined;
out.push({ from: `${t.op}${t.target?.raw ?? t.dup ?? ""}`, ...(literal !== undefined ? { literal } : {}) });
replacesPipe = true;
}
}
return replacesPipe ? out : [...fed, ...out];
}