automatic 14.12.2 → 26.08

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 (172) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +635 -83
  3. data/VERSION +1 -1
  4. data/automatic.gemspec +109 -248
  5. data/bin/automatic +20 -139
  6. data/config/feed2console.yml +10 -4
  7. data/config/feed2markdown.yml +41 -0
  8. data/doc/AI_TUTORIAL.md +518 -0
  9. data/doc/BASIC_DESIGN.md +516 -0
  10. data/doc/COPYING.LESSER +165 -0
  11. data/doc/DEPLOYMENT.md +824 -0
  12. data/doc/LICENSE.md +14 -0
  13. data/doc/PLUGINS.md +1875 -0
  14. data/doc/PLUGIN_DEVELOPMENT.md +86 -0
  15. data/doc/POLICY.md +857 -0
  16. data/doc/QUICKSTART.md +256 -0
  17. data/doc/RELEASING.md +381 -0
  18. data/doc/REQUIREMENTS.md +526 -0
  19. data/doc/VERSIONS +208 -0
  20. data/lib/automatic/cli.rb +248 -0
  21. data/lib/automatic/environment.rb +31 -5
  22. data/lib/automatic/feed_maker.rb +10 -9
  23. data/lib/automatic/feed_parser.rb +51 -35
  24. data/lib/automatic/http.rb +107 -0
  25. data/lib/automatic/log.rb +49 -18
  26. data/lib/automatic/opml.rb +3 -1
  27. data/lib/automatic/pipeline.rb +63 -32
  28. data/lib/automatic/recipe.rb +56 -17
  29. data/lib/automatic/version.rb +14 -1
  30. data/lib/automatic.rb +78 -20
  31. data/plugins/custom_feed/svn_log.rb +73 -32
  32. data/plugins/custom_feed/web.rb +348 -0
  33. data/plugins/filter/absolute_uri.rb +43 -27
  34. data/plugins/filter/accept.rb +38 -45
  35. data/plugins/filter/claude.rb +217 -0
  36. data/plugins/filter/clear.rb +12 -8
  37. data/plugins/filter/description_link.rb +49 -51
  38. data/plugins/filter/full_feed.rb +158 -52
  39. data/plugins/filter/gemini.rb +216 -0
  40. data/plugins/filter/github_feed.rb +38 -26
  41. data/plugins/filter/ignore.rb +33 -44
  42. data/plugins/filter/image.rb +36 -25
  43. data/plugins/filter/image_source.rb +58 -52
  44. data/plugins/filter/join.rb +107 -0
  45. data/plugins/filter/one.rb +19 -26
  46. data/plugins/filter/open_ai.rb +198 -0
  47. data/plugins/filter/rand.rb +16 -17
  48. data/plugins/filter/sakura_ai.rb +205 -0
  49. data/plugins/filter/sanitize.rb +29 -34
  50. data/plugins/filter/sort.rb +20 -27
  51. data/plugins/filter/tumblr_resize.rb +31 -23
  52. data/plugins/notify/ikachan.rb +86 -48
  53. data/plugins/provide/fluentd.rb +43 -24
  54. data/plugins/publish/amazon_s3.rb +73 -40
  55. data/plugins/publish/console.rb +19 -16
  56. data/plugins/publish/console_link.rb +20 -17
  57. data/plugins/publish/eject.rb +48 -26
  58. data/plugins/publish/fluentd.rb +50 -30
  59. data/plugins/publish/hatena_bookmark.rb +88 -71
  60. data/plugins/publish/instapaper.rb +69 -59
  61. data/plugins/publish/markdown.rb +278 -0
  62. data/plugins/publish/memcached.rb +35 -29
  63. data/plugins/store/database.rb +50 -48
  64. data/plugins/store/digest.rb +212 -0
  65. data/plugins/store/file.rb +99 -68
  66. data/plugins/store/full_text.rb +32 -25
  67. data/plugins/store/permalink.rb +18 -22
  68. data/plugins/subscription/feed.rb +34 -21
  69. data/plugins/subscription/link.rb +31 -32
  70. data/plugins/subscription/text.rb +32 -46
  71. data/plugins/subscription/tumblr.rb +55 -44
  72. data/plugins/subscription/xml.rb +40 -36
  73. metadata +108 -321
  74. data/Gemfile +0 -38
  75. data/Rakefile +0 -59
  76. data/doc/ChangeLog +0 -303
  77. data/doc/PLUGINS +0 -750
  78. data/doc/PLUGINS.ja +0 -753
  79. data/doc/README +0 -511
  80. data/doc/README.ja +0 -519
  81. data/plugins/filter/google_news.rb +0 -50
  82. data/plugins/publish/google_calendar.rb +0 -84
  83. data/plugins/publish/hipchat.rb +0 -46
  84. data/plugins/publish/pocket.rb +0 -45
  85. data/plugins/publish/twitter.rb +0 -58
  86. data/plugins/subscription/chan_toru.rb +0 -57
  87. data/plugins/subscription/g_guide.rb +0 -57
  88. data/plugins/subscription/pocket.rb +0 -51
  89. data/plugins/subscription/twitter.rb +0 -69
  90. data/plugins/subscription/twitter_search.rb +0 -50
  91. data/plugins/subscription/weather.rb +0 -33
  92. data/script/build +0 -84
  93. data/spec/fixtures/sampleFeeds.tsv +0 -1
  94. data/spec/fixtures/sampleFeeds2.tsv +0 -2
  95. data/spec/fixtures/sampleRecipe.yml +0 -24
  96. data/spec/lib/automatic/log_spec.rb +0 -32
  97. data/spec/lib/automatic/pipeline_spec.rb +0 -68
  98. data/spec/lib/automatic/recipe_spec.rb +0 -40
  99. data/spec/lib/automatic_spec.rb +0 -99
  100. data/spec/plugins/custom_feed/svn_log_spec.rb +0 -31
  101. data/spec/plugins/filter/absolute_uri_spec.rb +0 -61
  102. data/spec/plugins/filter/accept_spec.rb +0 -331
  103. data/spec/plugins/filter/clear_spec.rb +0 -49
  104. data/spec/plugins/filter/description_link_spec.rb +0 -138
  105. data/spec/plugins/filter/full_feed_spec.rb +0 -129
  106. data/spec/plugins/filter/github_feed_spec.rb +0 -55
  107. data/spec/plugins/filter/google_news_spec.rb +0 -69
  108. data/spec/plugins/filter/ignore_spec.rb +0 -328
  109. data/spec/plugins/filter/image_source_spec.rb +0 -89
  110. data/spec/plugins/filter/image_spec.rb +0 -65
  111. data/spec/plugins/filter/one_spec.rb +0 -71
  112. data/spec/plugins/filter/rand_spec.rb +0 -52
  113. data/spec/plugins/filter/sanitize_spec.rb +0 -153
  114. data/spec/plugins/filter/sort_spec.rb +0 -189
  115. data/spec/plugins/filter/tumblr_resize_spec.rb +0 -109
  116. data/spec/plugins/notify/ikachan_spec.rb +0 -58
  117. data/spec/plugins/provide/fluentd_spec.rb +0 -49
  118. data/spec/plugins/publish/amazon_s3_spec.rb +0 -40
  119. data/spec/plugins/publish/console_spec.rb +0 -30
  120. data/spec/plugins/publish/eject_spec.rb +0 -40
  121. data/spec/plugins/publish/fluentd_spec.rb +0 -40
  122. data/spec/plugins/publish/google_calendar_spec.rb +0 -83
  123. data/spec/plugins/publish/hatena_bookmark_spec.rb +0 -134
  124. data/spec/plugins/publish/hipchat_spec.rb +0 -69
  125. data/spec/plugins/publish/instapaper_spec.rb +0 -82
  126. data/spec/plugins/publish/memcached_spec.rb +0 -63
  127. data/spec/plugins/publish/pocket_spec.rb +0 -51
  128. data/spec/plugins/publish/twitter_spec.rb +0 -73
  129. data/spec/plugins/store/file_spec.rb +0 -58
  130. data/spec/plugins/store/full_text_spec.rb +0 -152
  131. data/spec/plugins/store/permalink_spec.rb +0 -206
  132. data/spec/plugins/subscription/chan_toru_spec.rb +0 -56
  133. data/spec/plugins/subscription/feed_spec.rb +0 -71
  134. data/spec/plugins/subscription/g_guide_spec.rb +0 -82
  135. data/spec/plugins/subscription/link_spec.rb +0 -72
  136. data/spec/plugins/subscription/pocket_spec.rb +0 -57
  137. data/spec/plugins/subscription/text_spec.rb +0 -84
  138. data/spec/plugins/subscription/tumblr_spec.rb +0 -74
  139. data/spec/plugins/subscription/twitter_search_spec.rb +0 -57
  140. data/spec/plugins/subscription/twitter_spec.rb +0 -73
  141. data/spec/plugins/subscription/weather_spec.rb +0 -44
  142. data/spec/plugins/subscription/xml_spec.rb +0 -84
  143. data/spec/spec_helper.rb +0 -106
  144. data/spec/user_dir/plugins/store/mock.rb +0 -16
  145. data/test/fixtures/sampleOPML.xml +0 -11
  146. data/test/integration/test_absoluteurl.yml +0 -25
  147. data/test/integration/test_activerecord.yml +0 -24
  148. data/test/integration/test_add_pocket.yml +0 -26
  149. data/test/integration/test_chan_toru.yml +0 -21
  150. data/test/integration/test_descriptionlink.yml +0 -21
  151. data/test/integration/test_fluentd.yml +0 -22
  152. data/test/integration/test_fulltext.yml +0 -30
  153. data/test/integration/test_google_news.yml +0 -21
  154. data/test/integration/test_googlealert.yml +0 -21
  155. data/test/integration/test_hatenabookmark.yml +0 -30
  156. data/test/integration/test_ignore.yml +0 -25
  157. data/test/integration/test_ignore2.yml +0 -22
  158. data/test/integration/test_image2local.yml +0 -33
  159. data/test/integration/test_instapaper.yml +0 -26
  160. data/test/integration/test_link2local.yml +0 -34
  161. data/test/integration/test_one.yml +0 -23
  162. data/test/integration/test_pocket.yml +0 -22
  163. data/test/integration/test_rand.yml +0 -21
  164. data/test/integration/test_sanitize.yml +0 -23
  165. data/test/integration/test_sort.yml +0 -36
  166. data/test/integration/test_svnlog.yml +0 -15
  167. data/test/integration/test_text2feed.yml +0 -36
  168. data/test/integration/test_tumblr2local.yml +0 -43
  169. data/test/integration/test_twitter_search.yml +0 -22
  170. data/test/integration/test_weather.yml +0 -19
  171. data/test/integration/test_xml2fluentd.yml +0 -21
  172. data/vendor/.gitkeep +0 -0
data/README.md CHANGED
@@ -1,146 +1,698 @@
1
- Automatic Ruby
2
- ==============
1
+ # Automatic Ruby
3
2
 
4
- **Ruby framework for the general-purpose automatic processing**
3
+ **A Ruby framework for general-purpose automatic processing.**
5
4
 
5
+ Automatic Ruby runs jobs that you *assemble* rather than write. A step is a
6
+ plugin — a small Ruby class with one method. A job is a **Recipe** — a YAML file
7
+ naming the plugins in order, with their settings. Running the job is one
8
+ command, which is safe to put in `cron`.
6
9
 
7
- Description
8
- -----------
9
-
10
- This is a general-purpose automatic processing
11
- ruby framework which can extend the functionality
12
- by plug-ins.
10
+ ```yaml
11
+ plugins:
12
+ - module: SubscriptionFeed
13
+ config:
14
+ feeds:
15
+ - https://www.ruby-lang.org/en/feeds/news.rss
13
16
 
14
- See doc/README file.
17
+ - module: StorePermalink
18
+ config:
19
+ db: seen.db
15
20
 
21
+ - module: PublishMarkdown
22
+ config:
23
+ file: ~/notes/feeds.md
24
+ ```
16
25
 
17
- Get Started
18
- -----------
26
+ ```sh
27
+ automatic -c my_recipe.yml
28
+ ```
19
29
 
20
- ``` html
21
- [Stable]
22
- $ gem install automatic
23
- $ automatic scaffold
24
- (Make ~/.automatic on your home directory.)
25
- $ automatic -c ~/.automatic/config/example/feed2console.yml
26
- (This process will be output my blog feed to your terminal.)
30
+ Fetch a public feed, remember what has already been seen, and write the new
31
+ items into a Markdown file: readable by people, reusable by
32
+ tools, ready to hand to an AI. Change the last plugin and the same pipeline
33
+ prints to the terminal instead, or downloads the images, or forwards them to
34
+ Fluentd, or writes them to a database. Write your own plugin and it composes
35
+ with all the others.
36
+
37
+ **[Follow the Quick Start](doc/QUICKSTART.md)** to install the gem, write a
38
+ Recipe that watches four public index pages, install what that Recipe needs, and
39
+ produce Markdown from the articles they list.
40
+
41
+ ---
42
+
43
+ ## Contents
44
+
45
+ 1. [Overview](#1-overview)
46
+ 2. [Features](#2-features)
47
+ 3. [Architecture](#3-architecture)
48
+ 4. [Supported environment](#4-supported-environment)
49
+ 5. [Installation](#5-installation)
50
+ 6. [Quick start](#6-quick-start)
51
+ 7. [Recipes](#7-recipes)
52
+ 8. [Plugins](#8-plugins)
53
+ 9. [CLI usage](#9-cli-usage)
54
+ 10. [Configuration](#10-configuration)
55
+ 11. [The user directory](#11-the-user-directory)
56
+ 12. [Testing](#12-testing)
57
+ 13. [Development](#13-development)
58
+ 14. [Repository structure](#14-repository-structure)
59
+ 15. [Documents](#15-documents)
60
+ 16. [Versioning](#16-versioning)
61
+ 17. [Name](#17-name)
62
+ 18. [License](#18-license)
63
+
64
+ ---
65
+
66
+ ## 1. Overview
67
+
68
+ A recurring job usually has the same shape: fetch something, decide which parts
69
+ of it are new or interesting, keep a record so the same item is not handled
70
+ twice, and send the result somewhere. Writing each such job as a script means
71
+ writing the fetching, the filtering, the de-duplication and the retrying again
72
+ every time.
73
+
74
+ "Somewhere" is often a file. A Recipe that ends in `PublishMarkdown` leaves what
75
+ it collected as a Markdown document: a person reads it as it is, `grep` searches
76
+ it, Git keeps its history, and a program — a language model or an agent among
77
+ them — takes it as input without a parser or an API. That is the general case,
78
+ and it needs no account anywhere; the plugins that publish to a service are for
79
+ when a particular service is the point.
80
+
81
+ Automatic Ruby exists so that those jobs are assembled instead. It contributes
82
+ exactly three things:
83
+
84
+ - **a uniform contract** every step obeys, so that steps compose,
85
+ - **one value** passed from step to step, so that they have something to compose
86
+ over,
87
+ - **a loader** that finds a step by name, so that a Recipe can name it.
88
+
89
+ Everything else is a plugin. The framework is under seven hundred lines of Ruby
90
+ and is meant to stay that size.
91
+
92
+ It is one person's tooling, run unattended from `cron`, against their own
93
+ accounts and their own files. It is not a service, and there is no notion of a
94
+ second user.
95
+
96
+ The project began in February 2012 and this is the first release since 2015. The
97
+ core, the Recipe format and the plugin contract are unchanged; what has changed
98
+ is that it runs on a current Ruby, installs from a current RubyGems, and ships
99
+ a plugin set every part of which still has somewhere to talk to. See
100
+ [`doc/VERSIONS`](doc/VERSIONS).
101
+
102
+ ## 2. Features
103
+
104
+ - **Recipes in YAML.** A job is a file, not a program. No Ruby is written to
105
+ wire a pipeline together.
106
+ - **41 plugins** across seven categories: subscribe, custom feed, filter,
107
+ store, provide, notify, publish — and every one of them has a current use.
108
+ - **Markdown out of the box.** `PublishMarkdown` writes the result as a plain
109
+ Markdown document, to a file or to standard output, with no service and no
110
+ credential behind it. It is the natural end of a new Recipe.
111
+ - **Plugins are found, not registered.** Adding one is dropping a file in a
112
+ directory. No framework file is edited.
113
+ - **Your plugins override the shipped ones.** `~/.automatic/plugins` is searched
114
+ first, so a shipped plugin can be replaced without touching the installation.
115
+ - **De-duplication built in.** The store plugins keep a SQLite record of what
116
+ has been seen, which is what makes a Recipe safe to run every hour. Their
117
+ gems are installed when you use them, not before.
118
+ - **Retry and interval** on everything that reaches the network, configured per
119
+ plugin in the Recipe.
120
+ - **A small installation.** A gem needed by one plugin is not a dependency of
121
+ the framework: `gem install automatic` brings four pure-Ruby gems and the
122
+ command, and installs neither an HTML parser nor a database — let alone an
123
+ AWS SDK.
124
+ - **No museum.** Every plugin is classified, with its reason, in
125
+ [`doc/PLUGINS.md`](doc/PLUGINS.md). Nothing dead is stubbed into looking
126
+ alive, and an integration whose service has gone is removed rather than
127
+ kept as a fossil.
128
+
129
+ ## 3. Architecture
130
+
131
+ ```text
132
+ bin/automatic process entry point; exit status only
133
+ |
134
+ v
135
+ Automatic::CLI options, subcommands, error reporting
136
+ |
137
+ v
138
+ Automatic -> Recipe -> Pipeline -> Automatic::Plugin::*
139
+ | |
140
+ v v
141
+ Automatic::Log Automatic::FeedMaker / FeedParser
27
142
  ```
28
143
 
29
- ``` html
30
- [Development]
31
- $ git clone git://github.com/automaticruby/automaticruby.git
32
- $ cd automaticruby
33
- $ bundle install --path vendor/gems
34
- $ bin/automatic scaffold
35
- $ bin/automatic -c ~/.automatic/config/example/feed2console.yml
36
- (The same as above.)
144
+ One run is: load the Recipe, then for each plugin entry in order, load the
145
+ class, construct it with its settings and the current pipeline, call `run`, and
146
+ take the result as the input to the next.
147
+
148
+ ```ruby
149
+ pipeline = []
150
+ recipe.each_plugin do |plugin|
151
+ mod = plugin.module
152
+ load_plugin(mod)
153
+ klass = Automatic::Plugin.const_get(mod)
154
+ pipeline = klass.new(plugin.config, pipeline).run
155
+ end
37
156
  ```
38
157
 
39
- Specify any recipe with -c option.
158
+ That is the whole of the framework's behaviour. The value passed along — the
159
+ *pipeline* — is an array of feed objects, and because every plugin takes and
160
+ returns that one shape, any plugin composes with any other.
40
161
 
41
- ``` html
42
- automatic -c <recipe>
43
- ```
162
+ The pipeline normally **narrows**: subscription plugins produce, filters and
163
+ stores reduce, publishers consume. Reading a Recipe top to bottom reads the
164
+ data flow.
165
+
166
+ The full account is [`doc/BASIC_DESIGN.md`](doc/BASIC_DESIGN.md).
167
+
168
+ ## 4. Supported environment
44
169
 
45
- Example.
170
+ - **Ruby 3.3 through 4.0.** CI validates 3.3, 3.4 and 4.0.
171
+ - A Unix-like system. GNU/Linux and macOS are what it is used on. Windows is not
172
+ supported.
173
+ - A compiler only if you install an optional plugin gem that builds from source
174
+ on your platform, such as `nokogiri` or `sqlite3`. The framework's own
175
+ dependencies are pure Ruby.
46
176
 
47
- ``` html
48
- $ automatic -c ~/.automatic/config/example/feed2console.yml
177
+ Ruby 3.3 is the floor: it is the oldest maintained release the dependencies are
178
+ resolved and tested against. Nothing older is tested or supported.
179
+
180
+ Two statements, and they are not the same one:
181
+
182
+ - **Supported range.** The code is written for Ruby 3.3 through 4.0, using APIs
183
+ the whole range shares. `required_ruby_version` is `>= 3.3.0` and has no upper
184
+ bound, so a Ruby newer than the matrix is permitted rather than refused.
185
+ - **Continuously validated versions.** CI runs the ends of the range and the
186
+ release in the middle — 3.3, 3.4 and 4.0 — rather than every intermediate
187
+ release. A version's absence from the matrix means it is not verified on every
188
+ commit; it does not mean it is expected to fail.
189
+
190
+ ## 5. Installation
191
+
192
+ ### From RubyGems
193
+
194
+ ```sh
195
+ gem install automatic
196
+ automatic --version
49
197
  ```
50
198
 
199
+ That installs the framework, the command and four pure-Ruby dependencies.
200
+ A gem that only one plugin needs is not among them: install it when you use
201
+ that plugin, with `gem install nokogiri` or `gem install activerecord sqlite3`.
202
+ [`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md) lists which plugin needs which.
51
203
 
52
- What is Recipe?
53
- ---------------
204
+ ### From a checkout
54
205
 
55
- Automatic Ruby parses configuration file that was written
56
- in the form of YAML which including variety of information
57
- of associated plug-ins.
206
+ Use a checkout to try the current development version, change the source,
207
+ develop a plugin or verify changes before a release. There are three ways to
208
+ set one up; start with the first.
58
209
 
59
- This YAML file is called "Recipe".
210
+ ```sh
211
+ git clone https://github.com/id774/automaticruby.git
212
+ cd automaticruby
60
213
 
61
- You can use -c option for specify a file name.
214
+ # Minimal: the framework and its test suite. No optional plugin gem.
215
+ bundle install
62
216
 
63
- The Recipe has an implicit naming convention.
217
+ # All supported optional plugin dependencies, for plugin work.
218
+ bundle config set --local with plugins
219
+ bundle install
220
+
221
+ # Or start minimal and add one group at a time, as you use its plugins.
222
+ bundle config set --local with store
223
+ bundle install
224
+ ```
225
+
226
+ ```sh
227
+ bundle exec bin/automatic --version
228
+ bundle exec rake
229
+ ```
64
230
 
65
- ``` html
231
+ A plain `bundle install` resolves the runtime dependencies declared by
232
+ `automatic.gemspec` and the development ones, and installs no optional plugin
233
+ gem: those are optional Bundler groups, which are installed only when asked
234
+ for. If the `bundle` command is unavailable, install Bundler first with
235
+ `gem install bundler`.
236
+
237
+ In a checkout, every `automatic` below becomes `bundle exec bin/automatic`, and
238
+ the checkout resolves its gems through the bundle — so a plugin's gem is added
239
+ with a group rather than with `gem install`, and a Recipe using plugins from two
240
+ groups selects both at once. The group names, which plugin needs which gem, and
241
+ a Recipe taken step by step through choosing its groups are in
242
+ [`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md); what each plugin does is in
243
+ [`doc/PLUGINS.md`](doc/PLUGINS.md).
244
+
245
+ ## 6. Quick start
246
+
247
+ The complete first-run guide is [`doc/QUICKSTART.md`](doc/QUICKSTART.md): it
248
+ writes one Recipe that reads four public index pages, installs what that Recipe
249
+ needs, and leaves the new articles in a Markdown document.
250
+
251
+ ```sh
252
+ automatic scaffold
253
+ automatic -c ~/.automatic/config/example/feed2markdown.yml
254
+ ```
255
+
256
+ `scaffold` creates `~/.automatic` with `config/`, `plugins/`, `db/` and
257
+ `assets/`, and copies the example Recipes into `~/.automatic/config/example`.
258
+ It never overwrites anything already there.
259
+
260
+ `feed2markdown.yml` is the shortest shipped Recipe: it fetches the public Ruby
261
+ news feed and appends its items to `~/.automatic/markdown/feeds.md`, using
262
+ nothing but the framework and what `gem install automatic` brought.
263
+ `feed2console.yml` beside it is the same pipeline printing to the terminal. Read
264
+ the file, `grep` it, put it in a repository, or hand it to whatever reads text
265
+ next. A Recipe that names a plugin with an optional gem installs that gem first,
266
+ which is step 4 of the Quick Start and the habit worth learning early.
267
+
268
+ To check the framework without any network, write this instead:
269
+
270
+ ```yaml
271
+ # ~/.automatic/config/selftest.yml
66
272
  plugins:
67
- - module: MODULE_NAME
273
+ - module: SubscriptionText
68
274
  config:
69
- VARIABLES
275
+ feeds:
276
+ - title: hello
277
+ url: https://example.com/
278
+ - module: PublishMarkdown
70
279
  ```
71
280
 
72
- For more info, refer to the document (doc/README).
281
+ ```sh
282
+ automatic -c selftest.yml
283
+ ```
284
+
285
+ A bare name is resolved inside `~/.automatic/config`, which is why that command
286
+ has no path in it. Anything with a `/` is used as a path as given.
287
+
288
+ Installing, scheduling and operating it is [`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md).
289
+
290
+ ## 7. Recipes
73
291
 
292
+ A Recipe is one job: which plugins run, in what order, with what settings.
74
293
 
75
- Environment
76
- -----------
294
+ ```yaml
295
+ global: # optional
296
+ log:
297
+ level: info # info | warn | error | none
77
298
 
78
- After Ruby 1.9. See Gemfile.
299
+ plugins: # required
300
+ - module: SubscriptionFeed # required: the plugin class name
301
+ config: # optional: passed to that plugin only
302
+ feeds:
303
+ - https://example.com/feed
304
+ retry: 3
305
+ interval: 5
79
306
 
307
+ - module: PublishConsole # a plugin needing no settings omits config
308
+ ```
80
309
 
81
- Development
82
- -----------
310
+ - Entries run **in the order written**, each receiving the previous one's
311
+ output.
312
+ - `config` is handed to the plugin untouched. The framework does not validate
313
+ it and does not know what any key means.
314
+ - A `module` that resolves to nothing fails **before any plugin runs**, so a
315
+ typo costs nothing.
316
+ - `global.log.level` is the only framework setting. `global.timezone` and
317
+ `global.cache` appear in old Recipes and are read by nothing.
318
+
319
+ Two conventions worth knowing before writing one:
320
+
321
+ - **Put a store plugin in front of anything with an effect.** `StorePermalink`
322
+ records what has been seen and passes on only what has not. It is what makes a
323
+ Recipe safe to run every hour instead of once.
324
+ - **Set `interval` on anything that fetches repeatedly**, in seconds. Scraping
325
+ politely is a requirement of this project, not a courtesy.
326
+
327
+ The full specification — every key, every failure mode, the type conventions,
328
+ and the compatibility promise — is [`doc/PLUGINS.md`](doc/PLUGINS.md) section 2.
329
+
330
+ ## 8. Plugins
331
+
332
+ A plugin is a class in `Automatic::Plugin`, built with two arguments, answering
333
+ one method:
334
+
335
+ ```ruby
336
+ module Automatic::Plugin
337
+ class FilterShortTitle
338
+ def initialize(config, pipeline = [])
339
+ @config = config || {}
340
+ @pipeline = pipeline
341
+ @max = (@config['max_length'] || 40).to_i
342
+ end
343
+
344
+ def run
345
+ @pipeline.each_with_object([]) do |feeds, returned|
346
+ next if feeds.nil?
347
+
348
+ kept = feeds.items.select { |item| item.title.to_s.length <= @max }
349
+ returned << Automatic::FeedMaker.create_pipeline(kept) unless kept.empty?
350
+ end
351
+ end
352
+ end
353
+ end
354
+ ```
83
355
 
84
- We need your help.
356
+ Save it as `~/.automatic/plugins/filter/short_title.rb` and a Recipe can name
357
+ `FilterShortTitle`. Nothing was registered: the class name and the file path are
358
+ the same fact written twice, and the loader converts between them.
359
+
360
+ For a smaller complete example, including testing guidance, see
361
+ [`doc/PLUGIN_DEVELOPMENT.md`](doc/PLUGIN_DEVELOPMENT.md).
362
+
363
+ | Category | Directory | Role |
364
+ | --- | --- | --- |
365
+ | `Subscription` | `subscription/` | Acquire from outside |
366
+ | `CustomFeed` | `custom_feed/` | Build a feed from a source that is not one |
367
+ | `Filter` | `filter/` | Select, reorder, rewrite |
368
+ | `Store` | `store/` | Persist, and drop what was seen before |
369
+ | `Provide` | `provide/` | Emit the payload elsewhere |
370
+ | `Notify` | `notify/` | Send a notification |
371
+ | `Publish` | `publish/` | Send the result out, print it, or write it as a document |
372
+
373
+ `~/.automatic/plugins` is searched **before** the installation, so a file named
374
+ like a shipped plugin replaces it.
375
+
376
+ ### Which plugins still work
377
+
378
+ 41 plugins ship with the gem. Every one is classified in
379
+ [`doc/PLUGINS.md`](doc/PLUGINS.md) section 6, with its settings and the reason
380
+ for its status:
381
+
382
+ | Status | Count | Meaning |
383
+ | --- | --- | --- |
384
+ | **Supported** | 26 | Works on the supported Rubies with current dependencies |
385
+ | **Supported (external)** | 14 | Works, but needs something you provide: a service, a command, a credential, a data file |
386
+ | **Needs rework** | 1 | The service exists; this plugin speaks a replaced interface |
387
+
388
+ Eleven plugins were removed in this release rather than kept as history: each
389
+ talked to a service that has shut down, or through an API that has been
390
+ withdrawn with no replacement. They are listed with their reasons in
391
+ [`doc/PLUGINS.md`](doc/PLUGINS.md) section 8, and Git history holds the code.
392
+ A Recipe naming one of them now fails at load, before anything runs.
393
+
394
+ Restoring the one in **Needs rework** — `PublishHatenaBookmark` — is
395
+ self-contained work and a good first contribution.
396
+
397
+ No plugin here is stubbed, mocked or simulated to make a test pass. Where a
398
+ plugin's gem is not installed its spec is skipped and says which gem is
399
+ missing; where the plugin still loads, its spec covers what does not need the
400
+ service. A dead integration is never made to look alive — it is removed.
401
+
402
+ The contract, a worked example, and how to test a plugin are in
403
+ [`doc/PLUGINS.md`](doc/PLUGINS.md) sections 3 and 4.
404
+
405
+ ## 9. CLI usage
406
+
407
+ ```sh
408
+ automatic -c RECIPE # run a Recipe
409
+ automatic SUBCOMMAND [ARGS] # run an auxiliary tool
410
+ automatic --help
411
+ automatic --version
412
+ ```
85
413
 
86
- **Repository**
414
+ ### Options
415
+
416
+ | Option | Meaning |
417
+ | --- | --- |
418
+ | `-c`, `--config FILE` | The Recipe to run. A bare name is looked for in `~/.automatic/config`. |
419
+ | `-h`, `--help` | Print usage and exit `0`. |
420
+ | `-v`, `--version` | Print the version and exit `0`. |
421
+
422
+ ### Subcommands
423
+
424
+ | Subcommand | What it does |
425
+ | --- | --- |
426
+ | `scaffold` | Create `~/.automatic` and its subdirectories. Overwrites nothing. |
427
+ | `unscaffold` | Remove `~/.automatic` entirely, **including your Recipes and databases**. |
428
+ | `autodiscovery <url>` | Print the feed URLs a page advertises. |
429
+ | `feedparser <url>` | Parse a feed and print the result. |
430
+ | `inspect <url>` | Discover a page's feeds, then parse the first. |
431
+ | `opmlparser <path>` | Print the feed URLs in an OPML file. |
432
+ | `log <level> <message>` | Emit one line in the framework's log format. |
433
+
434
+ The middle five answer "will this work as a Recipe input?" before you write the
435
+ Recipe:
436
+
437
+ ```sh
438
+ automatic autodiscovery https://example.com/
439
+ automatic inspect https://example.com/
440
+ automatic opmlparser subscriptions.opml > feeds.txt
441
+ ```
87
442
 
88
- + https://github.com/automaticruby/automaticruby
443
+ ### Exit status
89
444
 
90
- **Issues**
445
+ | Status | Meaning |
446
+ | --- | --- |
447
+ | `0` | The Recipe ran, the subcommand did its work, or help or version was printed |
448
+ | `1` | The run or the subcommand failed, or nothing was asked for |
449
+ | `2` | The command line was rejected |
91
450
 
92
- + https://github.com/automaticruby/automaticruby/issues
451
+ A `cron` entry can rely on these.
93
452
 
94
- **RubyFroge**
453
+ ## 10. Configuration
95
454
 
96
- + http://rubyforge.org/projects/automatic/
455
+ There is no configuration file besides the Recipe. Every setting a job needs is
456
+ in the Recipe that defines the job, which is what makes a Recipe portable
457
+ between machines.
97
458
 
98
- **RubyGems.org**
459
+ **Framework settings** — one, `global.log.level`, with the values `info`,
460
+ `warn`, `error` and `none`.
99
461
 
100
- + https://rubygems.org/gems/automatic
462
+ **Plugin settings** — the `config` mapping of a plugin entry, passed to that
463
+ plugin and read by nothing else. Established names: `retry` for an attempt
464
+ count, `interval` for seconds between attempts, `db` for a database file,
465
+ `path` for a directory.
101
466
 
102
- **CI**
467
+ **Credentials** are plugin settings, which makes a Recipe holding one a secret
468
+ file:
103
469
 
104
- + http://jenkins.id774.net/jenkins/
470
+ ```sh
471
+ chmod 600 ~/.automatic/config/publish.yml
472
+ ```
105
473
 
474
+ Nothing encrypts it and nothing keeps it elsewhere. Keep credentials in their
475
+ own Recipe, and never commit one. This is a weakness inherited from the original
476
+ design and is recorded as one in
477
+ [`doc/REQUIREMENTS.md`](doc/REQUIREMENTS.md) section 17.
106
478
 
107
- ChangeLog
108
- ---------
479
+ **A Recipe is trusted local configuration.** It names Ruby classes and the
480
+ framework runs them, so anyone who can write a Recipe — or a file under
481
+ `~/.automatic/plugins` — can run code as you. Do not run a Recipe from a source
482
+ you do not trust. (Recipes are parsed with `YAML.safe_load` so that the document
483
+ itself cannot instantiate arbitrary Ruby classes, but that is a second line of
484
+ defence, not the boundary.)
109
485
 
110
- See doc/ChangeLog file.
486
+ ## 11. The user directory
111
487
 
488
+ `~/.automatic` holds what is yours, so that it survives reinstalling the gem.
112
489
 
113
- Versioning
114
- ----------
490
+ | Path | Holds |
491
+ | --- | --- |
492
+ | `~/.automatic/config` | Your Recipes. A bare `-c` name is resolved here. |
493
+ | `~/.automatic/plugins` | Your plugins, in category subdirectories. Searched first. |
494
+ | `~/.automatic/db` | SQLite databases the store plugins write. |
495
+ | `~/.automatic/assets` | Data files plugins read, such as the fulltext siteinfo. |
115
496
 
116
- Releases will be numbered with the follow format:
497
+ Each part is optional; where one is absent, the corresponding directory inside
498
+ the installation is used instead. `automatic scaffold` creates and seeds them
499
+ and overwrites nothing, so it is safe to run after an upgrade.
117
500
 
118
- `<year>.<month>`
501
+ ## 12. Testing
119
502
 
120
- This naming convention is to mimic Ubuntu.
503
+ ```sh
504
+ bundle exec rake # the whole suite
505
+ bundle exec rake spec:lib # the framework only
506
+ bundle exec rake spec:plugins
507
+ COVERAGE=on bundle exec rake spec
508
+ ```
121
509
 
510
+ - **The suite reaches no network and needs no credential.** That is a rule, not
511
+ a coincidence, and CI configures no secret.
512
+ - Specs mirror the source tree: `spec/lib/` for the framework,
513
+ `spec/plugins/<category>/` for plugins.
514
+ - Examples tagged `:network` reach real hosts and are **excluded by default**.
515
+ Several point at hosts that no longer serve what they expect, which is why
516
+ they are not a gate. Run them deliberately:
517
+
518
+ ```sh
519
+ AUTOMATIC_NETWORK_SPECS=1 bundle exec rake spec
520
+ ```
521
+
522
+ - A plugin whose gem the Gemfile declares in an optional group is **not
523
+ verified by the default suite**, because no optional group is installed.
524
+ Install them to run those specs as part of the ordinary suite:
525
+
526
+ ```sh
527
+ bundle config set --local with plugins
528
+ bundle install
529
+ bundle exec rake
530
+ ```
531
+
532
+ - A spec whose plugin needs a gem that is not installed is skipped, and says
533
+ which gem is missing. That absence is the signal; a plugin whose service no
534
+ longer exists is never stubbed into passing.
535
+ - `test/integration/` holds Recipes for exercising plugins against real
536
+ services. They are run by hand, are not part of the suite, and are never run
537
+ in CI. Most need a credential or a service you run — read one before
538
+ running it.
539
+
540
+ The required check installs the bundle, builds the gem, loads the library, runs
541
+ the CLI and runs the default suite on each validated Ruby version, from
542
+ [`.github/workflows/ci.yml`](.github/workflows/ci.yml). It configures no secret
543
+ and installs no optional plugin gem, so no plugin's own dependency is a
544
+ condition of a change being merged — and what it proves on every commit is that
545
+ the framework needs nothing but its own runtime dependencies. A separate,
546
+ non-required workflow,
547
+ [`.github/workflows/plugins.yml`](.github/workflows/plugins.yml), installs the
548
+ `plugins` group and runs the same suite, which is how the all-plugins setup is
549
+ checked.
550
+
551
+ ## 13. Development
552
+
553
+ ```sh
554
+ git clone https://github.com/id774/automaticruby.git
555
+ cd automaticruby
556
+ bundle install
557
+ bundle exec rake
558
+ bundle exec bin/automatic -c config/feed2console.yml
559
+ ```
122
560
 
123
- Developers
124
- ----------
561
+ Contributions are welcome — a new plugin, or reviving the one that needs
562
+ rework, most of all.
563
+
564
+ 1. Fork the repository.
565
+ 2. Write the change, with a spec that reaches no network.
566
+ 3. Update the documents in the same commit. A behaviour change with no
567
+ documentation change is not finished.
568
+ 4. Send a pull request.
569
+
570
+ Read [`doc/POLICY.md`](doc/POLICY.md) first. It states the rules a change is
571
+ judged by: the direction of dependency, where a new capability belongs, how
572
+ dependencies are added, how the documents divide, and how a version history
573
+ entry is written.
574
+
575
+ Two rules worth knowing before you start:
576
+
577
+ - **A gem needed by one plugin is not a dependency of the framework.** Require
578
+ it at the top of the plugin's own file.
579
+ - **Nothing dead is faked.** A plugin whose service has shut down is removed,
580
+ not stubbed into passing a test.
581
+
582
+ - Repository: <https://github.com/id774/automaticruby>
583
+ - Issues: <https://github.com/id774/automaticruby/issues>
584
+ - RubyGems: <https://rubygems.org/gems/automatic>
585
+
586
+ ## 14. Repository structure
587
+
588
+ ```text
589
+ .
590
+ ├── bin/automatic The executable. A process entry point and nothing else.
591
+ ├── lib/
592
+ │ ├── automatic.rb The module: directories, and run
593
+ │ └── automatic/
594
+ │ ├── cli.rb Options and subcommands; returns an exit status
595
+ │ ├── recipe.rb Loads and validates a Recipe
596
+ │ ├── pipeline.rb Finds plugins by name and runs them in order
597
+ │ ├── log.rb Levelled logging to standard output
598
+ │ ├── feed_maker.rb Builds pipeline values from plain data
599
+ │ ├── feed_parser.rb Fetches and parses feeds
600
+ │ ├── http.rb The one way in for what plugins fetch
601
+ │ ├── opml.rb OPML parser, for the opmlparser subcommand
602
+ │ ├── environment.rb Bundler setup for a source checkout
603
+ │ └── version.rb
604
+ ├── plugins/ The shipped plugins, one directory per category
605
+ │ ├── subscription/ Acquire from outside
606
+ │ ├── custom_feed/ Build a feed from a source that is not one
607
+ │ ├── filter/ Select, reorder, rewrite
608
+ │ ├── store/ Persist, and drop what was seen before
609
+ │ ├── provide/ Emit the payload elsewhere
610
+ │ ├── notify/ Send a notification
611
+ │ └── publish/ Send the result out, print it, or write it as a document
612
+ ├── config/ Example Recipes; scaffold copies these out
613
+ ├── assets/siteinfo/ Data files plugins read
614
+ ├── db/ Fallback for SQLite files when ~/.automatic/db is absent
615
+ ├── spec/ RSpec suite, mirroring lib/ and plugins/
616
+ ├── test/
617
+ │ ├── fixtures/ Fixtures for the manual tests
618
+ │ └── integration/ Recipes run by hand against real services
619
+ ├── script/build Runs what CI runs, plus the integration recipes
620
+ ├── vendor/ Legacy placeholder; the normal setup does not install gems here
621
+ ├── doc/ See below
622
+ ├── automatic.gemspec Hand-maintained
623
+ ├── Gemfile
624
+ ├── Rakefile
625
+ └── VERSION
626
+ ```
125
627
 
126
- See doc/AUTHORS or following link.
628
+ ## 15. Documents
629
+
630
+ Everything needed to understand, build, run and change this repository is in
631
+ this repository. No document here defers to another repository.
632
+
633
+ | Document | What it holds |
634
+ | --- | --- |
635
+ | [`doc/QUICKSTART.md`](doc/QUICKSTART.md) | The shortest path from installation to a Markdown result |
636
+ | [`doc/AI_TUTORIAL.md`](doc/AI_TUTORIAL.md) | Building a Recipe that collects web articles and asks an AI service one question about them |
637
+ | [`doc/PLUGIN_DEVELOPMENT.md`](doc/PLUGIN_DEVELOPMENT.md) | A complete user plugin and practical testing guidance |
638
+ | [`doc/REQUIREMENTS.md`](doc/REQUIREMENTS.md) | What the system is for, what it guarantees, where its responsibility ends |
639
+ | [`doc/BASIC_DESIGN.md`](doc/BASIC_DESIGN.md) | How it is composed: the parts, their responsibilities, the flow of a run |
640
+ | [`doc/PLUGINS.md`](doc/PLUGINS.md) | The Recipe format, the plugin contract, and the catalogue of every shipped plugin |
641
+ | [`doc/POLICY.md`](doc/POLICY.md) | How a change is made and judged: style, dependencies, tests, versioning |
642
+ | [`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md) | Installing, scheduling, operating, and what to do when it fails |
643
+ | [`doc/RELEASING.md`](doc/RELEASING.md) | For maintainers: building, verifying and publishing the gem |
644
+ | [`doc/VERSIONS`](doc/VERSIONS) | The release history, from 2012 |
645
+ | [`doc/LICENSE.md`](doc/LICENSE.md) | The licence |
646
+ | [`doc/COPYING`](doc/COPYING) | The GPLv3 text |
647
+ | [`doc/COPYING.LESSER`](doc/COPYING.LESSER) | The LGPLv3 text |
648
+ | [`doc/AUTHORS`](doc/AUTHORS) | Contributors |
649
+
650
+ ## 16. Versioning
651
+
652
+ Releases are numbered `<year>.<month>`, two digits each, taken from the release
653
+ date. The scheme mimics Ubuntu's and has been used since the first release in
654
+ February 2012.
655
+
656
+ ```text
657
+ 26.08 a release made in August 2026
658
+ 26.08.1 a release correcting 26.08 in the same month
659
+ ```
127
660
 
128
- + https://github.com/automaticruby?tab=members
661
+ The number carries no compatibility meaning: a month is not a major version. A
662
+ change that affects an existing Recipe is stated as such in its
663
+ [`doc/VERSIONS`](doc/VERSIONS) entry.
129
664
 
130
- Project created by
665
+ What this repository promises not to break — the Recipe format, the plugin
666
+ contract, the plugin naming rule, the user directory, the CLI, and existing
667
+ store databases — is listed in [`doc/POLICY.md`](doc/POLICY.md) section 7.
131
668
 
132
- + http://id774.net
133
- + http://github.com/id774
669
+ ## 17. Name
134
670
 
671
+ The name **Automatic Ruby** was partly inspired by David Heinemeier Hansson's
672
+ story of naming Ruby on Rails. He first chose *Rails* for the idea of putting
673
+ development "on rails"; when the obvious Rails domain names were unavailable,
674
+ `rubyonrails.com` was free. He later said he liked the result because it put
675
+ Ruby itself more prominently in the name and helped introduce Ruby to a wider
676
+ audience. See [his account of the naming](https://tim.blog/2018/06/05/the-tim-ferriss-show-transcripts-david-dhh-heinemeier-hansson/).
135
677
 
136
- License
137
- -------
678
+ I took a simple lesson from that story: a software name does not have to
679
+ literally describe its architecture. This project was inspired in part by
680
+ Plagger, and I wanted the name to put two things up front: **automatic**
681
+ processing, and doing it in **Ruby** rather than Perl. *Automatic Ruby* said
682
+ both, sounded natural enough, and was easy to remember. That was enough.
138
683
 
139
- Licensed under the GNU GENERAL PUBLIC LICENSE, Version 3.0.
684
+ ## 18. License
140
685
 
141
- http://www.gnu.org/copyleft/gpl.html
686
+ Automatic Ruby is dual-licensed under the
687
+ [GNU General Public License, Version 3](https://www.gnu.org/licenses/gpl-3.0.html)
688
+ or the
689
+ [GNU Lesser General Public License, Version 3](https://www.gnu.org/licenses/lgpl-3.0.html).
690
+ You may choose either license at your discretion.
142
691
 
143
- Caution!!! This software is NOT under the terms of the LGPL.
144
- See the file doc/COPYING.
692
+ See [`doc/LICENSE.md`](doc/LICENSE.md), [`doc/COPYING`](doc/COPYING) and
693
+ [`doc/COPYING.LESSER`](doc/COPYING.LESSER) for the full license texts.
145
694
 
695
+ Copyright (c) 2012-2026 Automatic Ruby Developers.
146
696
 
697
+ Project created by [id774](http://id774.net). Contributors are listed in
698
+ [`doc/AUTHORS`](doc/AUTHORS).