← back to Cli Printing Press
feat(generator): make generated CLIs agent-native by default (#43)
a8a003daf7ca0a28244de3b563eba8fb7d133f27 · 2026-03-29 09:19:51 -0700 · Matt Van Horn
Templates now generate CLIs optimized for AI agent consumption:
- --compact uses blocklist on single objects (strips description, body,
comments, attachments) instead of allowlist — works across all API domains
- --agent meta-flag sets --json --compact --no-input --no-color --yes
- Analytics commands (stale, orphans, load) have --limit flag (default 50)
with total count in output to prevent unbounded context dumps
- Load command resolves assignee names from synced users instead of
outputting raw Go map debug format
- Search iterates per-table FTS individually to prevent duplicates,
filters nil/empty entries
- Store template includes IsUUID() and ResolveByName() for name-or-ID
resolution from synced data (with ambiguity detection)
- Scorecard's AgentNative dimension extended with token efficiency checks
- SKILL.md adds npm/PyPI SDK search to research brief and absorb gate,
plus search dedup guidance for build phase
Co-authored-by: Matt Van Horn <mvanhorn@MacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
M internal/generator/templates/helpers.go.tmplM internal/generator/templates/root.go.tmplM internal/generator/templates/search.go.tmplM internal/generator/templates/store.go.tmplM internal/generator/templates/workflows/pm_load.go.tmplM internal/generator/templates/workflows/pm_orphans.go.tmplM internal/generator/templates/workflows/pm_stale.go.tmplM internal/pipeline/scorecard.goM skills/printing-press/SKILL.md
Diff
commit a8a003daf7ca0a28244de3b563eba8fb7d133f27
Author: Matt Van Horn <mvanhorn@users.noreply.github.com>
Date: Sun Mar 29 09:19:51 2026 -0700
feat(generator): make generated CLIs agent-native by default (#43)
Templates now generate CLIs optimized for AI agent consumption:
- --compact uses blocklist on single objects (strips description, body,
comments, attachments) instead of allowlist — works across all API domains
- --agent meta-flag sets --json --compact --no-input --no-color --yes
- Analytics commands (stale, orphans, load) have --limit flag (default 50)
with total count in output to prevent unbounded context dumps
- Load command resolves assignee names from synced users instead of
outputting raw Go map debug format
- Search iterates per-table FTS individually to prevent duplicates,
filters nil/empty entries
- Store template includes IsUUID() and ResolveByName() for name-or-ID
resolution from synced data (with ambiguity detection)
- Scorecard's AgentNative dimension extended with token efficiency checks
- SKILL.md adds npm/PyPI SDK search to research brief and absorb gate,
plus search dedup guidance for build phase
Co-authored-by: Matt Van Horn <mvanhorn@MacBook-Pro.local>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
internal/generator/templates/helpers.go.tmpl | 65 +++++++++++------
internal/generator/templates/root.go.tmpl | 23 ++++++
internal/generator/templates/search.go.tmpl | 63 +++++++++++++++++
internal/generator/templates/store.go.tmpl | 61 ++++++++++++++++
.../generator/templates/workflows/pm_load.go.tmpl | 82 ++++++++++++++++++++--
.../templates/workflows/pm_orphans.go.tmpl | 20 +++++-
.../generator/templates/workflows/pm_stale.go.tmpl | 29 +++++++-
internal/pipeline/scorecard.go | 21 +++++-
skills/printing-press/SKILL.md | 17 +++--
9 files changed, 341 insertions(+), 40 deletions(-)
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 5b7fe427..87720dfc 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -319,44 +319,63 @@ func printOutputWithFlags(w io.Writer, data json.RawMessage, flags *rootFlags) e
}
// compactFields keeps only the most important fields for agent consumption.
+// For arrays: allowlist of high-gravity fields (no descriptions).
+// For single objects: blocklist that strips known-verbose fields (descriptions, comments, etc.).
func compactFields(data json.RawMessage) json.RawMessage {
+ // Try array first
+ var items []map[string]any
+ if err := json.Unmarshal(data, &items); err == nil {
+ return compactListFields(items)
+ }
+
+ // Single object — use blocklist
+ var obj map[string]any
+ if err := json.Unmarshal(data, &obj); err == nil {
+ return compactObjectFields(obj)
+ }
+
+ return data
+}
+
+// compactListFields keeps only high-gravity fields for array responses.
+func compactListFields(items []map[string]any) json.RawMessage {
keepFields := map[string]bool{
"id": true, "name": true, "title": true, "identifier": true,
"status": true, "state": true, "type": true, "priority": true,
"url": true, "email": true, "key": true,
"created_at": true, "updated_at": true, "createdAt": true, "updatedAt": true,
- "description": true,
}
- var items []map[string]any
- if err := json.Unmarshal(data, &items); err == nil {
- filtered := make([]map[string]any, 0, len(items))
- for _, item := range items {
- compact := map[string]any{}
- for k, v := range item {
- if keepFields[k] {
- compact[k] = v
- }
- }
- filtered = append(filtered, compact)
- }
- result, _ := json.Marshal(filtered)
- return result
- }
-
- var obj map[string]any
- if err := json.Unmarshal(data, &obj); err == nil {
+ filtered := make([]map[string]any, 0, len(items))
+ for _, item := range items {
compact := map[string]any{}
- for k, v := range obj {
+ for k, v := range item {
if keepFields[k] {
compact[k] = v
}
}
- result, _ := json.Marshal(compact)
- return result
+ filtered = append(filtered, compact)
}
+ result, _ := json.Marshal(filtered)
+ return result
+}
- return data
+// compactObjectFields strips known-verbose fields from single-object responses.
+// Uses a blocklist so it works across all API domains (project management, payments, CRM, etc.).
+func compactObjectFields(obj map[string]any) json.RawMessage {
+ stripFields := map[string]bool{
+ "description": true, "body": true, "content": true,
+ "comments": true, "attachments": true, "html": true, "markdown": true,
+ }
+
+ compact := map[string]any{}
+ for k, v := range obj {
+ if !stripFields[k] {
+ compact[k] = v
+ }
+ }
+ result, _ := json.Marshal(compact)
+ return result
}
// printCSV renders JSON arrays as CSV with header row.
diff --git a/internal/generator/templates/root.go.tmpl b/internal/generator/templates/root.go.tmpl
index d9604cd3..ddf52437 100644
--- a/internal/generator/templates/root.go.tmpl
+++ b/internal/generator/templates/root.go.tmpl
@@ -27,6 +27,7 @@ type rootFlags struct {
noCache bool
noInput bool
yes bool
+ agent bool
selectFields string
configPath string
timeout time.Duration
@@ -59,6 +60,28 @@ func Execute() error {
rootCmd.PersistentFlags().BoolVar(&flags.yes, "yes", false, "Skip confirmation prompts (for agents and scripts)")
rootCmd.PersistentFlags().BoolVar(&noColor, "no-color", false, "Disable colored output")
rootCmd.PersistentFlags().BoolVar(&humanFriendly, "human-friendly", false, "Enable colored output and rich formatting")
+ rootCmd.PersistentFlags().BoolVar(&flags.agent, "agent", false, "Set all agent-friendly defaults (--json --compact --no-input --no-color --yes)")
+
+ rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
+ if flags.agent {
+ if !cmd.Flags().Changed("json") {
+ flags.asJSON = true
+ }
+ if !cmd.Flags().Changed("compact") {
+ flags.compact = true
+ }
+ if !cmd.Flags().Changed("no-input") {
+ flags.noInput = true
+ }
+ if !cmd.Flags().Changed("yes") {
+ flags.yes = true
+ }
+ if !cmd.Flags().Changed("no-color") {
+ noColor = true
+ }
+ }
+ return nil
+ }
{{- range $name, $resource := .Resources}}
rootCmd.AddCommand(new{{camel $name}}Cmd(&flags)) {{/* FuncPrefix matches resource name for top-level */}}
diff --git a/internal/generator/templates/search.go.tmpl b/internal/generator/templates/search.go.tmpl
index e2d9b53a..3d080c32 100644
--- a/internal/generator/templates/search.go.tmpl
+++ b/internal/generator/templates/search.go.tmpl
@@ -8,11 +8,36 @@ import (
"fmt"
"os"
"path/filepath"
+ "strings"
"github.com/{{.Owner}}/{{.Name}}-pp-cli/internal/store"
"github.com/spf13/cobra"
)
+// isNilOrEmpty checks whether a JSON object has nil or empty values for
+// common identifier fields (title, name, identifier, id).
+func isNilOrEmpty(raw json.RawMessage) bool {
+ var obj map[string]interface{}
+ if err := json.Unmarshal(raw, &obj); err != nil {
+ return true
+ }
+ for _, key := range []string{"title", "name", "identifier", "id"} {
+ if v, ok := obj[key]; ok {
+ if v == nil {
+ continue
+ }
+ if s, ok := v.(string); ok && strings.TrimSpace(s) != "" {
+ return false
+ }
+ // Non-string, non-nil value (e.g. numeric ID) — keep it
+ if _, ok := v.(string); !ok {
+ return false
+ }
+ }
+ }
+ return true
+}
+
func newSearchCmd(flags *rootFlags) *cobra.Command {
var resourceType string
var limit int
@@ -54,14 +79,52 @@ Data must be synced first with the sync command. Searches are instant
case "{{.Name}}":
results, err = db.Search{{pascal .Name}}(query, limit)
{{- end}}
+{{- end}}
+ case "":
+ // Search all FTS-enabled tables individually to avoid duplicates.
+ // Do NOT use db.Search() here — it queries resources_fts which
+ // overlaps with per-table FTS indexes and causes duplicate results.
+ seen := make(map[string]bool)
+ _ = seen // prevent unused error when no FTS tables exist
+{{- range .Tables}}
+{{- if .FTS5}}
+ {
+ partial, searchErr := db.Search{{pascal .Name}}(query, limit)
+ if searchErr != nil {
+ return fmt.Errorf("search {{.Name}} failed: %w", searchErr)
+ }
+ for _, r := range partial {
+ key := string(r)
+ if !seen[key] {
+ seen[key] = true
+ results = append(results, r)
+ }
+ }
+ }
+{{- end}}
{{- end}}
default:
+ // Unrecognized type — fall back to generic search
results, err = db.Search(query, limit)
}
if err != nil {
return fmt.Errorf("search failed: %w", err)
}
+ // Filter out entries with nil or empty identifier fields.
+ filtered := make([]json.RawMessage, 0, len(results))
+ for _, r := range results {
+ if !isNilOrEmpty(r) {
+ filtered = append(filtered, r)
+ }
+ }
+ results = filtered
+
+ // Enforce limit across aggregated results.
+ if len(results) > limit {
+ results = results[:limit]
+ }
+
if len(results) == 0 {
fmt.Fprintf(os.Stderr, "No results for %q\n", query)
return nil
diff --git a/internal/generator/templates/store.go.tmpl b/internal/generator/templates/store.go.tmpl
index 743824c0..da00087a 100644
--- a/internal/generator/templates/store.go.tmpl
+++ b/internal/generator/templates/store.go.tmpl
@@ -12,12 +12,20 @@ import (
"fmt"
"os"
"path/filepath"
+ "regexp"
"strings"
"time"
_ "modernc.org/sqlite"
)
+var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
+
+// IsUUID returns true if the input looks like a UUID.
+func IsUUID(s string) bool {
+ return uuidPattern.MatchString(s)
+}
+
type Store struct {
db *sql.DB
path string
@@ -457,3 +465,56 @@ func (s *Store) Status() (map[string]int, error) {
}
return status, rows.Err()
}
+
+// ResolveByName resolves a human-readable name to a UUID from synced data.
+// If the input is already a UUID, it is returned as-is.
+// matchFields are JSON field names to search against (e.g., "name", "key", "email").
+func (s *Store) ResolveByName(resourceType string, input string, matchFields ...string) (string, error) {
+ if IsUUID(input) {
+ return input, nil
+ }
+
+ var matches []string
+ for _, field := range matchFields {
+ query := fmt.Sprintf(
+ `SELECT id FROM resources WHERE resource_type = ? AND LOWER(json_extract(data, '$.%s')) = LOWER(?)`,
+ field,
+ )
+ rows, err := s.db.Query(query, resourceType, input)
+ if err != nil {
+ continue
+ }
+ for rows.Next() {
+ var id string
+ if rows.Scan(&id) == nil {
+ // Deduplicate
+ found := false
+ for _, m := range matches {
+ if m == id {
+ found = true
+ break
+ }
+ }
+ if !found {
+ matches = append(matches, id)
+ }
+ }
+ }
+ rows.Close()
+ }
+
+ switch len(matches) {
+ case 0:
+ return "", fmt.Errorf("%s %q not found in local store. Run 'sync' first, or use the UUID directly", resourceType, input)
+ case 1:
+ return matches[0], nil
+ default:
+ hint := matches[0]
+ if len(matches) > 5 {
+ hint = strings.Join(matches[:5], ", ") + "..."
+ } else {
+ hint = strings.Join(matches, ", ")
+ }
+ return "", fmt.Errorf("ambiguous: %q matches %d %s entries (%s). Use the exact UUID instead", input, len(matches), resourceType, hint)
+ }
+}
diff --git a/internal/generator/templates/workflows/pm_load.go.tmpl b/internal/generator/templates/workflows/pm_load.go.tmpl
index bc6e8b2b..18bbfe5f 100644
--- a/internal/generator/templates/workflows/pm_load.go.tmpl
+++ b/internal/generator/templates/workflows/pm_load.go.tmpl
@@ -16,6 +16,7 @@ import (
func newLoadCmd(flags *rootFlags) *cobra.Command {
var dbPath string
+ var limit int
cmd := &cobra.Command{
Use: "load",
@@ -25,6 +26,9 @@ person. Helps identify overloaded team members and unbalanced workload.`,
Example: ` # Show workload distribution
{{.Name}}-pp-cli load
+ # Limit results
+ {{.Name}}-pp-cli load --limit 10
+
# Output as JSON
{{.Name}}-pp-cli load --json`,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -39,6 +43,36 @@ person. Helps identify overloaded team members and unbalanced workload.`,
}
defer db.Close()
+ // Build user name lookup from synced users
+ userNames := map[string]string{}
+ userRows, err := db.Query(`SELECT data FROM resources WHERE resource_type = 'users'`)
+ if err == nil {
+ defer userRows.Close()
+ for userRows.Next() {
+ var userData []byte
+ if userRows.Scan(&userData) != nil {
+ continue
+ }
+ var u map[string]any
+ if json.Unmarshal(userData, &u) != nil {
+ continue
+ }
+ uid := ""
+ if id, ok := u["id"]; ok {
+ uid = fmt.Sprintf("%v", id)
+ }
+ if uid == "" {
+ continue
+ }
+ for _, nk := range []string{"name", "displayName", "display_name"} {
+ if n, ok := u[nk]; ok && n != nil {
+ userNames[uid] = fmt.Sprintf("%v", n)
+ break
+ }
+ }
+ }
+ }
+
rows, err := db.Query(`SELECT id, resource_type, data FROM resources`)
if err != nil {
return fmt.Errorf("querying items: %w", err)
@@ -68,10 +102,33 @@ person. Helps identify overloaded team members and unbalanced workload.`,
assignee := ""
for _, key := range []string{"assigneeId", "assignee_id", "assignee", "ownerId", "owner_id", "owner"} {
- if v, ok := obj[key]; ok && v != nil {
- assignee = fmt.Sprintf("%v", v)
- break
+ v, ok := obj[key]
+ if !ok || v == nil {
+ continue
+ }
+ // Handle nested object: {id: "...", name: "Sheldon"}
+ if m, ok := v.(map[string]any); ok {
+ for _, nameKey := range []string{"name", "displayName", "display_name"} {
+ if n, ok := m[nameKey]; ok && n != nil {
+ assignee = fmt.Sprintf("%v", n)
+ break
+ }
+ }
+ if assignee == "" {
+ if aid, ok := m["id"]; ok {
+ assignee = fmt.Sprintf("%v", aid)
+ }
+ }
+ } else {
+ // Flat string — try to resolve from user lookup
+ raw := fmt.Sprintf("%v", v)
+ if name, ok := userNames[raw]; ok {
+ assignee = name
+ } else {
+ assignee = raw
+ }
}
+ break
}
if assignee == "" || assignee == "<nil>" {
assignee = "(unassigned)"
@@ -88,9 +145,14 @@ person. Helps identify overloaded team members and unbalanced workload.`,
sort.Slice(entries, func(i, j int) bool { return entries[i].Count > entries[j].Count })
if flags.asJSON {
+ shown := entries
+ if limit > 0 && len(shown) > limit {
+ shown = shown[:limit]
+ }
result := map[string]any{
- "total": total,
- "distribution": entries,
+ "total_count": total,
+ "showing": len(shown),
+ "distribution": shown,
}
enc := json.NewEncoder(cmd.OutOrStdout())
enc.SetIndent("", " ")
@@ -102,10 +164,15 @@ person. Helps identify overloaded team members and unbalanced workload.`,
return nil
}
- fmt.Fprintf(cmd.OutOrStdout(), "Workload Distribution (%d total items):\n\n", total)
+ shown := entries
+ if limit > 0 && len(shown) > limit {
+ shown = shown[:limit]
+ }
+
+ fmt.Fprintf(cmd.OutOrStdout(), "Workload Distribution (%d total items, showing %d):\n\n", total, len(shown))
fmt.Fprintf(cmd.OutOrStdout(), "%-40s %s\n", "ASSIGNEE", "COUNT")
fmt.Fprintf(cmd.OutOrStdout(), "%-40s %s\n", "--------", "-----")
- for _, entry := range entries {
+ for _, entry := range shown {
fmt.Fprintf(cmd.OutOrStdout(), "%-40s %d\n", entry.Assignee, entry.Count)
}
return nil
@@ -113,6 +180,7 @@ person. Helps identify overloaded team members and unbalanced workload.`,
}
cmd.Flags().StringVar(&dbPath, "db", "", "Database path (default: ~/.config/{{.Name}}-pp-cli/store.db)")
+ cmd.Flags().IntVar(&limit, "limit", 50, "Maximum entries to show")
return cmd
}
diff --git a/internal/generator/templates/workflows/pm_orphans.go.tmpl b/internal/generator/templates/workflows/pm_orphans.go.tmpl
index 65beb1ea..fefe3b18 100644
--- a/internal/generator/templates/workflows/pm_orphans.go.tmpl
+++ b/internal/generator/templates/workflows/pm_orphans.go.tmpl
@@ -16,6 +16,7 @@ import (
func newOrphansCmd(flags *rootFlags) *cobra.Command {
var dbPath string
+ var limit int
cmd := &cobra.Command{
Use: "orphans",
@@ -113,10 +114,20 @@ such as assignee, project, priority, or labels. Useful for triaging unowned work
})
}
+ totalCount := len(items)
+ if limit > 0 && len(items) > limit {
+ items = items[:limit]
+ }
+
if flags.asJSON {
+ result := map[string]any{
+ "total_count": totalCount,
+ "showing": len(items),
+ "items": items,
+ }
enc := json.NewEncoder(cmd.OutOrStdout())
enc.SetIndent("", " ")
- return enc.Encode(items)
+ return enc.Encode(result)
}
if len(items) == 0 {
@@ -124,7 +135,11 @@ such as assignee, project, priority, or labels. Useful for triaging unowned work
return nil
}
- fmt.Fprintf(cmd.OutOrStdout(), "Found %d orphaned items:\n\n", len(items))
+ if totalCount > len(items) {
+ fmt.Fprintf(cmd.OutOrStdout(), "Found %d orphaned items (showing first %d):\n\n", totalCount, len(items))
+ } else {
+ fmt.Fprintf(cmd.OutOrStdout(), "Found %d orphaned items:\n\n", len(items))
+ }
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-15s %-30s %s\n", "ID", "TYPE", "MISSING", "TITLE")
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-15s %-30s %s\n", "----", "----", "-------", "-----")
for _, item := range items {
@@ -140,6 +155,7 @@ such as assignee, project, priority, or labels. Useful for triaging unowned work
}
cmd.Flags().StringVar(&dbPath, "db", "", "Database path (default: ~/.config/{{.Name}}-pp-cli/store.db)")
+ cmd.Flags().IntVar(&limit, "limit", 50, "Maximum items to show")
return cmd
}
diff --git a/internal/generator/templates/workflows/pm_stale.go.tmpl b/internal/generator/templates/workflows/pm_stale.go.tmpl
index 5a67ac06..c6850e0c 100644
--- a/internal/generator/templates/workflows/pm_stale.go.tmpl
+++ b/internal/generator/templates/workflows/pm_stale.go.tmpl
@@ -18,6 +18,7 @@ func newStaleCmd(flags *rootFlags) *cobra.Command {
var days int
var team string
var dbPath string
+ var limit int
cmd := &cobra.Command{
Use: "stale",
@@ -62,6 +63,20 @@ the specified number of days. Useful for identifying forgotten or blocked work.`
query += ` ORDER BY json_extract(data, '$.updatedAt') ASC, json_extract(data, '$.updated_at') ASC`
+ // Get total count first
+ countQuery := "SELECT COUNT(*) FROM (" + query + ")"
+ var totalCount int
+ countRow, countErr := db.Query(countQuery, qArgs...)
+ if countErr == nil {
+ defer countRow.Close()
+ if countRow.Next() {
+ countRow.Scan(&totalCount)
+ }
+ }
+
+ query += ` LIMIT ?`
+ qArgs = append(qArgs, limit)
+
rows, err := db.Query(query, qArgs...)
if err != nil {
return fmt.Errorf("querying stale items: %w", err)
@@ -119,9 +134,14 @@ the specified number of days. Useful for identifying forgotten or blocked work.`
}
if flags.asJSON {
+ result := map[string]any{
+ "total_count": totalCount,
+ "showing": len(items),
+ "items": items,
+ }
enc := json.NewEncoder(cmd.OutOrStdout())
enc.SetIndent("", " ")
- return enc.Encode(items)
+ return enc.Encode(result)
}
if len(items) == 0 {
@@ -129,7 +149,11 @@ the specified number of days. Useful for identifying forgotten or blocked work.`
return nil
}
- fmt.Fprintf(cmd.OutOrStdout(), "Found %d stale items (no update in %d+ days):\n\n", len(items), days)
+ if totalCount > len(items) {
+ fmt.Fprintf(cmd.OutOrStdout(), "Found %d stale items (showing first %d, no update in %d+ days):\n\n", totalCount, len(items), days)
+ } else {
+ fmt.Fprintf(cmd.OutOrStdout(), "Found %d stale items (no update in %d+ days):\n\n", len(items), days)
+ }
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-15s %-8s %s\n", "ID", "TYPE", "DAYS", "TITLE")
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-15s %-8s %s\n", "----", "----", "----", "-----")
for _, item := range items {
@@ -146,6 +170,7 @@ the specified number of days. Useful for identifying forgotten or blocked work.`
cmd.Flags().IntVar(&days, "days", 30, "Number of days without update to consider stale")
cmd.Flags().StringVar(&team, "team", "", "Filter by team identifier")
cmd.Flags().StringVar(&dbPath, "db", "", "Database path (default: ~/.config/{{.Name}}-pp-cli/store.db)")
+ cmd.Flags().IntVar(&limit, "limit", 50, "Maximum items to show")
return cmd
}
diff --git a/internal/pipeline/scorecard.go b/internal/pipeline/scorecard.go
index 039915f6..b2bcea11 100644
--- a/internal/pipeline/scorecard.go
+++ b/internal/pipeline/scorecard.go
@@ -503,8 +503,25 @@ func scoreAgentNative(dir string) int {
}
}
if stdinExamples >= 3 {
- score += 2
- } else if stdinExamples >= 1 {
+ score++
+ }
+ // Token efficiency: --agent meta-flag
+ if strings.Contains(rootContent, `"agent"`) && strings.Contains(rootContent, "PersistentPreRun") {
+ score++
+ }
+ // Token efficiency: --compact strips verbose fields on single objects (blocklist approach)
+ if strings.Contains(helpersContent, "compactObjectFields") || strings.Contains(helpersContent, "stripVerboseFields") {
+ score++
+ }
+ // Token efficiency: analytics commands have --limit flag
+ staleContent := readFileContent(filepath.Join(dir, "internal", "cli", "pm_stale.go"))
+ loadContent := readFileContent(filepath.Join(dir, "internal", "cli", "pm_load.go"))
+ if (strings.Contains(staleContent, `"limit"`) || staleContent == "") && (strings.Contains(loadContent, `"limit"`) || loadContent == "") {
+ score++
+ }
+ // Token efficiency: store has ResolveByName for name-or-ID resolution
+ storeContent := readFileContent(filepath.Join(dir, "internal", "store", "store.go"))
+ if strings.Contains(storeContent, "ResolveByName") || strings.Contains(storeContent, "IsUUID") {
score++
}
if score > 10 {
diff --git a/skills/printing-press/SKILL.md b/skills/printing-press/SKILL.md
index fc2720f4..1312b2ba 100644
--- a/skills/printing-press/SKILL.md
+++ b/skills/printing-press/SKILL.md
@@ -258,6 +258,7 @@ The brief must answer:
Research checklist:
- Find the spec or docs source
- Find the top 1-2 competitors
+- Find official and popular SDK wrappers on npm (`site:npmjs.com`) and PyPI (`site:pypi.org`)
- Find 2-3 concrete user pain points
- Identify the highest-gravity entities
- Pick the top 3-5 commands that matter most
@@ -398,6 +399,8 @@ Run these searches in parallel:
6. **WebFetch**: Check `github.com/anthropics/claude-plugins-official/tree/main/external_plugins` for official plugin
7. **WebSearch**: `"<API name>" MCP site:lobehub.com OR site:mcpmarket.com OR site:fastmcp.me`
8. **WebSearch**: `"<API name>" automation script workflow site:github.com`
+9. **WebSearch**: `"<API name>" SDK wrapper site:npmjs.com`
+10. **WebSearch**: `"<API name>" client library site:pypi.org`
### Step 1.5b: Catalog every feature into the absorb manifest
@@ -416,6 +419,8 @@ For EACH tool found, list EVERY feature/tool/command it provides. Then define ho
Every row = a feature we MUST build. No exceptions. If someone else has it, we have it AND it works offline, with --json, --dry-run, typed exit codes, and SQLite persistence.
+SDK wrapper methods should be treated as features to absorb — each public method/function is a feature the CLI should match.
+
### Step 1.5c: Identify transcendence features
What compound use cases become possible ONLY when ALL absorbed features live in SQLite together?
@@ -537,6 +542,10 @@ After building each command in Priority 1 and Priority 2, verify these 7 princip
6. **Composability**: Exit codes are typed (0/2/3/4/5/7), output pipes to `jq` cleanly
7. **Bounded responses**: `--compact` returns only high-gravity fields, list commands have `--limit`
+### Search Dedup Rule
+
+When building cross-entity search commands, use per-table FTS search methods individually. Do NOT combine per-table search with the generic `db.Search()` — this causes duplicate results because the same entities exist in both `resources_fts` and per-table FTS indexes.
+
### Priority 1 Review Gate
After completing ALL Priority 1 (absorbed) features, BEFORE starting Priority 2 (transcendence):
@@ -645,15 +654,15 @@ If the next research step does not change those answers, stop and generate.
Do not:
- write 5 separate mandatory research documents
-- defer all workflows to “future work”
+- defer all workflows to "future work"
- skip verification because the CLI compiles
- treat scorecard alone as ship proof
- discover YAML/URL spec incompatibility late and manually convert specs if the tools can already consume them
- rerun the whole late-phase gauntlet for cosmetic README polish
-- skip features because “the MCP already handles that” (absorb everything, beat it with offline + agent-native)
-- build only “top 3-5 workflows” when the absorb manifest has 15+ (build them ALL, then transcend)
+- skip features because "the MCP already handles that" (absorb everything, beat it with offline + agent-native)
+- build only "top 3-5 workflows" when the absorb manifest has 15+ (build them ALL, then transcend)
- generate before the Phase 1.5 Ecosystem Absorb Gate is approved
-- call a CLI “GOAT” without matching every feature the best competitor has
+- call a CLI "GOAT" without matching every feature the best competitor has
### What counts as success
← 334a52af feat(skills): integrate sniff into printing-press skill work
·
back to Cli Printing Press
·
feat(skills): add browser-use as primary sniff capture backe 82bc5a37 →