ask-app-server 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +15 -0
- data/LICENSE +21 -0
- data/README.md +108 -0
- data/bin/ask-app-server +6 -0
- data/lib/ask/app_server/agent_adapter.rb +206 -0
- data/lib/ask/app_server/cli.rb +173 -0
- data/lib/ask/app_server/config.rb +258 -0
- data/lib/ask/app_server/event_translator.rb +194 -0
- data/lib/ask/app_server/permission_handler.rb +167 -0
- data/lib/ask/app_server/server.rb +384 -0
- data/lib/ask/app_server/session_manager.rb +237 -0
- data/lib/ask/app_server/session_store.rb +138 -0
- data/lib/ask/app_server/version.rb +7 -0
- data/lib/ask/app_server.rb +13 -0
- data/lib/ask-app-server.rb +15 -0
- metadata +201 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 77f123a7ad41bee740dc6678faa847ee174fc688ac09cab76b43c3ea2037da71
|
|
4
|
+
data.tar.gz: 1afcf87beb1af260dfc6042305e10d00d06b546c2acb464fe89e757732fccc31
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c7179daf83e380074cc04d7584dbaf4043d2c7cfff3df6e49c23a6fdc68bf34fc2b421b49866056a162702eb66d6054ecd437d63365f34dfad5a1cbb0968ce78
|
|
7
|
+
data.tar.gz: ad552bb79034f61d6b06a519cb041f711db6eb0f34c67b8e069cfc5c973782dcb47ab21d226f5726f78ee58992b3dbdc65322fbe59e0585cb73346ccda227eb8
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.0] - 2026-07-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Initial release of `ask-app-server` — JSON-RPC/stdio app-server for ask-rb agents.
|
|
8
|
+
- **Protocol handler** — implements the ZCode/Codex app-server JSON-RPC protocol over stdio.
|
|
9
|
+
- **Agent adapter** — wraps `Ask::Agent::Session` behind the app-server protocol.
|
|
10
|
+
- **Event translator** — converts ask-agent event types to app-server protocol event types.
|
|
11
|
+
- **Session manager** — create, list, resume, subscribe, and poll sessions.
|
|
12
|
+
- **CLI binary** — `ask-app-server` command (stdio mode).
|
|
13
|
+
- **Mid-execution injection** — abort running turns and send new messages.
|
|
14
|
+
- **Subscription streaming** — push `session/event` notifications to subscribed clients.
|
|
15
|
+
- **Event polling** — retrieve events by sequence number via `session/events`.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kaka Ruto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# ask-app-server
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/rb/ask-app-server)
|
|
4
|
+
|
|
5
|
+
**JSON-RPC/stdio app-server for ask-rb agents.** Exposes `Ask::Agent::Session` behind the standard app-server protocol, the same protocol spoken by ZCode and Codex app-servers.
|
|
6
|
+
|
|
7
|
+
## What is this?
|
|
8
|
+
|
|
9
|
+
`ask-app-server` turns an ask-rb agent into a **programmable service** that speaks JSON-RPC over stdio. Any client that can speak the app-server protocol can drive your agent:
|
|
10
|
+
|
|
11
|
+
- **Telegram bots**: the `zcode-telegram-bot` connects to ask-app-server instead of ZCode
|
|
12
|
+
- **AI SDK providers**: the Vercel AI SDK provider works with ask-app-server unchanged
|
|
13
|
+
- **IDE extensions**: VS Code, Cursor, and JetBrains extensions connect over stdio/socket
|
|
14
|
+
- **Headless automation**: CI/CD pipelines, batch processing, scriptable agent tasks
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
gem install ask-app-server
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or add to your Gemfile:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
gem "ask-app-server"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Start the server (reads JSON-RPC from stdin, writes to stdout)
|
|
32
|
+
ask-app-server
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
From another process, send JSON-RPC requests:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{"id":1, "method":"session/create", "params":{"workspace":{"workspacePath":"."}}}
|
|
39
|
+
{"id":2, "method":"session/send", "params":{"sessionId":"...", "content":"List files in this directory"}}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Protocol
|
|
43
|
+
|
|
44
|
+
### Methods
|
|
45
|
+
|
|
46
|
+
| Method | Description |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `initialize` | Handshake, returns server capabilities |
|
|
49
|
+
| `session/create` | Create a new agent session |
|
|
50
|
+
| `session/list` | List active sessions |
|
|
51
|
+
| `session/resume` | Resume an existing session |
|
|
52
|
+
| `session/subscribe` | Subscribe to streaming events |
|
|
53
|
+
| `session/send` | Send a message to a session |
|
|
54
|
+
| `session/events` | Poll for events after a sequence number |
|
|
55
|
+
| `session/abort` | Abort the current turn |
|
|
56
|
+
| `workspace/readState` | Read model and workspace settings |
|
|
57
|
+
|
|
58
|
+
### Events (server → client notifications)
|
|
59
|
+
|
|
60
|
+
| Event | When |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `turn.started` | A new turn begins processing |
|
|
63
|
+
| `model.streaming` | Text delta from the model |
|
|
64
|
+
| `tool.updated` | Tool execution started/updated/completed/failed |
|
|
65
|
+
| `turn.completed` | Turn finished successfully |
|
|
66
|
+
| `turn.failed` | Turn ended with an error |
|
|
67
|
+
|
|
68
|
+
Event payloads are delivered as `session/event` notifications on subscribed sessions. The server also sends `interaction/requestPermission` when a blocked tool needs approval and `interaction/requestUserInput` when it needs input from the user.
|
|
69
|
+
|
|
70
|
+
## Clients
|
|
71
|
+
|
|
72
|
+
This server is a drop-in replacement for `zcode app-server`. The following clients work without changes:
|
|
73
|
+
|
|
74
|
+
- [ask-coding-providers](https://github.com/ask-rb/ask-coding-providers): ZCode adapter (set `ZCODE_CLI_PATH` to the `ask-app-server` binary)
|
|
75
|
+
- [zcode-telegram-bot](https://github.com/ask-rb/zcode-telegram-bot): Python Telegram bot
|
|
76
|
+
- [ai-sdk-provider-codex-app-server](https://github.com/pablof7z/ai-sdk-provider-codex-app-server): Vercel AI SDK provider
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
Flags: `--version`, `--help`, `--config PATH`.
|
|
81
|
+
|
|
82
|
+
The config file is searched in order: `ASK_APP_SERVER_CONFIG` env var, then `./.ask-app-server.json`, then `~/.ask-app-server/config.json`.
|
|
83
|
+
|
|
84
|
+
Environment variables:
|
|
85
|
+
|
|
86
|
+
| Variable | Default | Description |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `ASK_APP_SERVER_CONFIG` | auto-detected | Path to the config file |
|
|
89
|
+
| `ASK_APP_SERVER_MODEL` | `opencode_go/deepseek-v4-flash` | Model identifier (overrides config file) |
|
|
90
|
+
| `ASK_APP_SERVER_PERMISSIONS` | `on_request` | Permission mode (`on_request`, `never`) |
|
|
91
|
+
| `DEBUG` | unset | Set to `1` for debug logging |
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
git clone https://github.com/ask-rb/ask-app-server.git
|
|
97
|
+
cd ask-app-server
|
|
98
|
+
bundle install
|
|
99
|
+
bundle exec rake test
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Full documentation
|
|
103
|
+
|
|
104
|
+
The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs. The [Gem reference](https://ask-rb.github.io/ask-docs/reference/gems) (Agent Infrastructure section) covers ask-app-server. API reference: https://ask-rb.github.io/ask-docs/reference/api.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
data/bin/ask-app-server
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module AppServer
|
|
5
|
+
# Wraps an Ask::Agent::Session and translates its events into the
|
|
6
|
+
# app-server protocol event format via EventTranslator.
|
|
7
|
+
#
|
|
8
|
+
# Each wrapper is associated with one session and maintains an
|
|
9
|
+
# EventTranslator that app-server clients poll or subscribe to.
|
|
10
|
+
class AgentAdapter
|
|
11
|
+
# The underlying ask-agent session.
|
|
12
|
+
attr_reader :session
|
|
13
|
+
|
|
14
|
+
# The event translator that accumulates protocol events.
|
|
15
|
+
attr_reader :translator
|
|
16
|
+
|
|
17
|
+
# The session ID (same as ask-agent session id).
|
|
18
|
+
attr_reader :session_id
|
|
19
|
+
|
|
20
|
+
# Whether a turn is currently in progress.
|
|
21
|
+
attr_reader :running
|
|
22
|
+
|
|
23
|
+
# When the session was created.
|
|
24
|
+
attr_reader :created_at
|
|
25
|
+
|
|
26
|
+
def initialize(model:, tools: nil, system_prompt: nil, agent_dir: nil, **session_opts)
|
|
27
|
+
@model = model
|
|
28
|
+
@system_prompt = system_prompt
|
|
29
|
+
@tools = resolve_tools(tools)
|
|
30
|
+
@session_opts = session_opts
|
|
31
|
+
@agent_dir = agent_dir
|
|
32
|
+
@session = nil
|
|
33
|
+
@translator = nil
|
|
34
|
+
@session_id = nil
|
|
35
|
+
@running = false
|
|
36
|
+
@running_mutex = Mutex.new
|
|
37
|
+
@run_thread = nil
|
|
38
|
+
@abort_requested = false
|
|
39
|
+
@created_at = Time.now
|
|
40
|
+
@logger = Logger.new($stdout, level: ENV["DEBUG"] ? Logger::DEBUG : Logger::WARN)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Start a new ask-agent session.
|
|
44
|
+
# Returns the session ID.
|
|
45
|
+
def start_session
|
|
46
|
+
opts = @session_opts.dup
|
|
47
|
+
|
|
48
|
+
# Extract hooks from session opts and pass them to Session
|
|
49
|
+
hooks = opts.delete(:hooks) || {}
|
|
50
|
+
|
|
51
|
+
@session = Ask::Agent::Session.new(
|
|
52
|
+
model: @model,
|
|
53
|
+
tools: @tools,
|
|
54
|
+
system_prompt: @system_prompt,
|
|
55
|
+
agent_dir: @agent_dir,
|
|
56
|
+
hooks: hooks,
|
|
57
|
+
**opts
|
|
58
|
+
)
|
|
59
|
+
@session_id = @session.id
|
|
60
|
+
@translator = EventTranslator.new(@session_id)
|
|
61
|
+
@session.on_event { |event| handle_agent_event(event) }
|
|
62
|
+
@session_id
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Resume an existing session (re-attach event handler).
|
|
66
|
+
def resume(session)
|
|
67
|
+
@session = session
|
|
68
|
+
@session_id = session.id
|
|
69
|
+
@translator = EventTranslator.new(@session_id)
|
|
70
|
+
@session.on_event { |event| handle_agent_event(event) }
|
|
71
|
+
@session_id
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Send a message and start processing. Runs in a background thread.
|
|
75
|
+
# The caller should poll or subscribe to receive events.
|
|
76
|
+
def send_message(content)
|
|
77
|
+
raise "Session not started" unless @session
|
|
78
|
+
raise "Session already busy" if @running
|
|
79
|
+
|
|
80
|
+
@running_mutex.synchronize do
|
|
81
|
+
@abort_requested = false
|
|
82
|
+
@running = true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
@run_thread = Thread.new do
|
|
86
|
+
begin
|
|
87
|
+
@session.run(content)
|
|
88
|
+
rescue => e
|
|
89
|
+
# Agent may have been aborted — that's fine
|
|
90
|
+
@logger.debug("Agent run error: #{e.message}") if ENV["DEBUG"]
|
|
91
|
+
ensure
|
|
92
|
+
@running_mutex.synchronize { @running = false }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Request abort of the current turn.
|
|
100
|
+
def abort_turn!
|
|
101
|
+
@abort_requested = true
|
|
102
|
+
@session&.abort if @session
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Wait for the current turn to complete (with timeout).
|
|
106
|
+
# Returns true if completed, false if timed out.
|
|
107
|
+
def wait_for_turn(timeout: 600)
|
|
108
|
+
thread = @run_thread
|
|
109
|
+
return true unless thread
|
|
110
|
+
|
|
111
|
+
thread.join(timeout)
|
|
112
|
+
!thread.alive?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Whether this session is idle (no turn running).
|
|
116
|
+
def idle?
|
|
117
|
+
!@running
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Inject a message into a running session (mid-execution).
|
|
121
|
+
# Aborts the current turn; the message will be processed when
|
|
122
|
+
# the next turn starts.
|
|
123
|
+
# Note: ask-agent doesn't natively support mid-execution injection.
|
|
124
|
+
# We abort and queue the message for the next run.
|
|
125
|
+
def inject_message(content)
|
|
126
|
+
if @running
|
|
127
|
+
abort_turn!
|
|
128
|
+
# The caller should wait for idle, then call send_message again
|
|
129
|
+
false
|
|
130
|
+
else
|
|
131
|
+
send_message(content)
|
|
132
|
+
true
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The accumulated streaming text from the current/ last turn.
|
|
137
|
+
def streaming_text
|
|
138
|
+
@translator&.instance_variable_get(:@streaming_text).to_s
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# All events since last drain.
|
|
142
|
+
def pending_events
|
|
143
|
+
@translator&.pending_events || []
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Drain and return pending events.
|
|
147
|
+
def drain_events
|
|
148
|
+
@translator&.drain_events || []
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Last sequence number.
|
|
152
|
+
def last_seq
|
|
153
|
+
@translator&.last_seq || 0
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Events after a given sequence number.
|
|
157
|
+
def events_after(after_seq)
|
|
158
|
+
pending_events.select { |e| e[:seq] > after_seq }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
def handle_agent_event(event)
|
|
164
|
+
translated = @translator.translate(event)
|
|
165
|
+
# Translated events are already stored in the translator's buffer.
|
|
166
|
+
# Nothing else to do here — clients poll or subscribe to get them.
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def resolve_tools(tool_list)
|
|
170
|
+
return default_tools if tool_list.nil?
|
|
171
|
+
|
|
172
|
+
tool_list.map do |t|
|
|
173
|
+
case t
|
|
174
|
+
when Class then t.new
|
|
175
|
+
when String then resolve_tool_by_name(t)
|
|
176
|
+
else t
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def resolve_tool_by_name(name)
|
|
182
|
+
case name.downcase
|
|
183
|
+
when "bash" then Ask::Tools::Bash.new
|
|
184
|
+
when "read" then Ask::Tools::Read.new
|
|
185
|
+
when "write" then Ask::Tools::Write.new
|
|
186
|
+
when "edit" then Ask::Tools::Edit.new
|
|
187
|
+
when "glob" then Ask::Tools::Glob.new
|
|
188
|
+
when "grep" then Ask::Tools::Grep.new
|
|
189
|
+
when "code" then Ask::Tools::Code.new
|
|
190
|
+
else raise ArgumentError, "Unknown tool: #{name}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def default_tools
|
|
195
|
+
[
|
|
196
|
+
Ask::Tools::Bash.new,
|
|
197
|
+
Ask::Tools::Read.new,
|
|
198
|
+
Ask::Tools::Write.new,
|
|
199
|
+
Ask::Tools::Edit.new,
|
|
200
|
+
Ask::Tools::Glob.new,
|
|
201
|
+
Ask::Tools::Grep.new
|
|
202
|
+
]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module AppServer
|
|
7
|
+
# CLI for the ask-app-server command.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# ask-app-server # Start in stdio mode (default)
|
|
11
|
+
# ask-app-server --version # Show version
|
|
12
|
+
# ask-app-server --help # Show help
|
|
13
|
+
# ask-app-server --config PATH # Use specific config file
|
|
14
|
+
#
|
|
15
|
+
# Environment variables:
|
|
16
|
+
# ASK_APP_SERVER_CONFIG - Path to config file (default: auto-detect)
|
|
17
|
+
# ASK_APP_SERVER_MODEL - Model to use (overrides config file)
|
|
18
|
+
# ASK_APP_SERVER_PERMISSIONS - Permission mode (overrides config file)
|
|
19
|
+
# DEBUG - Enable debug logging (1/0)
|
|
20
|
+
class CLI
|
|
21
|
+
def self.run!(args = ARGV)
|
|
22
|
+
new.run(args)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run(args)
|
|
26
|
+
case args.first
|
|
27
|
+
when "--version", "-v"
|
|
28
|
+
puts "ask-app-server v#{Ask::AppServer::VERSION}"
|
|
29
|
+
return
|
|
30
|
+
when "--help", "-h"
|
|
31
|
+
show_help
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Parse --config from args
|
|
36
|
+
config_path = nil
|
|
37
|
+
remaining_args = []
|
|
38
|
+
args.each_with_index do |arg, i|
|
|
39
|
+
if arg == "--config" && i + 1 < args.length
|
|
40
|
+
config_path = args[i + 1]
|
|
41
|
+
elsif !arg.start_with?("--config")
|
|
42
|
+
remaining_args << arg
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Load configuration
|
|
47
|
+
config = Config.new(config_path: config_path)
|
|
48
|
+
|
|
49
|
+
# Register custom models from config into the model catalog
|
|
50
|
+
config.register_models!
|
|
51
|
+
|
|
52
|
+
$stdout.sync = true
|
|
53
|
+
$stderr.sync = true
|
|
54
|
+
|
|
55
|
+
if config.debug?
|
|
56
|
+
$stderr.puts "[ask-app-server] Starting v#{Ask::AppServer::VERSION}"
|
|
57
|
+
$stderr.puts "[ask-app-server] Config: #{JSON.pretty_generate(config.to_h)}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Build the session manager with config values
|
|
61
|
+
store = build_state_store(config)
|
|
62
|
+
session_manager = SessionManager.new(
|
|
63
|
+
store: store,
|
|
64
|
+
permission_mode: config.permission_mode,
|
|
65
|
+
blocked_tools: config.blocked_tools,
|
|
66
|
+
permission_timeout: config.permission_timeout
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Start the server
|
|
70
|
+
server = Server.new(session_manager: session_manager)
|
|
71
|
+
|
|
72
|
+
# Wire the server as the protocol sender for permission requests.
|
|
73
|
+
# Every time a new session is created with a PermissionHandler,
|
|
74
|
+
# the server registers its outgoing request callback.
|
|
75
|
+
session_manager.on_new_permission_handler do |handler|
|
|
76
|
+
server.register_permission_handler(handler)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
begin
|
|
80
|
+
server.start
|
|
81
|
+
rescue Interrupt
|
|
82
|
+
$stderr.puts "\n[ask-app-server] Shutting down..." if config.debug?
|
|
83
|
+
server.stop
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def build_state_store(config)
|
|
90
|
+
# Default path: ~/.ask-app-server/state.db
|
|
91
|
+
sqlite_path = config.state_sqlite_path || File.expand_path("~/.ask-app-server/state.db")
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
require "sqlite3"
|
|
95
|
+
require "ask/state/providers/sqlite"
|
|
96
|
+
rescue LoadError
|
|
97
|
+
$stderr.puts "[ask-app-server] sqlite3 gem not available, using in-memory state store." if config.debug?
|
|
98
|
+
return nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
dir = File.dirname(File.expand_path(sqlite_path))
|
|
102
|
+
FileUtils.mkdir_p(dir) unless File.directory?(dir)
|
|
103
|
+
|
|
104
|
+
state = Ask::State::Providers::SQLite.new(path: sqlite_path)
|
|
105
|
+
$stderr.puts "[ask-app-server] State store: #{sqlite_path}" if config.debug?
|
|
106
|
+
Ask::AppServer::SessionStore.new(state: state)
|
|
107
|
+
rescue => e
|
|
108
|
+
$stderr.puts "[ask-app-server] Warning: Could not initialize SQLite state: #{e.message}"
|
|
109
|
+
$stderr.puts "[ask-app-server] Falling back to in-memory state store."
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def show_help
|
|
114
|
+
puts <<~HELP
|
|
115
|
+
ask-app-server v#{Ask::AppServer::VERSION}
|
|
116
|
+
|
|
117
|
+
JSON-RPC/stdio app-server for ask-rb agents.
|
|
118
|
+
Drop-in compatible with the ZCode/Codex app-server protocol.
|
|
119
|
+
|
|
120
|
+
USAGE:
|
|
121
|
+
ask-app-server Start in stdio mode
|
|
122
|
+
|
|
123
|
+
OPTIONS:
|
|
124
|
+
--version, -v Show version
|
|
125
|
+
--help, -h Show this help message
|
|
126
|
+
--config PATH Config file path (default: auto-detect)
|
|
127
|
+
|
|
128
|
+
CONFIG FILE (JSON):
|
|
129
|
+
Search order:
|
|
130
|
+
1. ASK_APP_SERVER_CONFIG env
|
|
131
|
+
2. ./.ask-app-server.json
|
|
132
|
+
3. ~/.ask-app-server/config.json
|
|
133
|
+
|
|
134
|
+
Example ~/.ask-app-server/config.json:
|
|
135
|
+
{
|
|
136
|
+
"model": "claude-sonnet-4",
|
|
137
|
+
"tools": ["bash", "read", "write", "edit", "glob", "grep"],
|
|
138
|
+
"permissions": {
|
|
139
|
+
"mode": "on_request",
|
|
140
|
+
"blocked_tools": ["write", "edit", "bash", "destroy"],
|
|
141
|
+
"timeout": 300
|
|
142
|
+
},
|
|
143
|
+
"system_prompt": "You are a coding assistant...",
|
|
144
|
+
"session": { "timeout": 600 }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
ENVIRONMENT:
|
|
148
|
+
ASK_APP_SERVER_CONFIG Config file path
|
|
149
|
+
ASK_APP_SERVER_MODEL Model identifier (overrides config)
|
|
150
|
+
ASK_APP_SERVER_PERMISSIONS Permission mode (overrides config)
|
|
151
|
+
DEBUG Set to 1 for debug logging
|
|
152
|
+
|
|
153
|
+
PROTOCOL:
|
|
154
|
+
Speaks the standard app-server JSON-RPC protocol over stdio.
|
|
155
|
+
Compatible clients:
|
|
156
|
+
- ask-coding-providers ZCode adapter
|
|
157
|
+
- ai-sdk-provider-codex-app-server (Vercel AI SDK)
|
|
158
|
+
- zcode-telegram-bot (Python Telegram bot)
|
|
159
|
+
|
|
160
|
+
EXAMPLE:
|
|
161
|
+
# Start with defaults
|
|
162
|
+
ask-app-server
|
|
163
|
+
|
|
164
|
+
# Start with a specific config
|
|
165
|
+
ask-app-server --config /path/to/config.json
|
|
166
|
+
|
|
167
|
+
# Send a ping from another shell:
|
|
168
|
+
echo '{"id":1,"method":"ping"}' | ask-app-server
|
|
169
|
+
HELP
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|