Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The `capture-html` command is also a public CLI subcommand that converts existin
## Requirements

- Go 1.25+
- An AI CLI that can be invoked with `-p <prompt>`
- An authenticated Codex CLI; the default integration uses `codex exec` and sends prompts through stdin
- Google Chrome, or a compatible browser binary that supports `--headless=new`
- `img2webp` (required only when Animated WebP export is enabled)
- `ffmpeg` (required when MP4 or Live export is enabled)
Expand Down Expand Up @@ -242,26 +242,31 @@ Additional notes:

## AI CLI examples

Using `ccs`:
Using `codex`:

```yaml
ai:
command: ccs
command: codex
args:
- codex
- --bare
- exec
- --ignore-user-config
- --disable
- apps
- --disable
- plugins
- --disable
- remote_plugin
- --ephemeral
- --skip-git-repo-check
- --sandbox
- read-only
- --color
- never
- --model
- gpt-5.6-sol
```

Using `claude`:

```yaml
ai:
command: claude
args:
- -p
```

Note: adjust the arguments to match your local AI CLI setup, as long as `mark2note` can use it to generate deck JSON.
For direct `codex` calls, `mark2note` sends the prompt through stdin. Custom AI CLIs retain the legacy `-p <prompt>` invocation, so adjust their arguments to match your local setup.

## Common commands

Expand Down
35 changes: 20 additions & 15 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
## 环境要求

- Go 1.25+
- 可通过 `-p <prompt>` 调用的 AI CLI
- 已完成登录的 Codex CLI;默认通过 `codex exec` 调用,并使用 stdin 传入 prompt
- Google Chrome,或兼容 `--headless=new` 的浏览器可执行文件
- `img2webp`(仅在启用 Animated WebP 增强导出时需要)
- `ffmpeg`(启用 MP4 或 Live 导出时需要)
Expand Down Expand Up @@ -242,26 +242,31 @@ posters:

## AI CLI 示例

使用 `ccs`:
使用 `codex`:

```yaml
ai:
command: ccs
command: codex
args:
- codex
- --bare
- exec
- --ignore-user-config
- --disable
- apps
- --disable
- plugins
- --disable
- remote_plugin
- --ephemeral
- --skip-git-repo-check
- --sandbox
- read-only
- --color
- never
- --model
- gpt-5.6-sol
```

使用 `claude`:

```yaml
ai:
command: claude
args:
- -p
```

说明:请根据你本地 AI CLI 的实际调用方式调整参数,只要能被 `mark2note` 用来生成 deck JSON 即可。
直连 `codex` 时,`mark2note` 会通过 stdin 传入 prompt。自定义 AI CLI 仍沿用原有的 `-p <prompt>` 调用方式,可按本机环境调整其他参数。

## 常用命令

Expand Down
20 changes: 17 additions & 3 deletions configs/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,24 @@ output:
dir: output

ai:
command: claude
command: codex
args:
- --bare
- --disable-slash-commands
- exec
- --ignore-user-config
- --disable
- apps
- --disable
- plugins
- --disable
- remote_plugin
- --ephemeral
- --skip-git-repo-check
- --sandbox
- read-only
- --color
- never
- --model
- gpt-5.6-sol
retry:
delays:
- 1s
Expand Down
41 changes: 31 additions & 10 deletions internal/ai/deck_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os/exec"
"path/filepath"
"slices"
"strings"
"time"
Expand Down Expand Up @@ -56,7 +57,7 @@ var (
)

type CommandRunner interface {
Run(name string, args ...string) (string, string, error)
Run(name, stdin string, args ...string) (string, string, error)
}

type Builder struct {
Expand Down Expand Up @@ -91,8 +92,11 @@ func buildDeckPromptWithMaxPages(markdown, promptExtra string, maxPages int) str

type execRunner struct{}

func (execRunner) Run(name string, args ...string) (string, string, error) {
func (execRunner) Run(name, stdin string, args ...string) (string, string, error) {
cmd := exec.Command(name, args...)
if stdin != "" {
cmd.Stdin = strings.NewReader(stdin)
}
out, err := cmd.Output()
if err == nil {
return strings.TrimSpace(string(out)), "", nil
Expand All @@ -117,13 +121,13 @@ func effectiveAICommandRetryDelays(delays []time.Duration) []time.Duration {
return cloneDurations(delays)
}

func runAICommand(runner CommandRunner, name string, retryDelays []time.Duration, args ...string) (string, string, error) {
func runAICommand(runner CommandRunner, name, stdin string, retryDelays []time.Duration, args ...string) (string, string, error) {
var stdout string
var stderr string
var err error
delays := effectiveAICommandRetryDelays(retryDelays)
for attempt := 0; attempt <= len(delays); attempt++ {
stdout, stderr, err = runner.Run(name, args...)
stdout, stderr, err = runner.Run(name, stdin, args...)
if err == nil {
return stdout, stderr, nil
}
Expand Down Expand Up @@ -178,12 +182,12 @@ func (b Builder) effectiveRunner() CommandRunner {
}

func (b Builder) BuildDeckJSON(markdown string) (string, error) {
args := append([]string{}, b.Args...)
if shouldUseBareOutput(b.Command, b.Args) && !containsArg(args, "--bare") {
args = append(args, "--bare")
}
args = append(args, "-p", buildDeckPromptWithMaxPages(markdown, b.PromptExtra, b.MaxPages))
stdout, stderr, err := runAICommand(b.effectiveRunner(), b.Command, b.RetryDelays, args...)
args, stdin := buildAICommandInvocation(
b.Command,
b.Args,
buildDeckPromptWithMaxPages(markdown, b.PromptExtra, b.MaxPages),
)
stdout, stderr, err := runAICommand(b.effectiveRunner(), b.Command, stdin, b.RetryDelays, args...)
if err != nil {
return "", fmt.Errorf("%w: %v\nstderr: %s", ErrAICommandFailed, err, stderr)
}
Expand Down Expand Up @@ -268,6 +272,23 @@ func containsArg(args []string, target string) bool {
return slices.Contains(args, target)
}

func buildAICommandInvocation(command string, configuredArgs []string, prompt string) ([]string, string) {
args := append([]string(nil), configuredArgs...)
if isDirectCodexCommand(command) {
return args, prompt
}
if shouldUseBareOutput(command, configuredArgs) && !containsArg(args, "--bare") {
args = append(args, "--bare")
}
return append(args, "-p", prompt), ""
}

func isDirectCodexCommand(command string) bool {
base := strings.ToLower(filepath.Base(strings.TrimSpace(command)))
base = strings.TrimSuffix(base, ".exe")
return base == "codex"
}

func shouldUseBareOutput(command string, args []string) bool {
if command != "ccs" {
return false
Expand Down
Loading