syntropy 0.39.0 → 0.41.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/TODO.md +44 -142
  4. data/cmd/console.rb +0 -2
  5. data/cmd/new/template/config/production.rb +2 -1
  6. data/cmd/new.rb +1 -1
  7. data/cmd/serve.rb +26 -0
  8. data/examples/agent/Dockerfile +25 -0
  9. data/examples/agent/Gemfile +3 -0
  10. data/examples/agent/README.md +40 -0
  11. data/examples/agent/TODO.md +5 -0
  12. data/examples/agent/app/_layout/default.rb +15 -0
  13. data/examples/agent/app/_lib/storage.rb +13 -0
  14. data/examples/agent/app/_schema/2026-01-01-initial.rb +9 -0
  15. data/examples/agent/app/assets/agent.js +63 -0
  16. data/examples/agent/app/assets/minigfm.js +207 -0
  17. data/examples/agent/app/assets/model.js +45 -0
  18. data/examples/agent/app/assets/style.css +25 -0
  19. data/examples/agent/app/assets/tools.js +74 -0
  20. data/examples/agent/app/assets/ui.js +50 -0
  21. data/examples/agent/app/index.rb +19 -0
  22. data/examples/agent/app/test.rb +7 -0
  23. data/examples/agent/config/Caddyfile +5 -0
  24. data/examples/agent/config/development.rb +5 -0
  25. data/examples/agent/config/production.rb +5 -0
  26. data/examples/agent/config/test.rb +5 -0
  27. data/examples/agent/docker-compose.yml +71 -0
  28. data/examples/agent/response_example.json +108 -0
  29. data/examples/agent/test/test_app.rb +14 -0
  30. data/examples/blog/app/foo.rb +1 -0
  31. data/examples/blog/config/production.rb +2 -1
  32. data/lib/syntropy/app.rb +6 -2
  33. data/lib/syntropy/applets/builtin/auto_refresh/watch.js +1 -1
  34. data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +7 -0
  35. data/lib/syntropy/dev_mode.rb +8 -3
  36. data/lib/syntropy/http/client.rb +17 -1
  37. data/lib/syntropy/http/client_connection.rb +16 -0
  38. data/lib/syntropy/http/io_extensions.rb +32 -4
  39. data/lib/syntropy/http/server.rb +55 -24
  40. data/lib/syntropy/http/server_connection.rb +75 -4
  41. data/lib/syntropy/logger.rb +8 -0
  42. data/lib/syntropy/markdown.rb +44 -20
  43. data/lib/syntropy/module_loader.rb +29 -16
  44. data/lib/syntropy/request/request_info.rb +1 -1
  45. data/lib/syntropy/request/response.rb +75 -6
  46. data/lib/syntropy/request/validation.rb +6 -0
  47. data/lib/syntropy/storage/connection_pool.rb +34 -0
  48. data/lib/syntropy/test.rb +4 -4
  49. data/lib/syntropy/version.rb +1 -1
  50. data/lib/syntropy.rb +7 -1
  51. data/syntropy.gemspec +4 -4
  52. data/test/test_markdown.rb +3 -3
  53. data/test/test_module_loader.rb +4 -1
  54. metadata +34 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6b3b46669d534dbaf6517708d7fef38fd44bf20bd99513e065d173ca231116d
4
- data.tar.gz: 2aaa7078c2c81adae4bae1141130bfedffd08c2239ea492b2c0076ad40ae9dcf
3
+ metadata.gz: 5fa009d6a635c90587a2a4e9a6b4b55751969f6185a175c80d4cc5a01cee82e5
4
+ data.tar.gz: 04fb7ee45c534fc052c1a4674d32c35a17a94fac8e4100c4af7e704df847f6e4
5
5
  SHA512:
6
- metadata.gz: '00499df3f4e992f461a36a33efb467d46d719e5b29c6756f0bdfc6a88dbf85355619fef1a61e39a96ec36d25b492c8a862db47b551f725ec0d841fc0ec66105a'
7
- data.tar.gz: 7d8a2f86e69873779e03a9acc65fed022c05ecd543ce48a9e0fa77d3fadc774712fc2627c8a299a1ee764e3eb3800cf4cb31ccafd5e91e035bc37bda401e8c11
6
+ metadata.gz: 271e3fddd94f4a209d1207ce7c6447a8dbd8d50890467cc2937da4ba60332740352035b4ec99de89a6ae75ec34798dd92fa58da00c8a7f146a5cfa27df6a312e
7
+ data.tar.gz: 29463012056e3f3a848fcfb5d8505bf47b89f2534fcee59b1872ea0f2dc82333719773c73e93f119a1ab6fb6508ee609b738565508d903efca158797faff4c2c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 0.41.0 2026-08-25
2
+
3
+ - Don't log ECONNRESET errors
4
+
5
+ # 0.40.0 2026-08-24
6
+
7
+ - Update UringMachine
8
+
1
9
  # 0.39.0 2026-06-21
2
10
 
3
11
  - Add support for embedded Papercraft snippets in Markdown files
data/TODO.md CHANGED
@@ -1,10 +1,43 @@
1
1
  ## Immediate
2
2
 
3
+ - [ ] Nicer logging in development mode:
4
+
5
+ Nice terminal formatting with colors:
6
+
7
+ ```
8
+ sharon@nf1:~/tmp/capatest2$ npx serve dist
9
+
10
+ ┌───────────────────────────────────────────┐
11
+ │ │
12
+ │ Serving! │
13
+ │ │
14
+ │ - Local: http://localhost:3000 │
15
+ │ - Network: http://192.168.0.106:3000 │
16
+ │ │
17
+ │ Copied local address to clipboard! │
18
+ │ │
19
+ └───────────────────────────────────────────┘
20
+
21
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /
22
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 42 ms
23
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/index-edfizW3i.js
24
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/index-PVjztr5e.css
25
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 3 ms
26
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 12 ms
27
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/p-Sh0ICmPV-D227nRX-.js
28
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 3 ms
29
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/p-C4t5ymfq-4gquBJ2r.js
30
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 6 ms
31
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/p-D6Ynv7Xh-CRAF8up3.js
32
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 4 ms
33
+ ^C
34
+ INFO Gracefully shutting down. Please wait...
35
+ ```
36
+
3
37
  - [ ] Ability to load modules from builtin applet
4
38
  - [ ] Can we mount them on the app's module loader?
5
39
 
6
- - [ ] Controllers
7
- - [ ] add dispatch_json_rpc, dispatch_json_api
40
+ Why we need that? The use case is making use of a default pub/sub instance
8
41
 
9
42
  - [ ] Pub/sub
10
43
  - [ ] Ruby side
@@ -12,6 +45,9 @@
12
45
  - [ ] Reimplement `auto_refresh` using a *default* event bus provided by
13
46
  Syntropy
14
47
 
48
+ - [ ] An alternative to the pub/sub design - add streaming responses to the
49
+ JSON/jS API (using SSE).
50
+
15
51
  ## Collections
16
52
 
17
53
  - [ ] Collection - treat directories and files as collections of data.
@@ -52,21 +88,18 @@
52
88
  - [ ] Website
53
89
  - [v] Frontend part of JSON API
54
90
  - [v] Auto-refresh page when file changes
55
- - [ ] SQLite database capabilities
56
- - [ ] Stores
57
- - [ ] KV store (with TTL)
58
91
  - [v] Examples
59
92
  - [v] Reactive app - counter or some other simple app showing interaction with
60
93
  server
61
- - [ ] blog
94
+ - [v] blog
62
95
 
63
96
  ## Testing facilities
64
97
 
65
98
  - What do we need to test?
66
- - Routes
67
- - Route responses
68
- - Changes to state / DB
69
- - Rendered HTML - presence of certain markup / elements / text
99
+ - [v] Routes
100
+ - [v] Route responses
101
+ - [v] Changes to state / DB
102
+ - [ ] Rendered HTML - presence of certain markup / elements / text
70
103
 
71
104
  ## Support for applets
72
105
 
@@ -102,138 +135,7 @@ class Syntropy::Admin < Syntropy::App
102
135
  end
103
136
  ```
104
137
 
105
- ## Response: cookies and headers
106
-
107
- We need a way to inject cookies into the response. This probably should be done
108
- in the TP2 code:
109
-
110
- ```ruby
111
- @@default_set_cookie_attr = 'HttpOnly'
112
- def self.default_set_cookie_attr=(v)
113
- @@default_set_cookie_attr = v
114
- end
115
-
116
- def set_cookie(key, value, attr = @@default_set_cookie_attr)
117
- @buffered_headers ||= +''
118
- @buffered_headers << format(
119
- "Set-Cookie: %<key>s=%<value>s; %<attr>s\n",
120
- key:, value:, attr:
121
- )
122
- end
123
-
124
- def set_headers(headers)
125
- @buffered_headers ||= +''
126
- @buffered_headers << format_headers(headers)
127
- end
128
-
129
- ...
130
-
131
- req.set_cookie('at', 'foobar', 'SameSite=none; Secure; HttpOnly')
132
- ```
133
-
134
- ## Middleware
135
-
136
- Some standard middleware:
137
-
138
- - request rewriter
139
- - logger
140
- - auth
141
- - selector + terminator
142
-
143
- ```Ruby
144
- # For the chainable DSL shown below, we need to create a custom class:
145
- class Syntropy::Middleware::Selector
146
- def initialize(select_proc, terminator_proc = nil)
147
- @select_proc = select_proc
148
- @terminator_proc = terminator_proc
149
- end
150
-
151
- def to_proc
152
- ->(req, proc) {
153
- @select_proc.(req) ? @terminator_proc.(req) : proc(req)
154
- }
155
- end
156
-
157
- def terminate(&proc)
158
- @terminator_proc = proc
159
- end
160
- end
161
- ```
162
-
163
- ```Ruby
164
- # a _site.rb file can be used to wrap a whole app
165
- # site/_site.rb
166
-
167
- # this means we route according to the host header, with each
168
- export Syntropy.route_by_host
169
-
170
- # we can also rewrite requests:
171
- rewriter = Syntropy
172
- .select { it.host =~ /^tolkora\.(org|com)$/ }
173
- .terminate { it.redirect_permanent('https://tolkora.net') }
174
-
175
- # This is actuall a pretty interesting DSL design:
176
- # a chain of operations that compose functions. So, we can select a
177
- export rewriter.wrap(default_app)
178
-
179
- # composing
180
- export rewriter.wrap(Syntropy.some_custom_app.wrap(app))
181
-
182
- # or maybe
183
- export rewriter << some_other_middleware << app
184
- ```
185
-
186
- ## CLI tool for setting up a site repo:
187
-
188
- ```bash
189
- # clone a newly created repo
190
- ~/repo$ git clone https://github.com/foo/bar
191
- ...
192
- ~/repo$ syntropy setup bar
193
-
194
- (syntropy banner)
195
-
196
- Setting up Syntropy project in /home/sharon/repo/bar:
197
-
198
- bar/
199
- bin/
200
- start
201
- stop
202
- restart
203
- console
204
- server
205
- docker-compose.yml
206
- Dockerfile
207
- Gemfile
208
- proxy/
209
- README.md
210
- site/
211
- _layout/
212
- default.rb
213
- _lib/
214
- about.md
215
- articles/
216
- long-form.md
217
- assets/
218
- js/
219
- css/
220
- style.css
221
- img/
222
- syntropy.png
223
- index.rb
224
- ```
225
-
226
- Some of the files might need templating, but we can maybe do without, or at
227
- least make it as generic as possible.
228
-
229
- `syntropy setup` steps:
230
-
231
- 1. Verify existence of target directory
232
- 2. Copy files from Syntropy template to target directory
233
- 3. Do chmod +x for bin/*
234
- 4. Do bundle install in the target directory
235
- 5. Show some information with regard to how to get started working with the
236
- repo
138
+ ## Some more CLI commands
237
139
 
238
140
  `syntropy provision` steps:
239
141
 
data/cmd/console.rb CHANGED
@@ -72,8 +72,6 @@ env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
72
72
  @app = Syntropy::App.load(env)
73
73
  @env = env
74
74
  @machine = env[:machine]
75
- @connection_pool = @app.connection_pool if @app.respond_to?(:connection_pool)
76
- @schema = @app.schema if @app.respond_to?(:schema)
77
75
  @module_loader = @app.module_loader
78
76
 
79
77
  require 'uringmachine/fiber_scheduler'
@@ -1,4 +1,5 @@
1
1
  export(
2
- storage:
2
+ storage: {
3
3
  path: ENV['DATABASE_PATH'] || 'storage/production.db'
4
+ }
4
5
  )
data/cmd/new.rb CHANGED
@@ -44,7 +44,7 @@ template_path = File.join(__dir__, 'new/template')
44
44
  begin
45
45
  `mkdir -p "#{path}"`
46
46
  system("cp -rv#{opts[:yes] ? '' : 'i'} #{template_path}/* \"#{path}/\"")
47
- puts "Your app is ready in #{path}"
47
+ puts "Your app is ready in #{full_path}"
48
48
  rescue => e
49
49
  p e
50
50
  p e.backtrace
data/cmd/serve.rb CHANGED
@@ -99,4 +99,30 @@ Fiber.set_scheduler(UM::FiberScheduler.new(env[:machine]))
99
99
  require 'syntropy/dev_mode' if Syntropy.dev_mode
100
100
 
101
101
  app = Syntropy::App.load(env)
102
+
103
+ if env[:mode] == 'development'
104
+ @main_fiber = Fiber.current
105
+ env[:machine].spin do
106
+ @app = app
107
+ @env = env
108
+ @machine = env[:machine]
109
+ @module_loader = @app.module_loader
110
+
111
+ loop do
112
+ puts "Enter command: (c + Enter to open console, q + Enter to quit)"
113
+ command = $stdin.gets.chomp
114
+ case command
115
+ when 'c'
116
+ t = Thread.new {
117
+ require 'irb'
118
+ IRB.start
119
+ }
120
+ t.join
121
+ when 'q'
122
+ @machine.schedule(@main_fiber, UM::Terminate.new)
123
+ end
124
+ end
125
+ end
126
+ end
127
+
102
128
  Syntropy.run(env) { app.call(it) }
@@ -0,0 +1,25 @@
1
+ FROM ruby:4.0-slim AS base
2
+ WORKDIR /syntropy
3
+ RUN apt-get update -qq && apt-get install -y \
4
+ curl \
5
+ build-essential \
6
+ libpq-dev \
7
+ libsqlite3-dev \
8
+ libssl-dev \
9
+ libyaml-dev \
10
+ pkg-config && \
11
+ rm -rf /var/lib/apt/lists /var/cache/apt/archives
12
+
13
+ ENV BUNDLE_PATH="/usr/local/bundle"
14
+ COPY Gemfile Gemfile.lock ./
15
+ RUN bundle install
16
+
17
+ RUN groupadd --system --gid 1000 syntropy && \
18
+ useradd syntropy --uid 1000 --gid 1000 --create-home --shell /bin/bash
19
+ USER 1000:1000
20
+
21
+ EXPOSE 1234
22
+
23
+ # Start the main process
24
+ # CMD ["bash"]
25
+ CMD ["bundle", "exec", "syntropy", "serve"]
@@ -0,0 +1,3 @@
1
+ source 'https://gem.coop'
2
+
3
+ gem 'syntropy', '~>0.39.0', path: '../..'
@@ -0,0 +1,40 @@
1
+ # My awesome Syntropy app
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ $ bundle install
7
+ ```
8
+
9
+ ## Running the web server
10
+
11
+ ```bash
12
+ $ bundle exec syntropy serve
13
+ ```
14
+
15
+ ## Starting the app console
16
+
17
+ ```bash
18
+ $ bundle exec syntropy console
19
+ ```
20
+
21
+ ### Running tests
22
+
23
+ ```bash
24
+ $ bundle exec syntropy tests
25
+ ```
26
+
27
+ ### Usage with Docker Compose
28
+
29
+ You can also run the app on Docker Compose:
30
+
31
+ ```bash
32
+ # run the app server
33
+ $ docker compose up
34
+
35
+ # run the console
36
+ $ docker compose run --remove-orphans console
37
+
38
+ # run tests
39
+ $ docker compose run --remove-orphans test
40
+ ```
@@ -0,0 +1,5 @@
1
+ - switch to JS modules, reorganize code
2
+ - bring it over to the Reality code
3
+ - Implement error response from tool (using try..catch)
4
+ - Implement OpenRouter OAuth integration (can it work on localhost?)
5
+ -
@@ -0,0 +1,15 @@
1
+ export template { |**props|
2
+ html {
3
+ head {
4
+ title "My awesome Syntropy web app"
5
+ meta charset: 'utf-8'
6
+ meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
7
+ link rel: "stylesheet", href: 'https://cdn.simplecss.org/simple.min.css'
8
+ # link rel: "stylesheet", type: "text/css", href: "/assets/style.css"
9
+ }
10
+ body {
11
+ render_children(**props)
12
+ auto_refresh!
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ export self
2
+
3
+ def connection_pool
4
+ @connection_pool ||= Storage::ConnectionPool.new(@machine, @env[:config][:storage][:path], 4)
5
+ end
6
+
7
+ def schema
8
+ Storage::Schema.new(module_loader: @module_loader, schema_root: '_schema')
9
+ end
10
+
11
+ def migrate!
12
+ schema.apply(connection_pool)
13
+ end
@@ -0,0 +1,9 @@
1
+ export ->(db) {
2
+ # db.execute <<~SQL
3
+ # create table posts (
4
+ # id integer primary key autoincrement,
5
+ # title text,
6
+ # body text
7
+ # )
8
+ # SQL
9
+ }
@@ -0,0 +1,63 @@
1
+ import * as UI from './ui.js';
2
+ import * as Model from './model.js';
3
+ import * as Tools from './tools.js';
4
+
5
+ export async function runPromptExchange(callHandler) {
6
+ const section = UI.createNewSection();
7
+ const prompt = await UI.askPrompt(section);
8
+ UI.addSectionEntryMarkdown(section, `*Prompt*: ${prompt}`);
9
+ const ctx = Model.buildExchangeBody(prompt);
10
+
11
+ while (true) {
12
+ UI.addPendingEntry(section);
13
+ const response = await Model.query(ctx);
14
+ UI.removePendingEntry(section);
15
+ if (!(await processResponse(section, ctx, response, callHandler)))
16
+ break;
17
+ }
18
+ }
19
+
20
+ async function processResponse(section, ctx, response, callHandler) {
21
+ const first_choice = response.choices[0];
22
+ if (first_choice.finish_reason == "tool_calls") {
23
+ Model.addMessage(ctx, first_choice.message);
24
+ const tool_calls = first_choice.message.tool_calls;
25
+ const tool_results = await performToolCalls(section, ctx, tool_calls, callHandler);
26
+ console.log("tool results", tool_results);
27
+ Model.mergeToolResults(ctx, tool_results);
28
+ return true;
29
+ }
30
+ else if (first_choice.finish_reason == "stop") {
31
+ UI.addSectionEntryMarkdown(section, first_choice.message.content);
32
+ return false;
33
+ }
34
+
35
+ }
36
+
37
+ async function performToolCalls(section, ctx, tool_calls, callHandler) {
38
+ const pending = tool_calls.map(async (c) => await runTool(section, ctx, callHandler, c));
39
+ return await Promise.all(pending);
40
+ }
41
+
42
+ async function runTool(section, ctx, callHandler, call) {
43
+ UI.addSectionEntryMarkdown(section, `Running \`${call.function.name}(${call.function.arguments})\``);
44
+ const name = call.function.name;
45
+ const args = JSON.parse(call.function.arguments);
46
+ try {
47
+ const res = await callHandler(name, args);
48
+ return {
49
+ id: call.id,
50
+ result: res
51
+ }
52
+ }
53
+ catch (e) {
54
+ return {
55
+ id: call.id,
56
+ error: e.message
57
+ }
58
+ }
59
+ }
60
+
61
+
62
+
63
+ runPromptExchange(Tools.toolHandler);