automatic 14.12.2 → 26.09

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