001/* 002 * SPDX-License-Identifier: Apache-2.0 003 * 004 * Copyright 2025-2026 The Enola <https://enola.dev> Authors 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package dev.enola.cli; 019 020import com.google.common.collect.ImmutableMap; 021 022import dev.enola.chat.AbstractAgent; 023import dev.enola.chat.Message; 024import dev.enola.chat.Prompter; 025import dev.enola.chat.Switchboard; 026import dev.enola.common.context.TLC; 027import dev.enola.common.linereader.SystemInOutIO; 028import dev.enola.common.linereader.jline.JLineAgent; 029import dev.enola.common.linereader.jline.JLineBuiltinCommandsProcessor; 030import dev.enola.common.linereader.jline.JLineIO; 031import dev.enola.common.secret.InMemorySecretManager; 032import dev.enola.identity.Subject; 033import dev.enola.identity.Subjects; 034import dev.enola.rdf.io.JavaThingIntoRdfAppendableConverter; 035import dev.enola.thing.impl.ImmutableThing; 036import dev.enola.thing.io.ThingIntoAppendableConverter; 037import dev.enola.thing.java.ProxyTBF; 038 039import org.jline.console.SystemRegistry; 040import org.jline.console.impl.SystemRegistryImpl; 041import org.jline.reader.impl.DefaultParser; 042import org.jline.terminal.TerminalBuilder; 043 044import picocli.CommandLine; 045import picocli.shell.jline3.PicocliCommands; 046 047import java.io.IOException; 048import java.util.List; 049import java.util.Set; 050import java.util.concurrent.Callable; 051 052@CommandLine.Command( 053 name = "chat", 054 description = "Chat with Enola, LLMs, Bots, Tools, Agents, and more.") 055public class ChatCommand implements Callable<Integer> { 056 057 // TODO Merge the new Chat2Command with this!! 058 059 @CommandLine.Spec CommandLine.Model.CommandSpec spec; 060 061 @Override 062 public Integer call() throws IOException { 063 try (var ctx = TLC.open()) { 064 var tbf = new ProxyTBF(ImmutableThing.FACTORY); 065 var subject = new Subjects(tbf).local(); 066 ctx.push(ThingIntoAppendableConverter.class, new JavaThingIntoRdfAppendableConverter()); 067 if (System.console() != null) { 068 // TODO Parser configuration should be in class JLineIO, not here 069 DefaultParser parser = new DefaultParser(); 070 parser.setEofOnUnclosedQuote(false); 071 parser.setEofOnEscapedNewLine(false); 072 parser.setEofOnUnclosedBracket((DefaultParser.Bracket[]) null); 073 parser.setRegexVariable(null); // We do not have console variables! 074 075 try (var terminal = TerminalBuilder.terminal()) { 076 var parentCommandLine = spec.commandLine().getParent(); 077 var picocliCommands = new PicocliCommands(parentCommandLine); 078 var builtinCmdsProcessor = new JLineBuiltinCommandsProcessor(terminal); 079 var cwdSupplier = builtinCmdsProcessor.cwdSupplier(); 080 SystemRegistry systemRegistry = 081 new SystemRegistryImpl(parser, terminal, cwdSupplier, null); 082 systemRegistry.setCommandRegistries( 083 builtinCmdsProcessor.commandRegistry(), picocliCommands); 084 085 try (var io = 086 new JLineIO( 087 System.getenv(), 088 terminal, 089 parser, 090 systemRegistry.completer(), 091 ImmutableMap.of(), 092 systemRegistry::commandDescription, 093 true)) { 094 builtinCmdsProcessor.lineReader(io.lineReader()); 095 var prompter = new Prompter(new InMemorySecretManager()); 096 var pbx = prompter.getSwitchboard(); 097 prompter.addAgent(new JLineAgent(pbx, builtinCmdsProcessor)); 098 prompter.addAgent(new EnolaAgent(pbx, parentCommandLine)); 099 prompter.chatLoop(io, subject, true); 100 } 101 } 102 } else { 103 var io = new SystemInOutIO(); 104 new Prompter(new InMemorySecretManager()).chatLoop(io, subject, true); 105 } 106 } 107 return 0; 108 } 109 110 /** EnolaAgent runs Enola's own CLI sub-commands. */ 111 private static class EnolaAgent extends AbstractAgent { 112 private final CommandLine picocliCommandLine; 113 private final Set<String> commands; 114 115 public EnolaAgent(Switchboard pbx, CommandLine commandLine) { 116 super( 117 tbf.create(Subject.Builder.class, Subject.class) 118 .iri("https://enola.dev") 119 .label("enola") 120 .comment("Enola's own CLI sub-commands.") 121 .build(), 122 pbx); 123 this.picocliCommandLine = commandLine; 124 this.commands = picocliCommandLine.getSubcommands().keySet(); 125 } 126 127 @Override 128 public void accept(Message message) { 129 var commandLine = message.content(); 130 // split() won't handle quoted arguments correctly, which is fine here (for simple 131 // Builtins), but don't re-use this as-is for other more complex external commands. 132 var splitCommandLine = List.of(commandLine.split("\\s+")); 133 134 var command = splitCommandLine.get(0); 135 if (!commands.contains(command)) return; 136 137 // TODO Do something with exit code (just like for exec, where it's also still ignored) 138 int exit = picocliCommandLine.execute(splitCommandLine.toArray(new String[0])); 139 } 140 } 141}