Compare commits

..

No commits in common. "77c0d1bce8a28f0e575b0e17fc4301b78497c240" and "db788760a3cb386560b3f8be2a5339a191e00091" have entirely different histories.

2 changed files with 34 additions and 36 deletions

View File

@ -4,25 +4,29 @@
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`
`edit`, `retry`, `continue` sub-commands. 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 efforts are made to ensure they only. They do not accept absolute paths, and all efforts are made to ensure
cannot escape above the working directory). **Close attention must be paid to they cannot escape above the working directory (not quite using chroot, but in
where you are running `lmcli`, as the model could at any time decide to use one effect). **Close attention must be paid to where you are running `lmcli`, as
of these tools to discover and read potentially sensitive information from your the model could at any time decide to use one of these tools to discover and
filesystem.** read potentially sensitive information from your 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 number of conversations shown with `ls`, without --all // Limit to number of conversations shown with `ls`, without --all
LS_LIMIT int = 25 LS_LIMIT int = 25
) )
@ -32,15 +32,14 @@ func init() {
cmd.MarkFlagsMutuallyExclusive("system-prompt", "system-prompt-file") cmd.MarkFlagsMutuallyExclusive("system-prompt", "system-prompt-file")
} }
listCmd.Flags().Bool("all", false, fmt.Sprintf("Show all conversations, by default only the last %d are shown", LS_LIMIT)) lsCmd.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,
listCmd, lsCmd,
newCmd, newCmd,
promptCmd, promptCmd,
renameCmd, renameCmd,
@ -115,7 +114,8 @@ func lookupConversationE(shortName string) (*Conversation, error) {
} }
// handleConversationReply handles sending messages to an existing // handleConversationReply handles sending messages to an existing
// conversation, optionally persisting both the sent replies and responses. // conversation, optionally persisting them. It displays the entire
// 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,15 +182,14 @@ var rootCmd = &cobra.Command{
}, },
} }
var listCmd = &cobra.Command{ var lsCmd = &cobra.Command{
Use: "list", Use: "ls",
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 {
Fatal("Could not fetch conversations.\n") fmt.Println("Could not fetch conversations.")
return return
} }
@ -660,39 +659,34 @@ 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 toEdit *Message var lastUserMessage *Message
for i := len(messages) - 1; i >= 0; i-- { for i := len(messages) - 1; i >= 0; i-- {
if i == desiredIdx { if messages[i].Role == MessageRoleUser {
toEdit = &messages[i] lastUserMessage = &messages[i]
} }
toRemove = append(toRemove, messages[i]) toRemove = append(toRemove, messages[i])
messages = messages[:i] messages = messages[:i]
if toEdit != nil { if lastUserMessage != nil {
break break
} }
} }
existingContents := toEdit.OriginalContent if lastUserMessage == nil {
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)
switch newContents { if newContents == existingContents {
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")
} }