context_spook 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +103 -12
  3. data/Rakefile +0 -1
  4. data/bin/context_spook +6 -3
  5. data/context_spook.gemspec +7 -8
  6. data/lib/context_spook/generator.rb +75 -45
  7. data/lib/context_spook/output_context.rb +2 -2
  8. data/lib/context_spook/toon/encoder.rb +628 -0
  9. data/lib/context_spook/toon/example.md +36 -66
  10. data/lib/context_spook/toon.rb +13 -15
  11. data/lib/context_spook/utils.rb +14 -9
  12. data/lib/context_spook/version.rb +1 -1
  13. data/spec/context_spook/generator_spec.rb +63 -2
  14. data/spec/context_spook/toon_encoder_spec.rb +90 -0
  15. data/spec/context_spook/toon_spec.rb +15 -16
  16. data/spec/examples/README.md +79 -0
  17. data/spec/examples/conversions/keyed-tabular-objects.json +19 -0
  18. data/spec/examples/conversions/keyed-tabular-objects.toon +4 -0
  19. data/spec/examples/conversions/nested-field-groups.json +28 -0
  20. data/spec/examples/conversions/nested-field-groups.toon +4 -0
  21. data/spec/examples/conversions/tabular-arrays.json +22 -0
  22. data/spec/examples/conversions/tabular-arrays.toon +4 -0
  23. data/spec/examples/invalid/delimiter-mismatch.toon +3 -0
  24. data/spec/examples/invalid/length-mismatch.toon +3 -0
  25. data/spec/examples/invalid/multiple-root-primitives.toon +2 -0
  26. data/spec/examples/valid/comments.toon +5 -0
  27. data/spec/examples/valid/delimiter-scoping.toon +5 -0
  28. data/spec/examples/valid/mixed-arrays.toon +5 -0
  29. data/spec/examples/valid/nested-objects.toon +9 -0
  30. data/spec/examples/valid/objects.toon +6 -0
  31. data/spec/examples/valid/pipe-delimiter.toon +3 -0
  32. data/spec/examples/valid/primitive-arrays.toon +3 -0
  33. data/spec/examples/valid/tab-delimiter.toon +3 -0
  34. data/spec/fixtures/encode/arrays-nested.json +120 -0
  35. data/spec/fixtures/encode/arrays-objects.json +160 -0
  36. data/spec/fixtures/encode/arrays-primitive.json +95 -0
  37. data/spec/fixtures/encode/arrays-tabular.json +182 -0
  38. data/spec/fixtures/encode/delimiters.json +214 -0
  39. data/spec/fixtures/encode/objects-keyed.json +160 -0
  40. data/spec/fixtures/encode/objects.json +288 -0
  41. data/spec/fixtures/encode/primitives.json +262 -0
  42. data/spec/fixtures/encode/whitespace.json +54 -0
  43. metadata +35 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b21e74b5db171841cc7af7c0e5204f252cb7474de2f45512c3597f61774ffcb
4
- data.tar.gz: 3068b2faa4a58ef85722f9e756b712d89e45127eff52d7ca9695535611e04670
3
+ metadata.gz: 3992c09d3428a0268385de6fbf268f96b92ff8cc8f84cc11d464e3c1cd9c1cfe
4
+ data.tar.gz: d8cbd80e0ca8a3df54f464d8eb120dd8d9864b51e1aafc12b23f60e9e8b42eef
5
5
  SHA512:
6
- metadata.gz: 41f8ac31162e123cf27b6ad109bcd7cc58c6b1e687ac1e4f3af57e6e446c1ebc07ab4a4290af659e7f5e8cd3026f2342a53c109550628bb4f2cf536ec1aea646
7
- data.tar.gz: dde406feb3c651bd99ea4a0aa4c10342690459604fb27c7e1d2d3df0302326dbabb83ef78350f860b23773bbd5d56ef31fcb66841b595be937006ae29160dbd8
6
+ metadata.gz: d186a38e1e0285498700f73d790ed0caa3fd4e19e38565c21f39657320311d1bf37bf32405ea176c44e8d7e4b8ef3978d853ce35f89a1e922ecbbf739b2dafa6
7
+ data.tar.gz: 9a86582d2c45a68a913ae82f64e76ca2235e96d4d3250901b49eced799ff7d236b08f869fe1dc20fe4c3453c21d35e4cb7e9c8b42c67d8ed67c7bb79d3e469f5
data/README.md CHANGED
@@ -3,7 +3,10 @@
3
3
  ContextSpook is a Ruby library that collects and organizes project information
4
4
  to help AI assistants understand codebases better. It provides a
5
5
  domain-specific language (DSL) for describing project context, which can then
6
- be exported as structured JSON data.
6
+ be exported as structured JSON or TOON (Token-Oriented Object Notation) data.
7
+
8
+ TOON output is produced by a native encoder conformant to the TOON v4.4
9
+ specification, validated against 186 official conformance fixtures.
7
10
 
8
11
  The DSL is general-purpose and can be used for any kind of project or
9
12
  collection of files, whether software development, documentation, research
@@ -14,7 +17,8 @@ domains.
14
17
 
15
18
  ## Documentation
16
19
 
17
- Complete API documentation is available at: [GitHub.io](https://flori.github.io/context_spook/)
20
+ Complete API documentation is available at:
21
+ [GitHub.io](https://flori.github.io/context_spook/)
18
22
 
19
23
  ## Installation
20
24
 
@@ -25,11 +29,13 @@ gem 'context_spook'
25
29
  ```
26
30
 
27
31
  And then execute:
32
+
28
33
  ```bash
29
34
  $ bundle install
30
35
  ```
31
36
 
32
37
  Or install it yourself as:
38
+
33
39
  ```bash
34
40
  $ gem install context_spook
35
41
  ```
@@ -72,7 +78,18 @@ application.
72
78
  ```ruby
73
79
  File.write 'context.json', context.to_json
74
80
  ```
81
+ You can also generate a manifest-only context by passing `manifest: true`.
82
+ File entries are reduced to their path plus metadata (size, tokens, MIME
83
+ types, tags), giving the model a lightweight index of file paths it can
84
+ use with `read_file` to load only what it needs:
75
85
 
86
+ ```ruby
87
+ context = ContextSpook::generate_context(manifest: true) do
88
+ context do
89
+ file 'lib/context_spook.rb'
90
+ end
91
+ end
92
+ ```
76
93
  #### From a context definition file
77
94
 
78
95
  Alternatively store the block's content above to a file `.contexts/project.rb`:
@@ -136,6 +153,28 @@ Or pipe directly to another tool:
136
153
  context_spook .contexts/project.rb | ollama_chat_send
137
154
  ```
138
155
 
156
+ You can choose the output format with the `-F` flag (JSON is the default):
157
+
158
+ ```bash
159
+ # Generate context as TOON
160
+ context_spook .contexts/project.rb -F TOON > context.toon
161
+
162
+ # Save TOON directly to a file
163
+ context_spook .contexts/project.rb -F TOON -o context.toon
164
+ ```
165
+ You can use manifest mode with `-m` to omit file content. Each file entry
166
+ is reduced to its path plus metadata (size, tokens, MIME types, tags),
167
+ giving the model a lightweight index of file paths it can use with
168
+ `read_file` to load only what it needs:
169
+
170
+ ```bash
171
+ # Generate manifest-only context as TOON
172
+ context_spook .contexts/project.rb -F TOON -m > context.toon
173
+
174
+ # Generate manifest-only context as JSON
175
+ context_spook .contexts/project.rb -m > context.json
176
+ ```
177
+
139
178
  The CLI tool also supports verbose output:
140
179
 
141
180
  ```bash
@@ -178,7 +217,8 @@ To estimate the size of your context without generating the large output file:
178
217
  context_spook .contexts/project.rb -S
179
218
  ```
180
219
 
181
- This is useful for optimizing context size before committing to large output files.
220
+ This is useful for optimizing context size before committing to large output
221
+ files.
182
222
 
183
223
  This is how you can show the usage message:
184
224
 
@@ -192,8 +232,10 @@ context_spook -h
192
232
  The DSL collects various types of project information:
193
233
 
194
234
  - **Variables**: Key-value pairs (e.g., git branch)
195
- - **Files**: Content, size, line count, and tags for each file
196
- - **Commands**: Shell command outputs with exit codes and working directory
235
+ - **Files**: Line-numbered content, byte size, token estimate, MIME types, line
236
+ count, and tags (or metadata-only in manifest mode)
237
+ - **Commands**: Shell command outputs with size, token estimate, exit codes,
238
+ working directory, and tags
197
239
  - **Metadata**: Project-wide attributes (e.g., Ruby version, coverage data)
198
240
 
199
241
  ## Intended Use Case
@@ -215,9 +257,20 @@ assistants understand:
215
257
  "files": {
216
258
  "lib/context_spook.rb": {
217
259
  "namespace": "lib",
218
- "content": "...",
219
- "size": 1234,
220
- "lines": 56,
260
+ "content": [
261
+ { "linenumber": 1, "line": "module ContextSpook" },
262
+ { "linenumber": 2, "line": "" },
263
+ { "linenumber": 3, "line": "require 'context_spook/version'" },
264
+ { "linenumber": 4, "line": "require 'context_spook/utils'" },
265
+ { "linenumber": 5, "line": "require 'context_spook/generator'" }
266
+ ],
267
+ "size": 120,
268
+ "tokens": 35,
269
+ "lines": 5,
270
+ "content_types": [
271
+ "application/x-ruby",
272
+ "text/x-ruby"
273
+ ],
221
274
  "tags": [
222
275
  "lib"
223
276
  ]
@@ -227,20 +280,58 @@ assistants understand:
227
280
  "tree": {
228
281
  "namespace": "structure",
229
282
  "output": "lib\n├── context_spook\n│ └── generator.rb\n└── context_spook.rb\n\n2 directories, 3 files",
283
+ "size": 88,
284
+ "tokens": 26,
230
285
  "exit_code": 0,
231
- "working_directory": "/path/to/project"
286
+ "working_directory": "/path/to/project",
287
+ "tags": [
288
+ "project_structure"
289
+ ]
232
290
  }
233
291
  },
234
292
  "metadata": {
235
- "ruby": "ruby 3.1.0 ...",
236
- "code_coverage": {}
293
+ "ruby": "ruby 4.0.7 (2026-09-15 ...) +PRISM [x86_64-linux]",
294
+ "code_coverage": null
237
295
  },
238
296
  "variables": {
239
- "branch": "main"
297
+ "branch": "master"
240
298
  }
241
299
  }
242
300
  ```
243
301
 
302
+ The same context in TOON v4 format:
303
+
304
+ ```
305
+ files:
306
+ "lib/context_spook.rb":
307
+ namespace: lib
308
+ content[5]{linenumber,line}:
309
+ 1,module ContextSpook
310
+ 2,""
311
+ 3,require 'context_spook/version'
312
+ 4,require 'context_spook/utils'
313
+ 5,require 'context_spook/generator'
314
+ size: 120
315
+ tokens: 35
316
+ lines: 5
317
+ content_types[2]: application/x-ruby,text/x-ruby
318
+ tags[1]: lib
319
+ commands:
320
+ tree:
321
+ namespace: structure
322
+ output: "lib\n├── context_spook\n│ └── generator.rb\n└── context_spook.rb\n\n2 directories, 3 files"
323
+ size: 88
324
+ tokens: 26
325
+ exit_code: 0
326
+ working_directory: /path/to/project
327
+ tags[1]: project_structure
328
+ metadata:
329
+ ruby: "ruby 4.0.7 (2026-09-15 ...) +PRISM [x86_64-linux]"
330
+ code_coverage: null
331
+ variables:
332
+ branch: master
333
+ ```
334
+
244
335
  ## License
245
336
 
246
337
  The gem is available as open source under the terms of the [MIT License](LICENSE)
data/Rakefile CHANGED
@@ -38,7 +38,6 @@ GemHadar do
38
38
  dependency 'mime-types', '~> 3.0'
39
39
  dependency 'yaml', '~> 0.4'
40
40
  dependency 'pathname', '~> 0.4'
41
- dependency 'toon-ruby', '>= 0.1'
42
41
  development_dependency 'all_images', '~> 0.6'
43
42
  development_dependency 'rspec', '~> 3.2'
44
43
  development_dependency 'debug'
data/bin/context_spook CHANGED
@@ -27,6 +27,7 @@ def usage
27
27
  -i PATTERN Include files matching PATTERN (can be used multiple times)
28
28
  Supports glob patterns like **, *, ?, [abc], {a,b,c}
29
29
  -F FORMAT Output context as format JSON (the default) or TOON
30
+ -m Manifest mode: omit file content, carry only metadata
30
31
  -h Show this help message
31
32
 
32
33
  Examples:
@@ -49,19 +50,21 @@ def usage
49
50
  # Generate informational output only to estimate context size
50
51
  #{File.basename($0)} .contexts/project.rb -S
51
52
 
53
+ # Generate context in manifest mode (metadata only, no file content)
54
+ #{File.basename($0)} .contexts/project.rb -m
52
55
  # Generate context and save to file
53
56
  #{File.basename($0)} .contexts/project.rb -o context.json
54
57
  EOT
55
58
  exit 0
56
59
  end
57
60
 
58
- opts = go 'o:i:F:pvSh', defaults: { ?v => true }
61
+ opts = go 'o:i:F:mvSh', defaults: { ?v => true }
59
62
  opts[?h] and usage
60
63
  context = nil
61
64
  output = nil
62
65
 
63
66
  if opts[?i]
64
- context = ContextSpook.generate_context(verbose: opts[?v], format: opts[?F]) do
67
+ context = ContextSpook.generate_context(verbose: opts[?v], format: opts[?F], manifest: opts[?m]) do
65
68
  context do
66
69
  opts[?i].to_a.each do |glob|
67
70
  glob = File.expand_path(glob)
@@ -74,7 +77,7 @@ if opts[?i]
74
77
  end
75
78
  else
76
79
  filename = ARGV.shift or fail 'require context definition file as an argument'
77
- context = ContextSpook.generate_context(filename, verbose: opts[?v], format: opts[?F])
80
+ context = ContextSpook.generate_context(filename, verbose: opts[?v], format: opts[?F], manifest: opts[?m])
78
81
  end
79
82
 
80
83
  output = STDOUT
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: context_spook 1.6.0 ruby lib
2
+ # stub: context_spook 1.7.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "context_spook".freeze
6
- s.version = "1.6.0".freeze
6
+ s.version = "1.7.0".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
@@ -12,19 +12,19 @@ Gem::Specification.new do |s|
12
12
  s.description = "context_spook is a library that collects and organizes project\ninformation to help AI assistants understand codebases better.\n".freeze
13
13
  s.email = "flori@ping.de".freeze
14
14
  s.executables = ["context_spook".freeze]
15
- s.extra_rdoc_files = ["README.md".freeze, "lib/context_spook.rb".freeze, "lib/context_spook/generator.rb".freeze, "lib/context_spook/output_context.rb".freeze, "lib/context_spook/toon.rb".freeze, "lib/context_spook/utils.rb".freeze, "lib/context_spook/verbose_puts.rb".freeze, "lib/context_spook/version.rb".freeze]
16
- s.files = [".contexts/project.rb".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "bin/context_spook".freeze, "context_spook.gemspec".freeze, "hello_world.json".freeze, "hey_world.yaml".freeze, "lib/context_spook.rb".freeze, "lib/context_spook/generator.rb".freeze, "lib/context_spook/output_context.rb".freeze, "lib/context_spook/toon.rb".freeze, "lib/context_spook/toon/example.md".freeze, "lib/context_spook/utils.rb".freeze, "lib/context_spook/verbose_puts.rb".freeze, "lib/context_spook/version.rb".freeze, "spec/context_spook/generator_spec.rb".freeze, "spec/context_spook/toon_spec.rb".freeze, "spec/spec_helper.rb".freeze]
15
+ s.extra_rdoc_files = ["README.md".freeze, "lib/context_spook.rb".freeze, "lib/context_spook/generator.rb".freeze, "lib/context_spook/output_context.rb".freeze, "lib/context_spook/toon.rb".freeze, "lib/context_spook/toon/encoder.rb".freeze, "lib/context_spook/utils.rb".freeze, "lib/context_spook/verbose_puts.rb".freeze, "lib/context_spook/version.rb".freeze]
16
+ s.files = [".contexts/project.rb".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "bin/context_spook".freeze, "context_spook.gemspec".freeze, "hello_world.json".freeze, "hey_world.yaml".freeze, "lib/context_spook.rb".freeze, "lib/context_spook/generator.rb".freeze, "lib/context_spook/output_context.rb".freeze, "lib/context_spook/toon.rb".freeze, "lib/context_spook/toon/encoder.rb".freeze, "lib/context_spook/toon/example.md".freeze, "lib/context_spook/utils.rb".freeze, "lib/context_spook/verbose_puts.rb".freeze, "lib/context_spook/version.rb".freeze, "spec/context_spook/generator_spec.rb".freeze, "spec/context_spook/toon_encoder_spec.rb".freeze, "spec/context_spook/toon_spec.rb".freeze, "spec/examples/README.md".freeze, "spec/examples/conversions/keyed-tabular-objects.json".freeze, "spec/examples/conversions/keyed-tabular-objects.toon".freeze, "spec/examples/conversions/nested-field-groups.json".freeze, "spec/examples/conversions/nested-field-groups.toon".freeze, "spec/examples/conversions/tabular-arrays.json".freeze, "spec/examples/conversions/tabular-arrays.toon".freeze, "spec/examples/invalid/delimiter-mismatch.toon".freeze, "spec/examples/invalid/length-mismatch.toon".freeze, "spec/examples/invalid/multiple-root-primitives.toon".freeze, "spec/examples/valid/comments.toon".freeze, "spec/examples/valid/delimiter-scoping.toon".freeze, "spec/examples/valid/mixed-arrays.toon".freeze, "spec/examples/valid/nested-objects.toon".freeze, "spec/examples/valid/objects.toon".freeze, "spec/examples/valid/pipe-delimiter.toon".freeze, "spec/examples/valid/primitive-arrays.toon".freeze, "spec/examples/valid/tab-delimiter.toon".freeze, "spec/fixtures/encode/arrays-nested.json".freeze, "spec/fixtures/encode/arrays-objects.json".freeze, "spec/fixtures/encode/arrays-primitive.json".freeze, "spec/fixtures/encode/arrays-tabular.json".freeze, "spec/fixtures/encode/delimiters.json".freeze, "spec/fixtures/encode/objects-keyed.json".freeze, "spec/fixtures/encode/objects.json".freeze, "spec/fixtures/encode/primitives.json".freeze, "spec/fixtures/encode/whitespace.json".freeze, "spec/spec_helper.rb".freeze]
17
17
  s.homepage = "https://github.com/flori/context_spook".freeze
18
18
  s.licenses = ["MIT".freeze]
19
19
  s.rdoc_options = ["--title".freeze, "ContextSpook - context_spook collects project context for AI".freeze, "--main".freeze, "README.md".freeze]
20
20
  s.required_ruby_version = Gem::Requirement.new(">= 3.1".freeze)
21
- s.rubygems_version = "4.0.10".freeze
21
+ s.rubygems_version = "4.0.22".freeze
22
22
  s.summary = "context_spook collects project context for AI".freeze
23
- s.test_files = ["spec/context_spook/generator_spec.rb".freeze, "spec/context_spook/toon_spec.rb".freeze, "spec/spec_helper.rb".freeze]
23
+ s.test_files = ["spec/context_spook/generator_spec.rb".freeze, "spec/context_spook/toon_encoder_spec.rb".freeze, "spec/context_spook/toon_spec.rb".freeze, "spec/spec_helper.rb".freeze]
24
24
 
25
25
  s.specification_version = 4
26
26
 
27
- s.add_development_dependency(%q<gem_hadar>.freeze, [">= 2.17.1".freeze])
27
+ s.add_development_dependency(%q<gem_hadar>.freeze, [">= 2.18.0".freeze])
28
28
  s.add_development_dependency(%q<all_images>.freeze, ["~> 0.6".freeze])
29
29
  s.add_development_dependency(%q<rspec>.freeze, ["~> 3.2".freeze])
30
30
  s.add_development_dependency(%q<debug>.freeze, [">= 0".freeze])
@@ -36,5 +36,4 @@ Gem::Specification.new do |s|
36
36
  s.add_runtime_dependency(%q<mime-types>.freeze, ["~> 3.0".freeze])
37
37
  s.add_runtime_dependency(%q<yaml>.freeze, ["~> 0.4".freeze])
38
38
  s.add_runtime_dependency(%q<pathname>.freeze, ["~> 0.4".freeze])
39
- s.add_runtime_dependency(%q<toon-ruby>.freeze, [">= 0.1".freeze])
40
39
  end
@@ -28,6 +28,8 @@ module ContextSpook
28
28
  # during processing
29
29
  # @param format [ String, nil ] the output format for the context, either
30
30
  # 'JSON' or 'TOON'
31
+ # @param manifest [ TrueClass, FalseClass ] if true, file entries
32
+ # omit their content and only carry metadata
31
33
  # @param block [ Proc ] a block containing the context definition, used if
32
34
  # filename is nil
33
35
  #
@@ -37,15 +39,18 @@ module ContextSpook
37
39
  # @raise [ ArgumentError ] if neither a filename nor a block is provided
38
40
  # @raise [ ArgumentError ] if an invalid format is specified
39
41
  # @raise [ ArgumentError ] if the context definition file cannot be read or parsed
40
- def self.generate_context(filename = nil, verbose: false, format: nil, &block)
42
+ def self.generate_context(filename = nil, verbose: false, format: nil,
43
+ manifest: false, &block)
41
44
  verbose = !!verbose
42
45
  filename.present? ^ block or
43
46
  raise ArgumentError, 'need either a filename or a &block argument'
44
- generator = if filename
45
- Generator.send(:new, verbose:, format:).send(:parse, File.read(filename))
46
- else
47
- Generator.send(:new, verbose:, format:, &block)
48
- end
47
+ generator =
48
+ if filename
49
+ source = File.read(filename)
50
+ Generator.send(:new, verbose:, format:, manifest:).send(:parse, source)
51
+ else
52
+ Generator.send(:new, verbose:, format:, manifest:, &block)
53
+ end
49
54
  generator.output_context_size
50
55
  generator.context
51
56
  end
@@ -70,6 +75,12 @@ module ContextSpook
70
75
  # @return [ String ] the format identifier, either 'JSON' or 'TOON'
71
76
  attr_reader :format
72
77
 
78
+ # The manifest method returns whether the generator is in manifest mode,
79
+ # where file entries omit their content.
80
+ #
81
+ # @return [ TrueClass, FalseClass ] true if in manifest mode
82
+ attr_reader :manifest
83
+
73
84
  private_class_method :new
74
85
 
75
86
  # The token_estimator attribute allows setting the logic used to estimate
@@ -86,9 +97,10 @@ module ContextSpook
86
97
  # output during processing, defaults to lfalse.
87
98
  # @param block [ Proc ] a block of code to be evaluated within the object's context
88
99
  # If no block is given, the method does nothing.
89
- def initialize(verbose: false, format: nil, &block)
90
- @verbose = !!verbose
91
- @format = (format || 'JSON').upcase
100
+ def initialize(verbose: false, format: nil, manifest: false, &block)
101
+ @verbose = !!verbose
102
+ @format = (format || 'JSON').upcase
103
+ @manifest = !!manifest
92
104
  %w[ TOON JSON ].include?(@format) or
93
105
  raise ArgumentError,
94
106
  "format needs to be either JSON or TOON, was #{@format.inspect}"
@@ -119,21 +131,18 @@ module ContextSpook
119
131
  include Tins::DSLAccessor
120
132
  include Term::ANSIColor
121
133
  include ContextSpook::TOON
134
+ include ContextSpook::Utils
122
135
 
123
136
  # The initialize method sets up the object by evaluating the provided block
124
137
  # in the object's context.
125
138
  #
126
- # @param verbose [ TrueClass, FalseClass ] flag to enable verbose output
127
- # during processing, defaults to false.
128
139
  # @param block [ Proc ] a block of code to be evaluated within the object's context
129
140
  # If no block is given, the method does nothing.
130
141
  def initialize(generator:, &block)
131
142
  @generator = generator
132
143
  block and instance_eval(&block)
133
144
  meta(format: generator.format)
134
- if generator.format == 'TOON'
135
- meta(toon_example: toon_example)
136
- end
145
+ meta(manifest: true) if generator.manifest
137
146
  end
138
147
 
139
148
  # The generator method returns the generator object associated with this
@@ -145,6 +154,8 @@ module ContextSpook
145
154
 
146
155
  delegate :verbose, to: :generator
147
156
 
157
+ delegate :manifest, to: :generator
158
+
148
159
  # The namespace method creates a scoped block with a given name.
149
160
  #
150
161
  # @param name [ Object ] the name to scope the block with
@@ -205,11 +216,10 @@ module ContextSpook
205
216
  #
206
217
  # @param filename [ String ] the path to the JSON file to be read and parsed
207
218
  #
208
- # @return [ Object, nil ] the parsed JSON data structure or nil if the file cannot be read
219
+ # @return [ Object, nil ] the parsed JSON data structure or nil if the
220
+ # file cannot be read
209
221
  def json(filename)
210
- file_size = Tins::Unit.format(
211
- File.size(filename), format: '%.2f %U', unit: ?b, prefix: 1024
212
- )
222
+ file_size = format_size(File.size(filename))
213
223
  verbose_puts "Read #{filename.inspect} as JSON (%s) for context." % file_size
214
224
  JSON.load_file(filename)
215
225
  rescue Errno::ENOENT, JSON::ParserError => e
@@ -231,9 +241,7 @@ module ContextSpook
231
241
  # @return [ Object, nil ] the parsed YAML data structure or nil if the
232
242
  # file cannot be read
233
243
  def yaml(filename)
234
- file_size = Tins::Unit.format(
235
- File.size(filename), format: '%.2f %U', unit: ?b, prefix: 1024
236
- )
244
+ file_size = format_size(File.size(filename))
237
245
  verbose_puts "Read #{filename.inspect} as YAML (%s) for context." % file_size
238
246
  YAML.load_file(filename)
239
247
  rescue Errno::ENOENT, Psych::SyntaxError => e
@@ -252,32 +260,51 @@ module ContextSpook
252
260
  # content.
253
261
  #
254
262
  # It reads the specified file and creates an entry in the files hash with
255
- # the file's content, along with its namespace and optional tags.
263
+ # the file's content, along with its namespace and optional tags. In
264
+ # manifest mode, only metadata fields are stored.
256
265
  #
257
266
  # @param filename [ String ] the path to the file to be read and stored
258
267
  # @param tags [ Array<String>, nil ] optional array of tags to associate with the file
259
268
  #
260
269
  # @return [ Hash ] the created file entry with content, namespace, and tags
261
270
  def file(filename, tags: nil)
262
- content = File.read(filename)
263
- files[filename] = {
264
- namespace: scope_top,
265
- content:,
266
- size: content.size,
267
- lines: content.lines.size,
268
- content_types: MIME::Types.type_for(filename).map(&:content_type).full?,
269
- tags: (Array(tags) if tags),
270
- }.compact
271
- file_size = Tins::Unit.format(
272
- content.size, format: '%.2f %U', unit: ?b, prefix: 1024
273
- )
274
- verbose_puts "Read #{filename.inspect} (%s) for context." % file_size
271
+ size = File.size(filename)
272
+ tokens = estimate_tokens(size)
273
+ if manifest
274
+ files[filename] = {
275
+ namespace: scope_top,
276
+ size: ,
277
+ tokens: ,
278
+ content_types: MIME::Types.type_for(filename)
279
+ .map(&:content_type).full?,
280
+ tags: (Array(tags) if tags),
281
+ }.compact
282
+ else
283
+ content = File.read(filename)
284
+ content_lines = content.lines
285
+ numbered_lines = content_lines.each_with_index.map {
286
+ { linenumber: _2 + 1, line: _1.chomp }
287
+ }
288
+ files[filename] = {
289
+ namespace: scope_top,
290
+ content: numbered_lines,
291
+ size: ,
292
+ tokens: ,
293
+ lines: content_lines.size,
294
+ content_types: MIME::Types.type_for(filename)
295
+ .map(&:content_type).full?,
296
+ tags: (Array(tags) if tags),
297
+ }.compact
298
+ end
299
+ verbose_puts "Read #{filename.inspect} (%s, %s) for context." %
300
+ [ format_size(size), format_tokens(tokens) ]
275
301
  nil
276
302
  rescue Errno::ENOENT => e
277
303
  verbose_puts color(208) { "Reading #{filename.inspect} caused #{e.class}: #{e}" }
278
304
  end
279
305
 
280
- # The commands method sets up a DSL accessor for provided command outputs.
306
+ # The commands method sets up a DSL accessor for provided command
307
+ # outputs.
281
308
  #
282
309
  # @param value [ Hash ] the hash of commands to be set
283
310
  dsl_accessor :commands do
@@ -286,8 +313,8 @@ module ContextSpook
286
313
 
287
314
  # The command method executes a shell command and stores its result.
288
315
  #
289
- # This method runs a given shell command and records the output, exit code,
290
- # working directory, and optional tags in the commands hash.
316
+ # This method runs a given shell command and records the output, exit
317
+ # code, working directory, and optional tags in the commands hash.
291
318
  #
292
319
  # @param shell_command [ String ] the shell command to execute
293
320
  # @param tags [ Array<String>, nil ] optional array of tags to associate
@@ -303,17 +330,20 @@ module ContextSpook
303
330
  "Executing #{shell_command.inspect} resulted in exit code #{exit_code}."
304
331
  }
305
332
  end
333
+ output_size = output.bytesize
334
+ output_tokens = estimate_tokens(output_size)
306
335
  commands[shell_command] = {
307
- namespace: scope_top,
336
+ namespace: scope_top,
308
337
  output:,
338
+ size: output_size,
339
+ tokens: output_tokens,
309
340
  exit_code:,
310
341
  working_directory: Dir.pwd,
311
- tags: (Array(tags) if tags),
342
+ tags: (Array(tags) if tags),
312
343
  }.compact
313
- output_size = Tins::Unit.format(
314
- output.size, format: '%.2f %U', unit: ?b, prefix: 1024
315
- )
316
- verbose_puts "Executed #{shell_command.inspect} with output (%s) for context." % output_size
344
+ verbose_puts "Executed #{shell_command.inspect} with output "\
345
+ "(%s, %s) for context." %
346
+ [ format_size(output_size), format_tokens(output_tokens) ]
317
347
  nil
318
348
  end
319
349
 
@@ -351,7 +381,7 @@ module ContextSpook
351
381
  # @return [ Integer ] the size in bytes of the JSON representation of the
352
382
  # context
353
383
  def size
354
- to_json.size
384
+ to_json.bytesize
355
385
  end
356
386
  end
357
387
 
@@ -15,7 +15,7 @@ module ContextSpook::OutputContext
15
15
  if @context
16
16
  context = @format == 'TOON' ? @context.to_toon : @context.to_json
17
17
  end
18
- formatted_size = ContextSpook::Utils.format_size(context.size)
18
+ formatted_size = ContextSpook::Utils.format_size(context.bytesize)
19
19
  tokens = @token_estimator.call(context)
20
20
  formatted_tokens = ContextSpook::Utils.format_tokens(tokens)
21
21
 
@@ -33,7 +33,7 @@ module ContextSpook::OutputContext
33
33
  # defaults to STDOUT
34
34
  def output_context(output: STDOUT)
35
35
  if @format == 'TOON'
36
- output.puts(@context.to_toon)
36
+ @context.to_toon_stream(output)
37
37
  else
38
38
  JSON.dump(@context.as_json, output)
39
39
  end