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

View File

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