quelch 0.9.2

Ingest data from Jira, Confluence, and more directly into Azure AI Search
Documentation
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
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
use clap::Parser;
use quelch::ai::AiCommands;
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 {
    /// Run a one-shot sync of all configured sources
    Sync {
        /// Auto-create missing indexes without prompting
        #[arg(long)]
        create_indexes: bool,
        /// Also purge orphaned documents from indexes
        #[arg(long)]
        purge: bool,
        /// Maximum number of documents to sync (useful for debugging)
        #[arg(long)]
        max_docs: Option<u64>,
    },
    /// Run continuous sync (polls at configured interval)
    Watch {
        /// Auto-create missing indexes without prompting
        #[arg(long)]
        create_indexes: bool,
        /// Maximum number of documents to sync per cycle (useful for debugging)
        #[arg(long)]
        max_docs: Option<u64>,
    },
    /// Check and create Azure AI Search indexes needed by the config
    Setup {
        /// Auto-create without prompting
        #[arg(short, long)]
        yes: bool,
    },
    /// Show sync status for all sources
    Status {
        /// Filter to cursors belonging to this deployment
        #[arg(long)]
        deployment: 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 (force full re-sync on next run)
    Reset {
        /// Source name to reset (omit to reset all)
        #[arg(long)]
        source: Option<String>,
        /// Only reset a single subsource (project or space key) within the source
        #[arg(long)]
        subsource: Option<String>,
        /// Skip the interactive confirmation prompt
        #[arg(long)]
        yes: bool,
    },
    /// Delete all configured indexes from Azure AI Search and clear sync state
    ResetIndexes,
    /// Validate config file without running
    Validate,
    /// Print the effective (sliced) config for one deployment
    EffectiveConfig {
        /// Name of the deployment to slice for
        name: String,
    },
    /// Interactive wizard to scaffold a quelch.yaml config
    Init {
        /// 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,
    },
    /// Generate ready-to-run on-prem deployment artefacts
    GenerateDeployment {
        /// Deployment name from quelch.yaml (should be target: onprem).
        name: String,
        /// Output target: docker, systemd, or k8s.
        #[arg(long, value_enum)]
        target: OnpremTargetArg,
        /// Output directory for generated artefacts.
        #[arg(long, default_value = "./deploy")]
        output: PathBuf,
    },
    /// Start a local mock Jira and Confluence server for testing
    Mock {
        /// Port to listen on
        #[arg(short, long, default_value = "9999")]
        port: u16,
    },
    /// Run quelch against a fully simulated environment for local testing and CI.
    Sim {
        /// Run for this long then exit. Default: run until Ctrl-C. Example: 30s, 2m, 1h.
        #[arg(long)]
        duration: Option<humantime::Duration>,
        /// Seed the activity generator for reproducible runs.
        #[arg(long)]
        seed: Option<u64>,
        /// Scale activity rate. 1.0 = default, 2.0 = twice as fast.
        #[arg(long, default_value = "1.0")]
        rate_multiplier: f64,
        /// Probability each Azure request gets a 429 or 503. 0.0 disables.
        #[arg(long, default_value = "0.03")]
        fault_rate: f64,
        /// CI-friendly: fail with exit code 1 if fewer than N docs are indexed.
        #[arg(long)]
        assert_docs: Option<u64>,
        /// Render the TUI to a headless backend and write a multi-frame text
        /// dump to this file. Enables deterministic verification of the TUI
        /// from CI or an AI agent. Implies --no-tui for stdout.
        #[arg(long)]
        snapshot_to: Option<PathBuf>,
        /// Number of frames to capture when --snapshot-to is set.
        #[arg(long, default_value = "10")]
        snapshot_frames: u32,
        /// Width of the TestBackend when --snapshot-to is set. Default 120.
        #[arg(long, default_value = "120")]
        snapshot_width: u16,
        /// Height of the TestBackend when --snapshot-to is set. Default 40.
        #[arg(long, default_value = "40")]
        snapshot_height: 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,
    },
    /// 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,
    },
    /// 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,
    },
    /// 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 Copilot Studio agent topics and instructions
    GenerateAgent {
        /// Output directory for generated files
        #[arg(short, long, default_value = "copilot-studio")]
        output: PathBuf,
    },
    /// Generate an agent or skill bundle for a specific platform
    Agent {
        #[command(subcommand)]
        command: AgentCommands,
    },
    /// Run the continuous ingest worker for a deployment
    Ingest {
        /// Deployment name — which slice of the config this worker owns.
        #[arg(long)]
        deployment: 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, deploy, pull, indexer, logs, destroy).
    Azure {
        #[command(subcommand)]
        command: AzureCommands,
    },
    /// Start the MCP HTTP server for a deployment.
    ///
    /// Agents (GitHub Copilot, Claude, etc.) connect to this server to query
    /// indexed data via the Model Context Protocol.
    ///
    /// Example: quelch mcp --deployment mcp --port 8080
    Mcp {
        /// Deployment name (required). Tells the server which slice of the
        /// config it owns and which data sources it exposes.
        #[arg(long)]
        deployment: 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>,
    },
}

/// On-prem target for `quelch generate-deployment`.
#[derive(Clone, clap::ValueEnum)]
pub enum OnpremTargetArg {
    /// Docker Compose.
    Docker,
    /// systemd unit file.
    Systemd,
    /// Kubernetes manifests.
    K8s,
}

/// Top-level `quelch azure` subcommands.
#[derive(clap::Subcommand)]
pub enum AzureCommands {
    /// Synthesise Bicep + rigg files; show the combined diff. No changes applied.
    Plan {
        /// Deployment name (omit to plan all).
        deployment: Option<String>,
        /// Write Bicep to a custom location (default .quelch/azure/<name>.bicep).
        #[arg(long)]
        out: Option<PathBuf>,
        /// Synthesise only; skip the `az deployment group what-if` call.
        #[arg(long)]
        no_what_if: bool,
    },
    /// Plan + apply the deployment to Azure.
    Deploy {
        /// Deployment name (omit to deploy all).
        deployment: Option<String>,
        /// Skip the interactive confirmation prompt.
        #[arg(long)]
        yes: bool,
        /// Equivalent to `quelch azure plan` — show the diff but don't apply.
        #[arg(long)]
        dry_run: bool,
    },
    /// Pull live AI Search/Foundry config back into rigg/.
    Pull {
        /// Optional resource type filter (e.g. "index", "indexer").
        kind: Option<String>,
        /// Show what would change without writing.
        #[arg(long)]
        diff: bool,
    },
    /// Operate Azure AI Search Indexers.
    Indexer {
        #[command(subcommand)]
        command: IndexerCommands,
    },
    /// Tail logs from a deployed Container App.
    Logs {
        /// Deployment name.
        deployment: String,
        /// Number of log lines to show.
        #[arg(long, default_value = "100")]
        tail: usize,
        /// Stream logs continuously (Ctrl-C to stop).
        #[arg(long)]
        follow: bool,
        /// Only show logs since this time (e.g. "1h", "30m").
        #[arg(long)]
        since: Option<String>,
    },
    /// Remove a single deployment's Container App from Azure.
    Destroy {
        /// Deployment name.
        deployment: String,
        /// Skip the interactive confirmation prompt.
        #[arg(long)]
        yes: bool,
    },
}

/// `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 deployment name (defaults to the first MCP deployment in config).
        #[arg(long)]
        deployment: 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 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,
}