ai_sentinel 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fbc0d9833c0562b188f975fa0d32cb8f175bda40149d1e3c3aa0ec64863f4ad
4
- data.tar.gz: 4fb375aa24d1dfa447bf9a52a10f06a03cde84610f75dcdf875cc2aff93b3f97
3
+ metadata.gz: 4ac43b28f3c4ba7b059a19714ed5f094273527d6f8b534c53823ef6e9f1af09a
4
+ data.tar.gz: c35e17935d631d41d171f26078320a4d03ccd72848c8fb5990d98b1c8aa6bb7e
5
5
  SHA512:
6
- metadata.gz: 3516b53aeea1e1175b0d96849a12a3d79ca1ad3c65e91c559f6b746ba2b091f1fd333fe2934abfe14aef82d1da8cc7998a58ca9caba4299332ebd3b226487563
7
- data.tar.gz: 9bf82e503bddd8194bf5b492097d507a95e494a48dc9ea4ca2f650638ca48a161e4c50ad922c95495e66d20cd67fb5fd71c23635372c4c5a063223536d34825e
6
+ metadata.gz: c5a008e3fdc0d2f4e5fbf71c3bb22a02d8b30939fc89b8b29346f8563eaa9440f8f1a1b51a32cf41814998ab47d51cf11f62347cd22d9a120ecab09159bb0395
7
+ data.tar.gz: ac98856532fee5d16c4873978dca30821bf32cf58e027b1c63c947775ad124c102c2f99f9916c9b62e557664a667695fa0b819b2b7278aa901589c33ebd8d104
data/README.md CHANGED
@@ -20,6 +20,7 @@ A lightweight Ruby gem for scheduling AI-driven tasks. Define workflows in a YAM
20
20
  - [Template interpolation](#template-interpolation)
21
21
  - [Conditions](#conditions)
22
22
  - [CLI](#cli)
23
+ - [Daemon mode](#daemon-mode)
23
24
  - [Tool use (AI agent autonomy)](#tool-use-ai-agent-autonomy)
24
25
  - [How tool use works](#how-tool-use-works)
25
26
  - [Available tools](#available-tools)
@@ -106,6 +107,15 @@ bundle exec ai_sentinel version
106
107
 
107
108
  ### 1. Set your API key
108
109
 
110
+ **Option A: In the YAML config** (recommended for headless/embedded systems):
111
+
112
+ ```yaml
113
+ global:
114
+ api_key: sk-ant-...
115
+ ```
116
+
117
+ **Option B: Via environment variable** (recommended for development):
118
+
109
119
  Create a `.env` file in the directory where you'll run AiSentinel:
110
120
 
111
121
  ```bash
@@ -116,6 +126,8 @@ echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
116
126
  echo "OPENAI_API_KEY=sk-..." > .env
117
127
  ```
118
128
 
129
+ The YAML `api_key` takes priority. If not set, AiSentinel falls back to the environment variable for the configured provider (`ANTHROPIC_API_KEY` or `OPENAI_API_KEY`).
130
+
119
131
  ### 2. Generate a config file
120
132
 
121
133
  ```bash
@@ -156,12 +168,13 @@ ai_sentinel validate -c /path/to/my_config.yml
156
168
 
157
169
  ## Configuration
158
170
 
159
- All configuration is done in `ai_sentinel.yml`. API keys are loaded from environment variables (via `.env` file). **Never put API keys in the YAML config.**
171
+ All configuration is done in `ai_sentinel.yml`. API keys can be set in the YAML config or loaded from environment variables (via `.env` file or shell profile). The YAML `api_key` takes priority over the environment variable.
160
172
 
161
173
  ### Global settings
162
174
 
163
175
  ```yaml
164
176
  global:
177
+ api_key: sk-ant-...
165
178
  provider: anthropic
166
179
  model: claude-sonnet-4-20250514
167
180
  database: ./ai_sentinel.sqlite3
@@ -170,6 +183,8 @@ global:
170
183
  compaction_threshold: 40
171
184
  compaction_buffer: 10
172
185
  on_prompt_change: ask
186
+ working_directory: /opt/etc/ai_sentinel
187
+ pid_file: /opt/var/run/ai_sentinel.pid
173
188
  log_file: ./logs/ai_sentinel.log
174
189
  log_file_size: 10485760
175
190
  log_files: 5
@@ -189,6 +204,7 @@ global:
189
204
 
190
205
  | Key | Default | Description |
191
206
  |-----|---------|-------------|
207
+ | `api_key` | `nil` | API key for the configured provider. Takes priority over the environment variable. |
192
208
  | `provider` | `anthropic` | LLM provider (`anthropic` or `openai`) |
193
209
  | `model` | Provider-specific (see below) | Default model for AI steps |
194
210
  | `database` | `~/.ai_sentinel/db.sqlite3` | SQLite database path |
@@ -197,6 +213,8 @@ global:
197
213
  | `compaction_threshold` | `40` | Message count that triggers automatic context compaction |
198
214
  | `compaction_buffer` | `10` | Number of recent messages to keep verbatim after compaction |
199
215
  | `on_prompt_change` | `ask` | Action when a prompt template changes (`ask`, `keep`, `drop`) |
216
+ | `working_directory` | `nil` | Working directory for the process. When set, the process `chdir`s to this directory on both `start` and `run`. Affects where relative file paths resolve (logs, database, files created by AI agents, shell command output). |
217
+ | `pid_file` | `~/.ai_sentinel/ai_sentinel.pid` | Path to the PID file written when running in daemon mode (`-d`). |
200
218
  | `log_file` | `nil` (STDOUT) | Log file path. When omitted, logs go to STDOUT. |
201
219
  | `log_file_size` | `10485760` (10 MB) | Max size per log file before rotation |
202
220
  | `log_files` | `5` | Number of rotated log files to keep |
@@ -218,10 +236,7 @@ AiSentinel supports two providers out of the box. Each has sensible defaults:
218
236
  global:
219
237
  provider: anthropic
220
238
  model: claude-sonnet-4-20250514 # optional, this is the default
221
- ```
222
-
223
- ```bash
224
- echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
239
+ api_key: sk-ant-... # or set ANTHROPIC_API_KEY env var
225
240
  ```
226
241
 
227
242
  #### OpenAI
@@ -230,10 +245,7 @@ echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
230
245
  global:
231
246
  provider: openai
232
247
  model: gpt-4o # optional, this is the default
233
- ```
234
-
235
- ```bash
236
- echo "OPENAI_API_KEY=sk-..." > .env
248
+ api_key: sk-... # or set OPENAI_API_KEY env var
237
249
  ```
238
250
 
239
251
  #### OpenAI-compatible APIs (Ollama, LM Studio, Azure, etc.)
@@ -481,7 +493,8 @@ String values on the right side can be wrapped in double or single quotes. Numer
481
493
 
482
494
  ```
483
495
  ai_sentinel start Start the scheduler
484
- ai_sentinel start -d Start in background mode
496
+ ai_sentinel start -d Start in daemon (background) mode
497
+ ai_sentinel stop Stop a running daemon
485
498
  ai_sentinel run WORKFLOW Manually trigger a workflow
486
499
  ai_sentinel validate Validate config file
487
500
  ai_sentinel list List workflows
@@ -497,9 +510,28 @@ ai_sentinel -v Show version
497
510
 
498
511
  # Use a custom config path (works with any command)
499
512
  ai_sentinel start -c path/to/config.yml
513
+ ai_sentinel stop -c path/to/config.yml
500
514
  ai_sentinel run my_workflow -c path/to/config.yml
501
515
  ```
502
516
 
517
+ ### Daemon mode
518
+
519
+ Use `-d` to run AiSentinel as a background daemon:
520
+
521
+ ```bash
522
+ ai_sentinel start -d -c /path/to/config.yml
523
+ ```
524
+
525
+ This detaches the process, writes a PID file (see `pid_file` in [Global settings](#global-settings)), and continues running in the background. Stop it with:
526
+
527
+ ```bash
528
+ ai_sentinel stop -c /path/to/config.yml
529
+ ```
530
+
531
+ The `stop` command reads the PID file, sends a `TERM` signal for graceful shutdown, and cleans up. If the PID file is stale (process already dead), it is removed automatically.
532
+
533
+ When `working_directory` is configured, the daemon changes to that directory before starting. This controls where relative file paths resolve -- useful for headless systems where the init system may start the process from an unpredictable directory.
534
+
503
535
  ## Tool use (AI agent autonomy)
504
536
 
505
537
  AiSentinel can give the AI autonomous access to tools, allowing it to perform actions on the local machine as part of responding to a prompt. This is the same mechanism that powers coding assistants like Cursor, Copilot, and opencode -- the AI decides when and how to use tools, and your gem executes them locally.
@@ -8,6 +8,7 @@ module AiSentinel
8
8
  VALID_ACTIONS = %w[http_get http_post ai_prompt shell_command].freeze
9
9
 
10
10
  GLOBAL_CONFIG_MAP = {
11
+ 'api_key' => ->(config, val) { config.api_key = val },
11
12
  'provider' => ->(config, val) { config.provider = val.to_sym },
12
13
  'model' => ->(config, val) { config.model = val },
13
14
  'database' => ->(config, val) { config.database_path = File.expand_path(val) },
@@ -21,7 +22,8 @@ module AiSentinel
21
22
  'on_prompt_change' => ->(config, val) { config.on_prompt_change = val.to_sym },
22
23
  'tool_safety' => ->(config, val) { config.tool_safety = val.transform_keys(&:to_sym) },
23
24
  'max_tool_rounds' => ->(config, val) { config.max_tool_rounds = val },
24
- 'pid_file' => ->(config, val) { config.pid_file = File.expand_path(val) }
25
+ 'pid_file' => ->(config, val) { config.pid_file = File.expand_path(val) },
26
+ 'working_directory' => ->(config, val) { config.working_directory = File.expand_path(val) }
25
27
  }.freeze
26
28
 
27
29
  attr_reader :config_path, :raw_config
@@ -31,7 +31,8 @@ module AiSentinel
31
31
  attr_accessor :provider, :api_key, :database_path, :max_context_messages,
32
32
  :compaction_threshold, :compaction_buffer, :log_file,
33
33
  :log_file_size, :log_files, :on_prompt_change,
34
- :tool_safety, :max_tool_rounds, :pid_file
34
+ :tool_safety, :max_tool_rounds, :pid_file,
35
+ :working_directory
35
36
  attr_writer :model, :base_url, :logger
36
37
 
37
38
  def initialize
@@ -51,6 +52,7 @@ module AiSentinel
51
52
  @tool_safety = nil
52
53
  @max_tool_rounds = DEFAULT_MAX_TOOL_ROUNDS
53
54
  @pid_file = File.join(Dir.home, '.ai_sentinel', 'ai_sentinel.pid')
55
+ @working_directory = nil
54
56
  end
55
57
 
56
58
  def logger
@@ -13,6 +13,8 @@ module AiSentinel
13
13
  end
14
14
 
15
15
  def start(daemonize: false)
16
+ apply_working_directory
17
+
16
18
  if daemonize
17
19
  Process.daemon(true, true)
18
20
  write_pid_file
@@ -36,6 +38,8 @@ module AiSentinel
36
38
  end
37
39
 
38
40
  def trigger(workflow_name)
41
+ apply_working_directory
42
+
39
43
  workflow = registry[workflow_name.to_s] || registry[workflow_name.to_sym]
40
44
  raise Error, "Unknown workflow: #{workflow_name}" unless workflow
41
45
 
@@ -49,6 +53,14 @@ module AiSentinel
49
53
 
50
54
  private
51
55
 
56
+ def apply_working_directory
57
+ dir = configuration.working_directory
58
+ return unless dir
59
+
60
+ FileUtils.mkdir_p(dir)
61
+ Dir.chdir(dir)
62
+ end
63
+
52
64
  def write_pid_file
53
65
  FileUtils.mkdir_p(File.dirname(pid_file))
54
66
  File.write(pid_file, Process.pid.to_s)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AiSentinel
4
- VERSION = '0.1.2'
4
+ VERSION = '0.2.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ai_sentinel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Celi