Skip to content

Commit 628fe0b

Browse files
josealekhineclaude
andcommitted
feat: add --file and stdin support to ctx add
Enable rich context entries by reading content from files or stdin: - --file/-f flag: Read content from specified file - Stdin support: Auto-detect pipe and read from stdin - Backward compatible: Argument-based content still works Examples: ctx add learning --file learning.md cat decision.md | ctx add decision ctx add task "Quick task" # still works This enables multi-line, structured entries for learnings and decisions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c04cfed commit 628fe0b

2 files changed

Lines changed: 50 additions & 8 deletions

File tree

.context/TASKS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@
139139
- [x] Document timestamp correlation approach in AGENT_PLAYBOOK.md — explain how to correlate entries to sessions by time overlap
140140

141141
### Phase 13: Rich Context Entries `#priority:medium` `#area:cli`
142-
- [ ] Add --file flag to ctx add — read entry content from a file instead of CLI arg
143-
- [ ] Add stdin support to ctx add — if no content arg and stdin is pipe, read from stdin
142+
- [x] Add --file flag to ctx add — read entry content from a file instead of CLI arg
143+
- [x] Add stdin support to ctx add — if no content arg and stdin is pipe, read from stdin
144144
- [ ] Create learning template with Context/Lesson/Application structure for --file usage
145145
- [ ] Create decision template with Context/Options/Decision/Rationale structure for --file usage
146146
- [ ] Document rich entry workflow in AGENT_PLAYBOOK.md — explain when/how agents should use --file vs inline

internal/cli/add.go

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package cli
88

99
import (
10+
"bufio"
1011
"fmt"
1112
"os"
1213
"path/filepath"
@@ -18,8 +19,9 @@ import (
1819
)
1920

2021
var (
21-
addPriority string
22-
addSection string
22+
addPriority string
23+
addSection string
24+
addFromFile string
2325
)
2426

2527
// fileTypeMap maps short names to actual file names
@@ -37,7 +39,7 @@ var fileTypeMap = map[string]string{
3739
// AddCmd returns the add command.
3840
func AddCmd() *cobra.Command {
3941
cmd := &cobra.Command{
40-
Use: "add <type> <content>",
42+
Use: "add <type> [content]",
4143
Short: "Add a new item to a context file",
4244
Long: `Add a new decision, task, learning, or convention to the appropriate context file.
4345
@@ -47,24 +49,64 @@ Types:
4749
learning Add to LEARNINGS.md
4850
convention Add to CONVENTIONS.md
4951
52+
Content can be provided as:
53+
- Command argument: ctx add learning "text here"
54+
- File: ctx add learning --file /path/to/content.md
55+
- Stdin: echo "text" | ctx add learning
56+
5057
Examples:
5158
ctx add decision "Use PostgreSQL for primary database"
5259
ctx add task "Implement user authentication" --priority high
5360
ctx add learning "Vitest mocks must be hoisted"
54-
ctx add convention "All API routes must be versioned"`,
55-
Args: cobra.MinimumNArgs(2),
61+
ctx add learning --file learning-template.md
62+
cat notes.md | ctx add decision`,
63+
Args: cobra.MinimumNArgs(1),
5664
RunE: runAdd,
5765
}
5866

5967
cmd.Flags().StringVarP(&addPriority, "priority", "p", "", "Priority level for tasks (high, medium, low)")
6068
cmd.Flags().StringVarP(&addSection, "section", "s", "", "Target section within file")
69+
cmd.Flags().StringVarP(&addFromFile, "file", "f", "", "Read content from file instead of argument")
6170

6271
return cmd
6372
}
6473

6574
func runAdd(cmd *cobra.Command, args []string) error {
6675
fileType := strings.ToLower(args[0])
67-
content := strings.Join(args[1:], " ")
76+
77+
// Determine content source: args, --file, or stdin
78+
var content string
79+
80+
if addFromFile != "" {
81+
// Read from file
82+
fileContent, err := os.ReadFile(addFromFile)
83+
if err != nil {
84+
return fmt.Errorf("failed to read file %s: %w", addFromFile, err)
85+
}
86+
content = strings.TrimSpace(string(fileContent))
87+
} else if len(args) > 1 {
88+
// Content from arguments
89+
content = strings.Join(args[1:], " ")
90+
} else {
91+
// Try reading from stdin (check if it's a pipe)
92+
stat, _ := os.Stdin.Stat()
93+
if (stat.Mode() & os.ModeCharDevice) == 0 {
94+
// stdin is a pipe, read from it
95+
scanner := bufio.NewScanner(os.Stdin)
96+
var lines []string
97+
for scanner.Scan() {
98+
lines = append(lines, scanner.Text())
99+
}
100+
if err := scanner.Err(); err != nil {
101+
return fmt.Errorf("failed to read from stdin: %w", err)
102+
}
103+
content = strings.TrimSpace(strings.Join(lines, "\n"))
104+
}
105+
}
106+
107+
if content == "" {
108+
return fmt.Errorf("no content provided. Use argument, --file, or pipe from stdin")
109+
}
68110

69111
fileName, ok := fileTypeMap[fileType]
70112
if !ok {

0 commit comments

Comments
 (0)