Class: Pikuri::Os::FileindexSearch

Inherits:
Tool
  • Object
show all
Defined in:
lib/pikuri/os/fileindex_search.rb

Overview

The fileindex_search tool — a fast, cheap keyword lookup over a desktop file index, returning matching paths each with the single snippet the index provides. A Tool in the Workspace::Search::Grep shape (shells out through the backend, fails loud at construction if the binary is missing).

The backend: is a stateless module satisfying the file-index duck type (see #initialize) — LocalSearch or Recoll, whichever Extension finds usable. Backend-specific text (the blind-spot note, the error label) is read from the backend, so a new index arm ships its own without touching the tool.

It's a locator, not a window: the index reports at most one match per file, so the tool previews one match each and tells the LLM to read the file for full content / other matches. It sees inside PDFs and office docs (indexed at extract time), and being an index lookup is cheap to run repeatedly.

Sharing: P_stateless — the backend is a stateless module and each call shells out to its own query, so one instance serves any number of agents.

Constant Summary collapse

DEFAULT_LIMIT =

Returns default / max matching files returned.

Returns:

  • (Integer)

    default / max matching files returned.

10
MAX_LIMIT =
25
MAX_BYTES =

Returns hard byte cap on combined output (matches Workspace::Search::Grep::MAX_BYTES).

Returns:

  • (Integer)

    hard byte cap on combined output (matches Workspace::Search::Grep::MAX_BYTES).

50 * 1024
MAX_BYTES_LABEL =

Returns human-readable form of MAX_BYTES.

Returns:

  • (String)

    human-readable form of MAX_BYTES.

"#{MAX_BYTES / 1024} KB"
DESCRIPTION =

Returns opencode-shape description (summary + Usage), backend-neutral. The runtime tool description is this constant plus the backend's #limitations, appended at construction (see #initialize) — and it is that appended text, not this constant, that states which folders the index actually covers and what it misses (recoll lists its topdirs; localsearch notes it ranks across the whole home). So keep coverage claims out of here; they belong with the backend that knows them.

Returns:

  • (String)

    opencode-shape description (summary + Usage), backend-neutral. The runtime tool description is this constant plus the backend's #limitations, appended at construction (see #initialize) — and it is that appended text, not this constant, that states which folders the index actually covers and what it misses (recoll lists its topdirs; localsearch notes it ranks across the whole home). So keep coverage claims out of here; they belong with the backend that knows them.

<<~DESC
  Find files whose contents contain your words or phrase, using the desktop search index.

  Usage:
  - Searches inside PDFs, Word/ODT/spreadsheet documents and plain text — reach for this when the answer may live in a file's contents.
  - Keyword match (and word stems); type the actual words that would appear in the file.
  - The index returns at most ONE match per file and may be slightly stale — this is a locator, not the full story. To read a file's full, current text, read it (see fileindex_read).
  - Cheap: it queries a prebuilt index rather than walking the filesystem, so it barely touches the disk of the machine the user is on — fine to run many times to narrow down.
  - Only indexed folders are covered; system paths like /etc or /var are not — search those with a text/regex tool instead.
  - Output is truncated to #{MAX_BYTES_LABEL}; narrow the query if it ends in a truncation marker.
DESC

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(backend:) ⇒ FileindexSearch

Parameters:

  • backend (Module)

    a file-index backend (LocalSearch / Recoll) — must respond to #search, #read_text, #check_binaries!, #limitations, #label and define a CommandError.

Raises:

  • (RuntimeError)

    if the backend's binary isn't on PATH.



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/pikuri/os/fileindex_search.rb', line 63

def initialize(backend:)
  @backend = backend
  backend.check_binaries!
  super(
    name: 'fileindex_search',
    description: "#{DESCRIPTION}\n#{backend.limitations}",
    parameters: Parameters.build { |p|
      p.required_string :query,
                        'Words or a phrase to find in file contents, ' \
                        'e.g. "tax return 2024".'
      p.optional_integer :limit,
                         "Max number of matching files to return " \
                         "(default #{DEFAULT_LIMIT}, max #{MAX_LIMIT}), e.g. 10."
    },
    execute: lambda { |query:, limit: DEFAULT_LIMIT|
      FileindexSearch.search(query: query, limit: limit, backend: backend)
    },
    # Private and hard untrusted, both fixed rather than derived: this searches
    # the *whole machine's* file index, which is the AllowAll case by
    # construction — a reachable set nobody can vouch for, holding whatever the
    # user keeps on disk.
    trifecta_legs: Pikuri::Tool::TrifectaLegs.new(private: true, untrusted: :hard, egress_payload_review: :no_egress)
  )
end

Class Method Details

.search(query:, backend:, limit: DEFAULT_LIMIT) ⇒ String

Returns formatted hits + disclaimer, a no-match message, or "Error: ...".

Parameters:

  • query (String)
  • limit (Integer) (defaults to: DEFAULT_LIMIT)
  • backend (Module)

    the file-index backend to query

Returns:

  • (String)

    formatted hits + disclaimer, a no-match message, or "Error: ..."



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/pikuri/os/fileindex_search.rb', line 93

def self.search(query:, backend:, limit: DEFAULT_LIMIT)
  q = query.to_s.strip
  return 'Error: empty query.' if q.empty?

  limit = clamp(limit, 1, MAX_LIMIT)
  hits = backend.search(query: q, limit: limit)
  return no_match_message(q) if hits.empty?

  content, marker = head_truncate(hits.map { |hit| render_hit(hit) }.join("\n"))
  [content.chomp, '', disclaimer(hits.size) + marker].join("\n")
rescue backend::CommandError => e
  # Cap the error too — a raw subprocess error returned verbatim is
  # the shape that overflows the model's context; share the success
  # path's ceiling.
  content, marker = head_truncate(e.message)
  "Error: #{backend.label}: #{content}#{marker}"
end