Add interactive shell mode to ac-cli

Running the jar with no arguments now starts a psql-like REPL (JLine,
with persistent history) that dispatches commands to the existing
picocli subcommands, plus a new `connect` command to set the session
server URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ingo Schnabel
2026-06-13 22:55:20 +02:00
parent 94ceaefa9f
commit 138569faff
8 changed files with 155 additions and 5 deletions

View File

@@ -185,6 +185,28 @@ java -jar ac-cli/target/ac-cli-1.0.0-SNAPSHOT.jar --help
By default the CLI talks to `http://localhost:8080`. Override with `-s/--server <url>` or the `AC_SERVER_URL` environment variable.
### Interactive shell
Running the jar with no arguments starts a `psql`-like interactive shell with line editing and history (`~/.agenticcode_history`):
```bash
java -jar ac-cli/target/ac-cli-1.0.0-SNAPSHOT.jar
```
```
AgenticCode interactive shell. Type 'help' for commands, 'exit' to quit.
agenticcode> connect http://my-server:8080
Connected to http://my-server:8080
agenticcode> callers MY-MODULE
[ ... ]
agenticcode> ingest ./src/main/natural
OK ./src/main/natural/FOO.nat
...
agenticcode> exit
```
`connect <url>` sets the server for the rest of the session (overridden by `-s` on an individual command). `help`/`?` shows available commands, `exit`/`quit` ends the session.
### Ingest files or folders
Recursively walks a file or directory; `.java` files are sent to `/api/ingest/java` and `.nat`/`.nsn` files to `/api/ingest/natural`. Files with other extensions are skipped.

6
ac-cli/ac-cli.iml Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="CheckStyle-IDEA-Module" serialisationVersion="2">
<option name="activeLocationsIds" />
</component>
</module>

View File

@@ -29,6 +29,10 @@
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -58,6 +62,7 @@
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.agenticcode.cli.AgenticCodeCli</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>

View File

@@ -11,18 +11,24 @@ import java.util.concurrent.Callable;
*/
abstract class AbstractApiCommand implements Callable<Integer> {
static final String SERVER_URL_PROPERTY = "ac.server.url";
private static final String DEFAULT_SERVER_URL = "http://localhost:8080";
@Option(names = {"-s", "--server"},
description = "Base URL of the AgenticCode server (default: ${DEFAULT-VALUE}, or $AC_SERVER_URL)",
description = "Base URL of the AgenticCode server (default: ${DEFAULT-VALUE}, "
+ "or the most recent 'connect', or $AC_SERVER_URL)",
defaultValue = DEFAULT_SERVER_URL)
String server = DEFAULT_SERVER_URL;
protected ApiClient apiClient() {
String baseUrl = server;
if (DEFAULT_SERVER_URL.equals(server)) {
String connected = System.getProperty(SERVER_URL_PROPERTY);
String env = System.getenv("AC_SERVER_URL");
if (env != null && !env.isBlank()) {
if (connected != null && !connected.isBlank()) {
baseUrl = connected;
} else if (env != null && !env.isBlank()) {
baseUrl = env;
}
}

View File

@@ -2,9 +2,15 @@ package com.agenticcode.cli;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Spec;
import java.util.concurrent.Callable;
/**
* Command-line client for ingesting source code and querying the AgenticCode AST graph API.
*
* <p>Run with a subcommand for one-shot use, or with no arguments to enter an interactive shell.
*/
@Command(
name = "agenticcode",
@@ -12,6 +18,7 @@ import picocli.CommandLine.Command;
version = "AgenticCode CLI 1.0.0",
description = "Ingest source code and query the AgenticCode AST graph API.",
subcommands = {
ConnectCommand.class,
IngestCommand.class,
CallersCommand.class,
CalleesCommand.class,
@@ -19,7 +26,11 @@ import picocli.CommandLine.Command;
SearchIdentifierCommand.class
}
)
public final class AgenticCodeCli implements Runnable {
public final class AgenticCodeCli implements Callable<Integer> {
@SuppressWarnings("NullAway.Init")
@Spec
CommandSpec spec;
public static void main(String[] args) {
int exitCode = new CommandLine(new AgenticCodeCli()).execute(args);
@@ -27,7 +38,7 @@ public final class AgenticCodeCli implements Runnable {
}
@Override
public void run() {
CommandLine.usage(this, System.out);
public Integer call() throws Exception {
return new InteractiveShell(spec.commandLine()).run();
}
}

View File

@@ -0,0 +1,24 @@
package com.agenticcode.cli;
import picocli.CommandLine.Command;
import picocli.CommandLine.Parameters;
import java.util.concurrent.Callable;
/**
* Sets the server URL used by subsequent commands in this session.
*/
@Command(name = "connect", mixinStandardHelpOptions = true, description = "Set the server URL for this session")
final class ConnectCommand implements Callable<Integer> {
@SuppressWarnings("NullAway.Init")
@Parameters(index = "0", description = "Base URL of the AgenticCode server")
String url;
@Override
public Integer call() {
System.setProperty(AbstractApiCommand.SERVER_URL_PROPERTY, url);
System.out.println("Connected to " + url);
return 0;
}
}

View File

@@ -0,0 +1,69 @@
package com.agenticcode.cli;
import org.jline.reader.EndOfFileException;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.ParsedLine;
import org.jline.reader.UserInterruptException;
import org.jline.reader.impl.DefaultParser;
import org.jline.reader.impl.history.DefaultHistory;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import picocli.CommandLine;
import java.io.IOException;
import java.nio.file.Path;
/**
* Interactive read-eval-print loop dispatching command lines to the picocli command tree.
*/
final class InteractiveShell {
private static final Path HISTORY_FILE = Path.of(System.getProperty("user.home"), ".agenticcode_history");
private final CommandLine commandLine;
InteractiveShell(CommandLine commandLine) {
this.commandLine = commandLine;
}
int run() throws IOException {
try (Terminal terminal = TerminalBuilder.builder().build()) {
LineReader reader = LineReaderBuilder.builder()
.terminal(terminal)
.parser(new DefaultParser())
.appName("agenticcode")
.variable(LineReader.HISTORY_FILE, HISTORY_FILE)
.build();
new DefaultHistory(reader);
terminal.writer().println("AgenticCode interactive shell. Type 'help' for commands, 'exit' to quit.");
while (true) {
String line;
try {
line = reader.readLine("agenticcode> ").trim();
} catch (UserInterruptException e) {
continue;
} catch (EndOfFileException e) {
break;
}
if (line.isEmpty()) {
continue;
}
if (line.equals("exit") || line.equals("quit")) {
break;
}
if (line.equals("help") || line.equals("?")) {
commandLine.usage(terminal.writer());
continue;
}
ParsedLine parsed = reader.getParser().parse(line, line.length());
commandLine.execute(parsed.words().toArray(new String[0]));
}
}
return 0;
}
}

View File

@@ -38,6 +38,7 @@
<nullaway.version>0.13.1</nullaway.version>
<jspecify.version>1.0.0</jspecify.version>
<picocli.version>4.7.6</picocli.version>
<jline.version>3.26.3</jline.version>
<maven-shade-plugin.version>3.6.0</maven-shade-plugin.version>
<ac-mvn-plugins.version>${project.version}</ac-mvn-plugins.version>
</properties>
@@ -109,6 +110,12 @@
<artifactId>picocli</artifactId>
<version>${picocli.version}</version>
</dependency>
<dependency>
<groupId>org.jline</groupId>
<artifactId>jline</artifactId>
<version>${jline.version}</version>
</dependency>
</dependencies>
</dependencyManagement>