context_spook 1.5.1 → 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 +7 -8
  4. data/bin/context_spook +6 -3
  5. data/context_spook.gemspec +7 -8
  6. data/lib/context_spook/generator.rb +83 -45
  7. data/lib/context_spook/output_context.rb +13 -8
  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 +31 -4
  12. data/lib/context_spook/version.rb +1 -1
  13. data/spec/context_spook/generator_spec.rb +65 -1
  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: 2bb51df13e01f4262026aea154cc901997a0b5cec6566f4bc71e42467ec3dfed
4
- data.tar.gz: b26ead5c765e99a931026d267e6de374dc0c93f8a5c64e46bfe09c28d892dd99
3
+ metadata.gz: 3992c09d3428a0268385de6fbf268f96b92ff8cc8f84cc11d464e3c1cd9c1cfe
4
+ data.tar.gz: d8cbd80e0ca8a3df54f464d8eb120dd8d9864b51e1aafc12b23f60e9e8b42eef
5
5
  SHA512:
6
- metadata.gz: 4f8995fb526a7f6fd8be4c15c2a869775fdc5a826571bd84723689e695ca3b982325d56bf971da3982afc6cecdb2575d20d0319ea073d505d85e19a6415fdffc
7
- data.tar.gz: 56bc054c3fe75f245ed97a7d92c0c91cb31dcfb1e7b101b2ee3a2092324a7290b7ec0449c5dba310b809a102e64d2bd40a656d126feef5a3d03ec588eaead314
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
@@ -31,14 +31,13 @@ GemHadar do
31
31
 
32
32
  required_ruby_version '>= 3.1'
33
33
 
34
- dependency 'tins', '~>1.46'
35
- dependency 'json', '~>2.0'
36
- dependency 'term-ansicolor', '~> 1.11'
37
- dependency 'mize', '~> 0.6'
38
- dependency 'mime-types', '~> 3.0'
39
- dependency 'yaml', '~> 0.4'
40
- dependency 'pathname', '~> 0.4'
41
- dependency 'toon-ruby', '>= 0.1'
34
+ dependency 'tins', '~> 1.46'
35
+ dependency 'json', '~> 2.0'
36
+ dependency 'term-ansicolor', '~> 1.11'
37
+ dependency 'mize', '~> 0.6'
38
+ dependency 'mime-types', '~> 3.0'
39
+ dependency 'yaml', '~> 0.4'
40
+ dependency 'pathname', '~> 0.4'
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.5.1 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.5.1".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.3".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.0".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,8 +75,21 @@ 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
 
86
+ # The token_estimator attribute allows setting the logic used to estimate
87
+ # the number of tokens in a serialized context.
88
+ #
89
+ # It expects an object that responds to #call(text), where text is the
90
+ # serialized context string.
91
+ attr_writer :token_estimator
92
+
75
93
  # The initialize method sets up the object by evaluating the provided block
76
94
  # in the object's context.
77
95
  #
@@ -79,12 +97,14 @@ module ContextSpook
79
97
  # output during processing, defaults to lfalse.
80
98
  # @param block [ Proc ] a block of code to be evaluated within the object's context
81
99
  # If no block is given, the method does nothing.
82
- def initialize(verbose: false, format: nil, &block)
83
- @verbose = !!verbose
84
- @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
85
104
  %w[ TOON JSON ].include?(@format) or
86
105
  raise ArgumentError,
87
106
  "format needs to be either JSON or TOON, was #{@format.inspect}"
107
+ self.token_estimator = ContextSpook::Utils.method(:estimate_tokens)
88
108
  block and instance_eval(&block)
89
109
  end
90
110
 
@@ -111,21 +131,18 @@ module ContextSpook
111
131
  include Tins::DSLAccessor
112
132
  include Term::ANSIColor
113
133
  include ContextSpook::TOON
134
+ include ContextSpook::Utils
114
135
 
115
136
  # The initialize method sets up the object by evaluating the provided block
116
137
  # in the object's context.
117
138
  #
118
- # @param verbose [ TrueClass, FalseClass ] flag to enable verbose output
119
- # during processing, defaults to false.
120
139
  # @param block [ Proc ] a block of code to be evaluated within the object's context
121
140
  # If no block is given, the method does nothing.
122
141
  def initialize(generator:, &block)
123
142
  @generator = generator
124
143
  block and instance_eval(&block)
125
144
  meta(format: generator.format)
126
- if generator.format == 'TOON'
127
- meta(toon_example: toon_example)
128
- end
145
+ meta(manifest: true) if generator.manifest
129
146
  end
130
147
 
131
148
  # The generator method returns the generator object associated with this
@@ -137,6 +154,8 @@ module ContextSpook
137
154
 
138
155
  delegate :verbose, to: :generator
139
156
 
157
+ delegate :manifest, to: :generator
158
+
140
159
  # The namespace method creates a scoped block with a given name.
141
160
  #
142
161
  # @param name [ Object ] the name to scope the block with
@@ -197,11 +216,10 @@ module ContextSpook
197
216
  #
198
217
  # @param filename [ String ] the path to the JSON file to be read and parsed
199
218
  #
200
- # @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
201
221
  def json(filename)
202
- file_size = Tins::Unit.format(
203
- File.size(filename), format: '%.2f %U', unit: ?b, prefix: 1024
204
- )
222
+ file_size = format_size(File.size(filename))
205
223
  verbose_puts "Read #{filename.inspect} as JSON (%s) for context." % file_size
206
224
  JSON.load_file(filename)
207
225
  rescue Errno::ENOENT, JSON::ParserError => e
@@ -223,9 +241,7 @@ module ContextSpook
223
241
  # @return [ Object, nil ] the parsed YAML data structure or nil if the
224
242
  # file cannot be read
225
243
  def yaml(filename)
226
- file_size = Tins::Unit.format(
227
- File.size(filename), format: '%.2f %U', unit: ?b, prefix: 1024
228
- )
244
+ file_size = format_size(File.size(filename))
229
245
  verbose_puts "Read #{filename.inspect} as YAML (%s) for context." % file_size
230
246
  YAML.load_file(filename)
231
247
  rescue Errno::ENOENT, Psych::SyntaxError => e
@@ -244,32 +260,51 @@ module ContextSpook
244
260
  # content.
245
261
  #
246
262
  # It reads the specified file and creates an entry in the files hash with
247
- # 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.
248
265
  #
249
266
  # @param filename [ String ] the path to the file to be read and stored
250
267
  # @param tags [ Array<String>, nil ] optional array of tags to associate with the file
251
268
  #
252
269
  # @return [ Hash ] the created file entry with content, namespace, and tags
253
270
  def file(filename, tags: nil)
254
- content = File.read(filename)
255
- files[filename] = {
256
- namespace: scope_top,
257
- content:,
258
- size: content.size,
259
- lines: content.lines.size,
260
- content_types: MIME::Types.type_for(filename).map(&:content_type).full?,
261
- tags: (Array(tags) if tags),
262
- }.compact
263
- file_size = Tins::Unit.format(
264
- content.size, format: '%.2f %U', unit: ?b, prefix: 1024
265
- )
266
- 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) ]
267
301
  nil
268
302
  rescue Errno::ENOENT => e
269
303
  verbose_puts color(208) { "Reading #{filename.inspect} caused #{e.class}: #{e}" }
270
304
  end
271
305
 
272
- # 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.
273
308
  #
274
309
  # @param value [ Hash ] the hash of commands to be set
275
310
  dsl_accessor :commands do
@@ -278,8 +313,8 @@ module ContextSpook
278
313
 
279
314
  # The command method executes a shell command and stores its result.
280
315
  #
281
- # This method runs a given shell command and records the output, exit code,
282
- # 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.
283
318
  #
284
319
  # @param shell_command [ String ] the shell command to execute
285
320
  # @param tags [ Array<String>, nil ] optional array of tags to associate
@@ -295,17 +330,20 @@ module ContextSpook
295
330
  "Executing #{shell_command.inspect} resulted in exit code #{exit_code}."
296
331
  }
297
332
  end
333
+ output_size = output.bytesize
334
+ output_tokens = estimate_tokens(output_size)
298
335
  commands[shell_command] = {
299
- namespace: scope_top,
336
+ namespace: scope_top,
300
337
  output:,
338
+ size: output_size,
339
+ tokens: output_tokens,
301
340
  exit_code:,
302
341
  working_directory: Dir.pwd,
303
- tags: (Array(tags) if tags),
342
+ tags: (Array(tags) if tags),
304
343
  }.compact
305
- output_size = Tins::Unit.format(
306
- output.size, format: '%.2f %U', unit: ?b, prefix: 1024
307
- )
308
- 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) ]
309
347
  nil
310
348
  end
311
349
 
@@ -343,7 +381,7 @@ module ContextSpook
343
381
  # @return [ Integer ] the size in bytes of the JSON representation of the
344
382
  # context
345
383
  def size
346
- to_json.size
384
+ to_json.bytesize
347
385
  end
348
386
  end
349
387
 
@@ -5,16 +5,21 @@
5
5
  # functionality for generated context objects.
6
6
  module ContextSpook::OutputContext
7
7
  # The output_context_size method prints the total size of the generated
8
- # context JSON representation.
8
+ # context representation and an estimated token count.
9
9
  #
10
10
  # This method calculates the size of the context object when serialized to
11
- # JSON, formats it using binary units (KiB, MiB, etc.), and outputs the
12
- # result to standard error.
11
+ # JSON (or TOON), formats it using binary units, estimates the token count
12
+ # based on bytes, and outputs the result to standard error.
13
13
  def output_context_size
14
- context_size =
15
- (@format == 'TOON' ? @context&.toon_size : @context&.size).to_i
16
- context_size = ContextSpook::Utils.format_size(context_size)
17
- verbose_puts "Built #{context_size} of #@format context in total."
14
+ context = ''
15
+ if @context
16
+ context = @format == 'TOON' ? @context.to_toon : @context.to_json
17
+ end
18
+ formatted_size = ContextSpook::Utils.format_size(context.bytesize)
19
+ tokens = @token_estimator.call(context)
20
+ formatted_tokens = ContextSpook::Utils.format_tokens(tokens)
21
+
22
+ verbose_puts "Built #{formatted_size} (#{formatted_tokens} tokens) of #@format context in total."
18
23
  end
19
24
 
20
25
  # The output_context method writes the generated context to the specified
@@ -28,7 +33,7 @@ module ContextSpook::OutputContext
28
33
  # defaults to STDOUT
29
34
  def output_context(output: STDOUT)
30
35
  if @format == 'TOON'
31
- output.puts(@context.to_toon)
36
+ @context.to_toon_stream(output)
32
37
  else
33
38
  JSON.dump(@context.as_json, output)
34
39
  end