1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
use clap::Parser;
use quelch::ai::AiCommands;
use quelch::commands::instance::InstanceKindArg;
use quelch::commands::search::IncludeContentArg;
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "quelch",
version,
about = "Ingest data directly into Azure AI Search"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
/// Config file path
#[arg(short, long, default_value = "quelch.yaml", global = true)]
pub config: PathBuf,
/// Increase verbosity (-v, -vv, -vvv)
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
pub verbose: u8,
/// Suppress TUI, only log errors
#[arg(short, long, global = true)]
pub quiet: bool,
/// Output logs as JSON
#[arg(long, global = true)]
pub json: bool,
/// Disable TUI and fall back to plain structured logs
#[arg(long, global = true)]
pub no_tui: bool,
}
#[derive(clap::Subcommand)]
pub enum Commands {
/// Show sync status for all sources
Status {
/// Filter to cursors belonging to this instance
#[arg(long)]
instance: Option<String>,
/// Emit machine-readable JSON instead of a table
#[arg(long)]
json: bool,
/// Launch the interactive TUI (planned for Phase 10)
#[arg(long)]
tui: bool,
},
/// Reset sync state for a single `(source, subsource)` cursor.
///
/// The instance must already own the cursor — pass `--take-ownership`
/// to rewrite the cursor's owner from another instance to this one.
Reset {
/// Instance that owns (or wants to own) the cursor.
#[arg(long)]
instance: String,
/// Source connection name as defined in `quelch.yaml`.
#[arg(long)]
source: String,
/// Subsource (project key for Jira, space key for Confluence).
#[arg(long)]
subsource: String,
/// Rewrite the cursor's `owner_instance` to this instance even if a
/// different instance currently owns it.
#[arg(long)]
take_ownership: bool,
/// Skip the interactive confirmation prompt.
#[arg(long)]
yes: bool,
},
/// Validate config file without running
Validate,
/// Interactive wizard to scaffold a quelch.yaml config
Init {
/// Folder to write `quelch.yaml` into. Defaults to the current
/// directory; `quelch init .` is the explicit form. The folder is
/// created if it does not already exist.
#[arg(default_value = ".")]
directory: PathBuf,
/// Skip all prompts and write a template directly.
#[arg(long)]
non_interactive: bool,
/// Template name to use in non-interactive mode (minimal, multi-source, distributed).
#[arg(long)]
from_template: Option<String>,
/// Overwrite an existing quelch.yaml without asking.
#[arg(long)]
force: bool,
},
/// Start a local mock Jira and Confluence server for testing
Mock {
/// Port to listen on
#[arg(short, long, default_value = "9999")]
port: u16,
},
/// Structured query against a Cosmos-backed data source
Query {
/// Logical data-source name (e.g. jira_issues)
#[arg(long, value_name = "NAME")]
data_source: String,
/// Structured filter predicate as JSON (e.g. '{"status":"Open"}')
#[arg(long, value_name = "JSON")]
r#where: Option<String>,
/// Read filter JSON from a file instead of --where
#[arg(long, value_name = "PATH")]
where_file: Option<PathBuf>,
/// Sort clause — repeatable, format field:dir (e.g. updated:desc)
#[arg(long, value_name = "FIELD:DIR")]
order_by: Vec<String>,
/// Maximum documents per page
#[arg(long, default_value = "50")]
top: usize,
/// Pagination cursor from a prior response
#[arg(long)]
cursor: Option<String>,
/// Return only the document count
#[arg(long)]
count_only: bool,
/// Include soft-deleted documents
#[arg(long)]
include_deleted: bool,
/// Emit machine-readable JSON
#[arg(long)]
json: bool,
/// MCP instance name; auto-detected when the config declares
/// exactly one MCP instance.
#[arg(long)]
instance: Option<String>,
},
/// Semantic / hybrid search via Azure AI Search
Search {
/// Free-text search query
query: String,
/// Comma-separated logical data-source names to search
#[arg(long, value_name = "NAMES")]
data_sources: Option<String>,
/// Structured filter predicate as JSON
#[arg(long, value_name = "JSON")]
r#where: Option<String>,
/// Maximum hits per page
#[arg(long, default_value = "25")]
top: usize,
/// Pagination cursor from a prior response
#[arg(long)]
cursor: Option<String>,
/// Content level to return
#[arg(long, value_enum, default_value = "snippet")]
include_content: IncludeContentArg,
/// Include soft-deleted documents
#[arg(long)]
include_deleted: bool,
/// Emit machine-readable JSON
#[arg(long)]
json: bool,
/// MCP instance name; auto-detected when the config declares
/// exactly one MCP instance.
#[arg(long)]
instance: Option<String>,
},
/// Fetch a single document by ID from a data source
Get {
/// Document ID
id: String,
/// Logical data-source name (required)
#[arg(long)]
data_source: String,
/// Include soft-deleted documents
#[arg(long)]
include_deleted: bool,
/// Emit machine-readable JSON
#[arg(long)]
json: bool,
/// MCP instance name; auto-detected when the config declares
/// exactly one MCP instance.
#[arg(long)]
instance: Option<String>,
},
/// All-in-one local development mode (sim + ingest + MCP in one process).
///
/// Starts a mock Jira/Confluence server, an in-memory Cosmos backend, an
/// ingest worker, and an embedded MCP server — no cloud accounts needed.
Dev {
/// Use the real Azure AI Search adapter (requires Azure credentials).
#[arg(long)]
use_real_search: bool,
/// Use the Cosmos emulator at https://localhost:8081 instead of in-memory.
#[arg(long)]
use_cosmos_emulator: bool,
/// Port for the embedded MCP server.
#[arg(long, default_value = "8080")]
mcp_port: u16,
/// Seed the fixture data generator (reserved for future use).
#[arg(long)]
seed: Option<u64>,
/// Scale activity rate (reserved for future use).
#[arg(long, default_value = "1.0")]
rate_multiplier: f64,
},
/// Manage AI embedding configuration
Ai {
#[command(subcommand)]
command: Option<AiCommands>,
},
/// Generate an agent or skill bundle for a specific platform
Agent {
#[command(subcommand)]
command: AgentCommands,
},
/// Run the continuous ingest worker for an instance
Ingest {
/// Instance name — which slice of the config this worker owns.
///
/// Optional: if the config declares exactly one ingest instance,
/// this flag is omitted; with multiple ingest instances, it is
/// required to disambiguate.
#[arg(long)]
instance: Option<String>,
/// Run one cycle then exit (useful for debugging and CI).
#[arg(long)]
once: bool,
/// Stop after ingesting N documents (debugging).
#[arg(long)]
max_docs: Option<u64>,
},
/// Azure resource management commands (plan, indexer).
Azure {
#[command(subcommand)]
command: AzureCommands,
},
/// Manage named instances declared in `quelch.yaml`.
///
/// Use these subcommands to inspect the instances declared in the master
/// config and to emit per-instance config slices ready to copy onto the
/// host that runs Q-Ingest or Q-MCP.
Instance {
#[command(subcommand)]
command: InstanceCommand,
},
/// Start the MCP HTTP server for an instance.
///
/// Agents (GitHub Copilot, Claude, etc.) connect to this server to query
/// indexed data via the Model Context Protocol.
///
/// Example: quelch mcp --instance mcp --port 8080
Mcp {
/// Instance name. Tells the server which slice of the config it owns
/// and which data sources it exposes.
///
/// Optional: if the config declares exactly one MCP instance, this
/// flag is omitted; with multiple MCP instances, it is required to
/// disambiguate.
#[arg(long)]
instance: Option<String>,
/// Port to listen on.
#[arg(short, long, default_value = "8080")]
port: u16,
/// Bind address.
#[arg(long, default_value = "0.0.0.0")]
bind: String,
/// Override the API key (default: read from QUELCH_MCP_API_KEY env var).
/// When neither is set the server runs in unauthenticated dev mode.
#[arg(long)]
api_key: Option<String>,
},
}
/// Top-level `quelch azure` subcommands.
#[derive(clap::Subcommand)]
pub enum AzureCommands {
/// Compute the Cosmos + AI Search diff against the configured Azure
/// account and print it to stdout. Read-only; never mutates Azure.
Plan,
/// Compute the diff, prompt for confirmation, then push the desired
/// Cosmos containers and AI Search resources to Azure.
Apply {
/// Skip the interactive `[y/N]` confirmation prompt — useful in
/// CI / non-TTY environments.
#[arg(long)]
yes: bool,
},
/// Operate Azure AI Search Indexers.
Indexer {
#[command(subcommand)]
command: IndexerCommands,
},
}
/// `quelch agent` subcommands.
#[derive(clap::Subcommand)]
pub enum AgentCommands {
/// Generate an agent or skill bundle for the given target platform.
Generate {
/// Target platform to generate the bundle for.
#[arg(long, value_enum)]
target: AgentTarget,
/// Output format: agent, skill, or both (where supported).
#[arg(long, value_enum)]
format: Option<AgentFormat>,
/// Output directory for the generated bundle.
#[arg(long, default_value = "./agent-bundle")]
output: PathBuf,
/// MCP instance name.
///
/// Optional: if the config declares exactly one MCP instance, this
/// flag is omitted; with multiple MCP instances, it is required to
/// disambiguate.
#[arg(long)]
instance: Option<String>,
/// Override the public URL of the MCP server.
///
/// Required when the URL is not derivable from config (e.g. custom domain).
#[arg(long)]
url: Option<String>,
},
}
/// Target platform for `quelch agent generate`.
#[derive(Clone, clap::ValueEnum)]
pub enum AgentTarget {
/// Microsoft Copilot Studio (agent form).
CopilotStudio,
/// Anthropic Claude Code (skill form).
ClaudeCode,
/// GitHub Copilot CLI (skill form).
CopilotCli,
/// VS Code GitHub Copilot (skill form).
VscodeCopilot,
/// OpenAI Codex CLI (skill form).
Codex,
/// Generic markdown — both agent and skill forms.
Markdown,
}
/// Output format override for `quelch agent generate`.
#[derive(Clone, clap::ValueEnum)]
pub enum AgentFormat {
/// Generate agent-form output only.
Agent,
/// Generate skill-form output only.
Skill,
/// Generate both agent and skill forms.
Both,
}
/// `quelch instance` subcommands.
#[derive(clap::Subcommand, Debug)]
pub enum InstanceCommand {
/// List instances declared in the master config.
List,
/// Emit a per-instance config file (slimmed slice of the master).
///
/// The emitted YAML contains only the configuration the named instance
/// needs at runtime: control-plane fields (`subscription_id`,
/// `resource_group`, `account`) and the `ai:` block are always stripped;
/// `search:` is stripped for ingest instances; `source_connections` are
/// stripped for MCP instances and limited to the referenced ones for
/// ingest instances.
Config {
/// Instance name from `quelch.yaml`.
name: String,
/// Sanity-check that the instance has the expected kind.
///
/// Errors out if the instance's actual kind in the config does not
/// match this flag — guards against a typo in the instance name
/// dispatching the wrong slice to the wrong host.
#[arg(long, value_enum)]
kind: InstanceKindArg,
/// Write to this path instead of stdout.
#[arg(long)]
output: Option<PathBuf>,
},
}
/// `quelch azure indexer` subcommands.
#[derive(clap::Subcommand)]
pub enum IndexerCommands {
/// Trigger an immediate indexer run.
Run {
/// Indexer name.
name: String,
},
/// Reset the indexer (forces full re-index on next run).
Reset {
/// Indexer name.
name: String,
},
/// Show all indexers and their current state.
Status,
}