ask-coding-providers 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.
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ask
6
+ module CodingProviders
7
+ module Codex
8
+ # Queries Codex's SQLite database for thread/project data.
9
+ #
10
+ # Codex stores sessions in `~/.codex/state_5.sqlite` (threads table)
11
+ # and full message history in JSONL rollout files.
12
+ class CodexDB
13
+ DEFAULT_DB = File.expand_path("~/.codex/state_5.sqlite")
14
+
15
+ def initialize(db_path = nil)
16
+ @db_path = db_path || DEFAULT_DB
17
+ end
18
+
19
+ def available?
20
+ File.exist?(@db_path)
21
+ end
22
+
23
+ # List all projects (directories with threads), ordered by most recent.
24
+ # Returns [{project_id:, directory:, session_count:}] or [].
25
+ def list_projects
26
+ return [] unless available?
27
+
28
+ db = open_db
29
+ rows = db.execute(<<~SQL)
30
+ SELECT cwd AS directory,
31
+ COUNT(*) AS session_count,
32
+ MAX(updated_at) AS last_active
33
+ FROM threads
34
+ WHERE archived = 0
35
+ GROUP BY cwd
36
+ ORDER BY last_active DESC
37
+ LIMIT 20
38
+ SQL
39
+ rows.map do |r|
40
+ {
41
+ project_id: r["directory"],
42
+ directory: r["directory"],
43
+ session_count: r["session_count"].to_i
44
+ }
45
+ end
46
+ rescue => e
47
+ []
48
+ ensure
49
+ db&.close
50
+ end
51
+
52
+ # Find sessions in a given directory.
53
+ # Returns [{session_id:, title:, updated:}] or [].
54
+ def find_sessions(directory:, limit: 20)
55
+ return [] unless available?
56
+
57
+ db = open_db
58
+ rows = db.execute(<<~SQL, [directory, directory])
59
+ SELECT id, title, updated_at, preview
60
+ FROM threads
61
+ WHERE (cwd = ? OR ? LIKE cwd || '/%')
62
+ AND archived = 0
63
+ ORDER BY updated_at DESC
64
+ LIMIT ?
65
+ SQL
66
+ rows.map do |r|
67
+ {
68
+ session_id: r["id"],
69
+ title: r["title"].to_s.empty? ? (r["preview"] || "(untitled)") : r["title"],
70
+ updated: r["updated_at"]
71
+ }
72
+ end
73
+ rescue => e
74
+ []
75
+ ensure
76
+ db&.close
77
+ end
78
+
79
+ # Find the most recent session across all projects.
80
+ # Returns {session_id:, directory:} or nil.
81
+ def find_recent_session
82
+ return nil unless available?
83
+
84
+ db = open_db
85
+ row = db.get_first_row(<<~SQL)
86
+ SELECT id, cwd FROM threads
87
+ WHERE archived = 0
88
+ ORDER BY updated_at DESC LIMIT 1
89
+ SQL
90
+ row ? { session_id: row["id"], directory: row["cwd"] } : nil
91
+ rescue => e
92
+ nil
93
+ ensure
94
+ db&.close
95
+ end
96
+
97
+ # Find the most recent TUI session in a workspace.
98
+ def find_recent_tui_session(workspace_path)
99
+ return nil unless available?
100
+
101
+ db = open_db
102
+ row = db.get_first_row(<<~SQL, workspace_path, workspace_path)
103
+ SELECT id, title, cwd FROM threads
104
+ WHERE (cwd = ? OR ? LIKE cwd || '/%')
105
+ AND archived = 0
106
+ ORDER BY updated_at DESC LIMIT 1
107
+ SQL
108
+ return nil unless row
109
+ {
110
+ session_id: row["id"],
111
+ title: row["title"],
112
+ directory: row["cwd"]
113
+ }
114
+ rescue => e
115
+ nil
116
+ ensure
117
+ db&.close
118
+ end
119
+
120
+ # Look up a session's workspace directory by ID.
121
+ def session_directory(session_id)
122
+ return nil unless available?
123
+
124
+ db = open_db
125
+ row = db.get_first_row("SELECT cwd FROM threads WHERE id = ?", [session_id])
126
+ row&.dig("cwd")
127
+ rescue => e
128
+ nil
129
+ ensure
130
+ db&.close
131
+ end
132
+
133
+ # Get session message history from the rollout JSONL file.
134
+ # Returns [{text:, role:, origin:}] or [].
135
+ def session_history(session_id, limit: 100)
136
+ return [] unless available?
137
+
138
+ db = open_db
139
+ row = db.get_first_row("SELECT rollout_path, title, preview FROM threads WHERE id = ?", [session_id])
140
+ return [] unless row
141
+
142
+ rollout_path = row["rollout_path"]
143
+ return [] unless rollout_path && File.exist?(rollout_path)
144
+
145
+ parse_rollout(rollout_path, limit)
146
+ rescue => e
147
+ []
148
+ ensure
149
+ db&.close
150
+ end
151
+
152
+ # List recent sessions across all projects.
153
+ # Returns [{session_id:, title:, updated:, msg_count:}] or [].
154
+ def recent_sessions
155
+ return [] unless available?
156
+
157
+ db = open_db
158
+ rows = db.execute(<<~SQL)
159
+ SELECT id, title, updated_at, preview
160
+ FROM threads
161
+ WHERE archived = 0
162
+ ORDER BY updated_at DESC
163
+ LIMIT 50
164
+ SQL
165
+ rows.map do |r|
166
+ {
167
+ session_id: r["id"],
168
+ title: r["title"].to_s.empty? ? (r["preview"] || "(untitled)") : r["title"],
169
+ updated: r["updated_at"],
170
+ msg_count: 0
171
+ }
172
+ end
173
+ rescue => e
174
+ []
175
+ ensure
176
+ db&.close
177
+ end
178
+
179
+ private
180
+
181
+ def open_db
182
+ require "sqlite3"
183
+ db = SQLite3::Database.new(@db_path, readonly: true)
184
+ db.results_as_hash = true
185
+ db
186
+ end
187
+
188
+ def parse_rollout(path, limit)
189
+ messages = []
190
+ File.readlines(path).each do |line|
191
+ begin
192
+ event = JSON.parse(line)
193
+ rescue JSON::ParserError
194
+ next
195
+ end
196
+
197
+ case event["type"]
198
+ when "user_message"
199
+ messages << { text: event["content"].to_s, role: "You", origin: "real_user" }
200
+ when "agent_message"
201
+ messages << { text: event["content"].to_s, role: "Agent", origin: nil }
202
+ end
203
+ break if messages.length >= limit * 2
204
+ end
205
+ messages.last(limit)
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "codex/app_server_client"
4
+ require_relative "codex/adapter"
5
+ require_relative "codex/codex_db"
6
+
7
+ module Ask
8
+ module CodingProviders
9
+ module Codex
10
+ class Error < Ask::CodingProviders::Error; end
11
+ class TimeoutError < Ask::CodingProviders::TimeoutError; end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module CodingProviders
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ask
6
+ module CodingProviders
7
+ module ZCode
8
+ # Wraps the ZCode SQLite database, centralizing all session/project queries.
9
+ #
10
+ # Usage:
11
+ # db = ZCodeDB.new
12
+ # projects = db.list_projects
13
+ # sessions = db.find_sessions(directory: "/path")
14
+ #
15
+ # All methods return nil or empty arrays on error (never raise).
16
+ # This makes it safe to use from any context without wrapping every call.
17
+ class ZCodeDB
18
+ def initialize(db_path = nil)
19
+ @db_path = db_path || File.expand_path("~/.zcode/cli/db/db.sqlite")
20
+ end
21
+
22
+ def available?
23
+ File.exist?(@db_path)
24
+ end
25
+
26
+ # List all projects with session counts, ordered by most recently updated.
27
+ # Returns [{project_id:, directory:, session_count:}] or [].
28
+ def list_projects
29
+ return [] unless available?
30
+
31
+ db = open_db
32
+ rows = db.execute(<<~SQL)
33
+ SELECT project_id, directory, count(*) as session_count
34
+ FROM session
35
+ WHERE task_type = 'interactive' AND time_archived IS NULL
36
+ GROUP BY project_id
37
+ ORDER BY MAX(time_updated) DESC
38
+ LIMIT 10
39
+ SQL
40
+ rows.map { |r| { project_id: r["project_id"], directory: r["directory"], session_count: r["session_count"].to_i } }
41
+ rescue => e
42
+ nil
43
+ ensure
44
+ db&.close
45
+ end
46
+
47
+ # Find sessions in a given directory (exact or prefix match).
48
+ # Returns [{session_id:, title:, updated:}] or [].
49
+ def find_sessions(directory:, limit: 20)
50
+ return [] unless available?
51
+
52
+ db = open_db
53
+ rows = db.execute(<<~SQL, [directory, directory, limit])
54
+ SELECT id, title, time_updated
55
+ FROM session
56
+ WHERE (directory = ? OR ? LIKE directory || '/%')
57
+ AND time_archived IS NULL AND task_type = 'interactive'
58
+ ORDER BY time_updated DESC
59
+ LIMIT ?
60
+ SQL
61
+ rows.map { |r| { session_id: r["id"], title: r["title"], updated: r["time_updated"] } }
62
+ rescue => e
63
+ []
64
+ ensure
65
+ db&.close
66
+ end
67
+
68
+ # Find the single most recent session across all projects.
69
+ # Returns {session_id:, directory:} or nil.
70
+ def find_recent_session
71
+ return nil unless available?
72
+
73
+ db = open_db
74
+ row = db.get_first_row(<<~SQL)
75
+ SELECT id, directory FROM session
76
+ WHERE time_archived IS NULL AND task_type = 'interactive'
77
+ ORDER BY time_updated DESC LIMIT 1
78
+ SQL
79
+ row ? { session_id: row["id"], directory: row["directory"] } : nil
80
+ rescue => e
81
+ nil
82
+ ensure
83
+ db&.close
84
+ end
85
+
86
+ # Find the most recent TUI session in a workspace path.
87
+ # Returns {session_id:, title:, directory:} or nil.
88
+ def find_recent_tui_session(workspace_path)
89
+ return nil unless available?
90
+
91
+ db = open_db
92
+ row = db.get_first_row(<<~SQL, [workspace_path, workspace_path])
93
+ SELECT id, title, directory
94
+ FROM session
95
+ WHERE (directory = ? OR ? LIKE directory || '/%')
96
+ AND time_archived IS NULL AND task_type = 'interactive'
97
+ ORDER BY time_updated DESC
98
+ LIMIT 1
99
+ SQL
100
+ row ? { session_id: row["id"], title: row["title"], directory: row["directory"] } : nil
101
+ rescue => e
102
+ nil
103
+ ensure
104
+ db&.close
105
+ end
106
+
107
+ # Look up a session's workspace directory by its ID.
108
+ # Returns the directory string, or nil.
109
+ def session_directory(session_id)
110
+ return nil unless available?
111
+
112
+ db = open_db
113
+ row = db.get_first_row("SELECT directory FROM session WHERE id = ?", [session_id])
114
+ row&.dig("directory")
115
+ rescue => e
116
+ nil
117
+ ensure
118
+ db&.close
119
+ end
120
+
121
+ # Load the last N text parts from a session, newest first.
122
+ # Returns [{text:, role:, origin:}] or [].
123
+ def session_history(session_id, limit: 100)
124
+ return [] unless available?
125
+
126
+ db = open_db
127
+ rows = db.execute(<<~SQL, [session_id, limit])
128
+ SELECT p.data, m.data as msg_data FROM part p
129
+ JOIN message m ON p.message_id = m.id
130
+ WHERE p.session_id = ? AND p.data LIKE '%"type":"text"%'
131
+ ORDER BY p.time_created DESC
132
+ LIMIT ?
133
+ SQL
134
+
135
+ rows.filter_map do |r|
136
+ part = JSON.parse(r["data"]) rescue next
137
+ content = part["text"] || ""
138
+ next if content.empty?
139
+
140
+ msg_data = JSON.parse(r["msg_data"]) rescue {}
141
+ role = msg_data["role"] == "user" ? "You" : "Agent"
142
+ origin = msg_data.dig("semantics", "origin") rescue nil
143
+
144
+ # Skip system-generated messages masquerading as user
145
+ next if role == "You" && origin != "real_user" && origin != nil
146
+
147
+ { text: content, role: role, origin: origin }
148
+ end
149
+ rescue => e
150
+ []
151
+ ensure
152
+ db&.close
153
+ end
154
+
155
+ # List recent sessions across all projects.
156
+ # Returns [{session_id:, title:, updated:, msg_count:}] or [].
157
+ def recent_sessions
158
+ return [] unless available?
159
+
160
+ db = open_db
161
+ rows = db.execute(<<~SQL)
162
+ SELECT s.id AS session_id, s.title AS title,
163
+ s.time_updated AS updated,
164
+ (SELECT count(*) FROM message m WHERE m.session_id = s.id) AS msg_count
165
+ FROM session s
166
+ WHERE s.task_type = 'interactive' AND s.time_archived IS NULL
167
+ ORDER BY s.time_updated DESC
168
+ LIMIT 20
169
+ SQL
170
+ rows.map do |r|
171
+ {
172
+ session_id: r["session_id"],
173
+ title: r["title"],
174
+ updated: r["updated"],
175
+ msg_count: r["msg_count"].to_i
176
+ }
177
+ end
178
+ rescue => e
179
+ []
180
+ ensure
181
+ db&.close
182
+ end
183
+
184
+ private
185
+
186
+ def open_db
187
+ require "sqlite3"
188
+ db = SQLite3::Database.new(@db_path, readonly: true)
189
+ db.results_as_hash = true
190
+ db
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "zcode/zcode_db"
4
+
5
+ module Ask
6
+ module CodingProviders
7
+ # ZCode database reader — queries ZCode's SQLite session store.
8
+ # See {ZCodeDB} for session/project lookup methods.
9
+ module ZCode
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "coding_providers/version"
4
+ require_relative "coding_providers/adapter"
5
+
6
+ module Ask
7
+ module CodingProviders
8
+ class Error < StandardError; end
9
+ class ConfigurationError < Error; end
10
+ class ConnectionError < Error; end
11
+ class TimeoutError < Error; end
12
+
13
+ # Registry of coding provider adapters by name.
14
+ # Adapters register themselves via {register_adapter}.
15
+ # The CLI resolves the active adapter via {build_adapter}.
16
+ #
17
+ # @example
18
+ # adapter = Ask::CodingProviders.build_adapter("acp", workspace_path: Dir.pwd)
19
+ # adapter = Ask::CodingProviders.build_adapter("ask_agent", model: "deepseek-v4-flash")
20
+ @adapter_registry = {}
21
+
22
+ class << self
23
+ # Register a coding adapter class under a name.
24
+ # Called automatically by adapter files when loaded.
25
+ def register_adapter(name, klass)
26
+ @adapter_registry[name.to_s] = klass
27
+ end
28
+
29
+ # Resolve an adapter class by name.
30
+ # @raise [ConfigurationError] if unknown
31
+ def resolve_adapter(name)
32
+ @adapter_registry[name.to_s] || raise(
33
+ ConfigurationError,
34
+ "Unknown coding provider: #{name.inspect}. " \
35
+ "Available: #{@adapter_registry.keys.join(', ')}. " \
36
+ "Set CODING_PROVIDER in your environment."
37
+ )
38
+ end
39
+
40
+ # Build an adapter instance by name, passing config options.
41
+ # Each adapter's .from_config class method decides how to use the config.
42
+ def build_adapter(name, **config)
43
+ resolve_adapter(name).from_config(**config)
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ require_relative "coding_providers/zcode"
50
+ require_relative "coding_providers/ask_agent"
51
+ require_relative "coding_providers/codex"
52
+ require_relative "coding_providers/acp"
53
+ require_relative "coding_providers/claude"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ask/coding_providers"
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ask-coding-providers
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaka Ruto
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: minitest
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '5.25'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '5.25'
26
+ - !ruby/object:Gem::Dependency
27
+ name: mocha
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.1'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: simplecov
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.22'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.22'
68
+ description: Coding agent adapter interface and implementations (ACP, Codex, Claude,
69
+ AskAgent) for the askoda ecosystem.
70
+ email:
71
+ - kaka@myrrlabs.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - lib/ask-coding-providers.rb
77
+ - lib/ask/coding_providers.rb
78
+ - lib/ask/coding_providers/acp.rb
79
+ - lib/ask/coding_providers/acp/adapter.rb
80
+ - lib/ask/coding_providers/adapter.rb
81
+ - lib/ask/coding_providers/ask_agent.rb
82
+ - lib/ask/coding_providers/ask_agent/adapter.rb
83
+ - lib/ask/coding_providers/claude.rb
84
+ - lib/ask/coding_providers/claude/adapter.rb
85
+ - lib/ask/coding_providers/codex.rb
86
+ - lib/ask/coding_providers/codex/adapter.rb
87
+ - lib/ask/coding_providers/codex/app_server_client.rb
88
+ - lib/ask/coding_providers/codex/codex_db.rb
89
+ - lib/ask/coding_providers/version.rb
90
+ - lib/ask/coding_providers/zcode.rb
91
+ - lib/ask/coding_providers/zcode/zcode_db.rb
92
+ homepage: https://github.com/ask-rb/ask-coding-providers
93
+ licenses:
94
+ - MIT
95
+ metadata:
96
+ homepage_uri: https://github.com/ask-rb/ask-coding-providers
97
+ source_code_uri: https://github.com/ask-rb/ask-coding-providers
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '3.2'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubygems_version: 4.0.3
113
+ specification_version: 4
114
+ summary: Coding agent adapters for the ask-rb ecosystem
115
+ test_files: []