Compare commits

...

3 Commits

Author SHA1 Message Date
77c0d1bce8 Update README.md 2024-01-11 10:25:52 -07:00
51ce74ad3a Add --offset flag to edit command 2024-01-09 18:10:05 +00:00
b93ee94233 Rename lsCmd to listCmd, add ls as an alias 2024-01-03 17:45:02 +00:00
2 changed files with 36 additions and 34 deletions

View File

@ -4,29 +4,25 @@
Current features:
- Perform one-shot prompts with `lmcli prompt <message>`
- Manage persistent conversations with the `new`, `reply`, `view`, and `rm`
sub-commands.
- Manage persistent conversations with the `new`, `reply`, `view`, and `rm`,
`edit`, `retry`, `continue` sub-commands.
- Syntax highlighted output
- Tool calling, see the [Tools](#tools) section.
Planned features:
- Ask questions about content received on stdin
- Conversation editing
Maybe features:
- Chat-like interface (`lmcli chat`) for rapid back-and-forth conversations
- Support for additional models/APIs besides just OpenAI
- Natural language image generation, iterative editing
## Tools
Tools must be explicitly enabled by adding the tool's name to the
`openai.enabledTools` array in `config.yaml`.
Note: all filesystem related tools operate relative to the current directory
only. They do not accept absolute paths, and all efforts are made to ensure
they cannot escape above the working directory (not quite using chroot, but in
effect). **Close attention must be paid to where you are running `lmcli`, as
the model could at any time decide to use one of these tools to discover and
read potentially sensitive information from your filesystem.**
only. They do not accept absolute paths, and efforts are made to ensure they
cannot escape above the working directory). **Close attention must be paid to
where you are running `lmcli`, as the model could at any time decide to use one
of these tools to discover and read potentially sensitive information from your
filesystem.**
It's best to only have tools enabled in `config.yaml` when you intend to be
using them, since their descriptions (see `pkg/cli/functions.go`) count towards

View File

@ -18,7 +18,7 @@ var (
)
const (
// Limit to number of conversations shown with `ls`, without --all
// Limit number of conversations shown with `ls`, without --all
LS_LIMIT int = 25
)
@ -32,14 +32,15 @@ func init() {
cmd.MarkFlagsMutuallyExclusive("system-prompt", "system-prompt-file")
}
lsCmd.Flags().Bool("all", false, fmt.Sprintf("Show all conversations, by default only the last %d are shown", LS_LIMIT))
listCmd.Flags().Bool("all", false, fmt.Sprintf("Show all conversations, by default only the last %d are shown", LS_LIMIT))
renameCmd.Flags().Bool("generate", false, "Generate a conversation title")
editCmd.Flags().Int("offset", 1, "Offset from the last reply to edit (Default: edit your last message, assuming there's an assistant reply)")
rootCmd.AddCommand(
cloneCmd,
continueCmd,
editCmd,
lsCmd,
listCmd,
newCmd,
promptCmd,
renameCmd,
@ -114,8 +115,7 @@ func lookupConversationE(shortName string) (*Conversation, error) {
}
// handleConversationReply handles sending messages to an existing
// conversation, optionally persisting them. It displays the entire
// conversation before
// conversation, optionally persisting both the sent replies and responses.
func handleConversationReply(c *Conversation, persist bool, toSend ...Message) {
existing, err := store.Messages(c)
if err != nil {
@ -182,14 +182,15 @@ var rootCmd = &cobra.Command{
},
}
var lsCmd = &cobra.Command{
Use: "ls",
var listCmd = &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List conversations",
Long: `List conversations in order of recent activity`,
Run: func(cmd *cobra.Command, args []string) {
conversations, err := store.Conversations()
if err != nil {
fmt.Println("Could not fetch conversations.")
Fatal("Could not fetch conversations.\n")
return
}
@ -659,34 +660,39 @@ var editCmd = &cobra.Command{
Fatal("Could not retrieve messages for conversation: %s\n", conversation.Title)
}
offset, _ := cmd.Flags().GetInt("offset")
if offset < 0 {
offset = -offset
}
if offset > len(messages) - 1 {
Fatal("Offset %d is before the start of the conversation\n", offset)
}
desiredIdx := len(messages) - 1 - offset
// walk backwards through the conversation deleting messages until and
// including the last user message
toRemove := []Message{}
var lastUserMessage *Message
var toEdit *Message
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Role == MessageRoleUser {
lastUserMessage = &messages[i]
if i == desiredIdx {
toEdit = &messages[i]
}
toRemove = append(toRemove, messages[i])
messages = messages[:i]
if lastUserMessage != nil {
if toEdit != nil {
break
}
}
if lastUserMessage == nil {
Fatal("No messages left in the conversation, nothing to edit.\n")
}
existingContents := lastUserMessage.OriginalContent
existingContents := toEdit.OriginalContent
newContents := inputFromArgsOrEditor(args[1:], "# Save when finished editing\n", existingContents)
if newContents == existingContents {
switch newContents {
case existingContents:
Fatal("No edits were made.\n")
}
if newContents == "" {
case "":
Fatal("No message was provided.\n")
}