@jaguilar87/gaia-ops 3.8.0 β†’ 3.9.1

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,267 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @jaguilar87/gaia-ops - Pending Context Updates Review CLI
5
+ *
6
+ * Interactive tool for reviewing, approving, and rejecting
7
+ * pending context update suggestions from agents.
8
+ *
9
+ * Usage:
10
+ * npx gaia-review # Interactive review mode
11
+ * npx gaia-review --list # List pending updates
12
+ * npx gaia-review --approve ID # Approve specific update
13
+ * npx gaia-review --reject ID # Reject specific update
14
+ * npx gaia-review --stats # Show statistics
15
+ * npx gaia-review --json # Output as JSON
16
+ */
17
+
18
+ import { execSync } from 'child_process';
19
+ import { existsSync } from 'fs';
20
+ import { join } from 'path';
21
+ import chalk from 'chalk';
22
+ import yargs from 'yargs';
23
+ import { hideBin } from 'yargs/helpers';
24
+ import prompts from 'prompts';
25
+
26
+ const CWD = process.cwd();
27
+
28
+ // ============================================================================
29
+ // Python Backend Calls
30
+ // ============================================================================
31
+
32
+ function findReviewEngine() {
33
+ const candidates = [
34
+ join(CWD, '.claude', 'tools', 'review', 'review_engine.py'),
35
+ join(CWD, 'node_modules', '@jaguilar87', 'gaia-ops', 'tools', 'review', 'review_engine.py'),
36
+ join(import.meta.dirname, '..', 'tools', 'review', 'review_engine.py'),
37
+ ];
38
+
39
+ for (const path of candidates) {
40
+ if (existsSync(path)) return path;
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function callReviewEngine(action, opts = {}) {
46
+ const enginePath = findReviewEngine();
47
+ if (!enginePath) {
48
+ console.error(chalk.red('Error: review_engine.py not found'));
49
+ process.exit(1);
50
+ }
51
+
52
+ let cmd = `python3 "${enginePath}" ${action}`;
53
+ if (opts.updateId) cmd += ` --update-id "${opts.updateId}"`;
54
+ if (opts.contextPath) cmd += ` --context-path "${opts.contextPath}"`;
55
+ cmd += ' --json';
56
+
57
+ try {
58
+ const stdout = execSync(cmd, { encoding: 'utf-8', cwd: CWD, timeout: 30000 });
59
+ return JSON.parse(stdout.trim());
60
+ } catch (err) {
61
+ if (err.stdout) {
62
+ try { return JSON.parse(err.stdout.trim()); } catch { /* fall through */ }
63
+ }
64
+ return { error: err.message };
65
+ }
66
+ }
67
+
68
+ // ============================================================================
69
+ // Display Helpers
70
+ // ============================================================================
71
+
72
+ function statusColor(status) {
73
+ switch (status) {
74
+ case 'pending': return chalk.yellow(status);
75
+ case 'approved': return chalk.green(status);
76
+ case 'rejected': return chalk.red(status);
77
+ case 'applied': return chalk.blue(status);
78
+ default: return status;
79
+ }
80
+ }
81
+
82
+ function categoryIcon(category) {
83
+ const icons = {
84
+ new_resource: 'πŸ†•',
85
+ configuration_issue: 'βš™οΈ',
86
+ drift_detected: 'πŸ“',
87
+ dependency_discovered: 'πŸ”—',
88
+ topology_change: 'πŸ—ΊοΈ',
89
+ };
90
+ return icons[category] || 'πŸ“‹';
91
+ }
92
+
93
+ function displayTable(updates) {
94
+ if (!updates || updates.length === 0) {
95
+ console.log(chalk.dim(' No pending updates found.'));
96
+ return;
97
+ }
98
+
99
+ // Header
100
+ const header = ` ${'ID'.padEnd(14)} ${'Category'.padEnd(22)} ${'Summary'.padEnd(50)} ${'Seen'.padEnd(5)} ${'Status'.padEnd(10)}`;
101
+ console.log(chalk.bold.underline(header));
102
+
103
+ for (const u of updates) {
104
+ const icon = categoryIcon(u.category);
105
+ const id = (u.update_id || '').substring(0, 12);
106
+ const cat = `${icon} ${u.category}`.padEnd(22);
107
+ const summary = (u.summary || '').substring(0, 48).padEnd(50);
108
+ const seen = String(u.seen_count || 1).padEnd(5);
109
+ const status = statusColor(u.status || 'pending');
110
+ console.log(` ${chalk.cyan(id.padEnd(14))} ${cat} ${summary} ${seen} ${status}`);
111
+ }
112
+ }
113
+
114
+ function displayStats(stats) {
115
+ console.log(chalk.bold('\n Pending Update Statistics\n'));
116
+ console.log(` Total: ${chalk.bold(stats.total || 0)}`);
117
+ console.log(` Pending: ${chalk.yellow(stats.pending || 0)}`);
118
+ console.log(` Approved: ${chalk.green(stats.approved || 0)}`);
119
+ console.log(` Rejected: ${chalk.red(stats.rejected || 0)}`);
120
+ console.log(` Applied: ${chalk.blue(stats.applied || 0)}`);
121
+
122
+ if (stats.by_category) {
123
+ console.log(chalk.bold('\n By Category:'));
124
+ for (const [cat, count] of Object.entries(stats.by_category)) {
125
+ console.log(` ${categoryIcon(cat)} ${cat}: ${count}`);
126
+ }
127
+ }
128
+
129
+ if (stats.by_agent) {
130
+ console.log(chalk.bold('\n By Agent:'));
131
+ for (const [agent, count] of Object.entries(stats.by_agent)) {
132
+ console.log(` ${agent}: ${count}`);
133
+ }
134
+ }
135
+ }
136
+
137
+ // ============================================================================
138
+ // Interactive Review Mode
139
+ // ============================================================================
140
+
141
+ async function interactiveReview() {
142
+ console.log(chalk.bold('\n Gaia Review - Interactive Mode\n'));
143
+
144
+ const result = callReviewEngine('list');
145
+ if (result.error) {
146
+ console.error(chalk.red(` Error: ${result.error}`));
147
+ return;
148
+ }
149
+
150
+ const updates = result.updates || [];
151
+ if (updates.length === 0) {
152
+ console.log(chalk.green(' No pending updates to review.'));
153
+ return;
154
+ }
155
+
156
+ console.log(` Found ${chalk.bold(updates.length)} pending update(s):\n`);
157
+ displayTable(updates);
158
+ console.log();
159
+
160
+ // Find context path
161
+ const ctxPath = join(CWD, '.claude', 'project-context', 'project-context.json');
162
+
163
+ for (const update of updates) {
164
+ const id = (update.update_id || '').substring(0, 12);
165
+ const response = await prompts({
166
+ type: 'select',
167
+ name: 'action',
168
+ message: `${categoryIcon(update.category)} ${update.summary} (${id})`,
169
+ choices: [
170
+ { title: chalk.green('Approve'), value: 'approve' },
171
+ { title: chalk.red('Reject'), value: 'reject' },
172
+ { title: chalk.dim('Skip'), value: 'skip' },
173
+ { title: chalk.dim('Quit'), value: 'quit' },
174
+ ],
175
+ });
176
+
177
+ if (!response.action || response.action === 'quit') break;
178
+ if (response.action === 'skip') continue;
179
+
180
+ const actionResult = callReviewEngine(response.action, {
181
+ updateId: update.update_id,
182
+ contextPath: response.action === 'approve' ? ctxPath : undefined,
183
+ });
184
+
185
+ if (actionResult.error) {
186
+ console.log(chalk.red(` Error: ${actionResult.error}`));
187
+ } else if (response.action === 'approve') {
188
+ const applied = actionResult.applied ? chalk.green('applied') : chalk.yellow('approved (not applied)');
189
+ console.log(` ${chalk.green('βœ“')} Approved and ${applied}`);
190
+ } else {
191
+ console.log(` ${chalk.red('βœ—')} Rejected`);
192
+ }
193
+ }
194
+ }
195
+
196
+ // ============================================================================
197
+ // CLI
198
+ // ============================================================================
199
+
200
+ const argv = yargs(hideBin(process.argv))
201
+ .usage('Usage: $0 [options]')
202
+ .option('list', { alias: 'l', describe: 'List all pending updates', type: 'boolean' })
203
+ .option('approve', { alias: 'a', describe: 'Approve update by ID', type: 'string' })
204
+ .option('reject', { alias: 'r', describe: 'Reject update by ID', type: 'string' })
205
+ .option('stats', { alias: 's', describe: 'Show statistics', type: 'boolean' })
206
+ .option('json', { describe: 'Output as JSON', type: 'boolean' })
207
+ .option('context-path', { describe: 'Path to project-context.json', type: 'string' })
208
+ .help()
209
+ .alias('help', 'h')
210
+ .parse();
211
+
212
+ async function main() {
213
+ if (argv.list) {
214
+ const result = callReviewEngine('list');
215
+ if (argv.json) {
216
+ console.log(JSON.stringify(result, null, 2));
217
+ } else {
218
+ if (result.error) {
219
+ console.error(chalk.red(`Error: ${result.error}`));
220
+ process.exit(1);
221
+ }
222
+ console.log(chalk.bold(`\n Pending Updates (${result.count || 0}):\n`));
223
+ displayTable(result.updates);
224
+ console.log();
225
+ }
226
+ } else if (argv.approve) {
227
+ const ctxPath = argv.contextPath || join(CWD, '.claude', 'project-context', 'project-context.json');
228
+ const result = callReviewEngine('approve', { updateId: argv.approve, contextPath: ctxPath });
229
+ if (argv.json) {
230
+ console.log(JSON.stringify(result, null, 2));
231
+ } else if (result.error) {
232
+ console.error(chalk.red(`Error: ${result.error}`));
233
+ process.exit(1);
234
+ } else {
235
+ console.log(chalk.green(`βœ“ Update ${argv.approve} approved and ${result.applied ? 'applied' : 'queued'}`));
236
+ }
237
+ } else if (argv.reject) {
238
+ const result = callReviewEngine('reject', { updateId: argv.reject });
239
+ if (argv.json) {
240
+ console.log(JSON.stringify(result, null, 2));
241
+ } else if (result.error) {
242
+ console.error(chalk.red(`Error: ${result.error}`));
243
+ process.exit(1);
244
+ } else {
245
+ console.log(chalk.red(`βœ— Update ${argv.reject} rejected`));
246
+ }
247
+ } else if (argv.stats) {
248
+ const result = callReviewEngine('stats');
249
+ if (argv.json) {
250
+ console.log(JSON.stringify(result, null, 2));
251
+ } else if (result.error) {
252
+ console.error(chalk.red(`Error: ${result.error}`));
253
+ process.exit(1);
254
+ } else {
255
+ displayStats(result.statistics || {});
256
+ console.log();
257
+ }
258
+ } else {
259
+ // Default: interactive mode
260
+ await interactiveReview();
261
+ }
262
+ }
263
+
264
+ main().catch(err => {
265
+ console.error(chalk.red(`Fatal error: ${err.message}`));
266
+ process.exit(1);
267
+ });
@@ -0,0 +1,170 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "description": "Rules for classifying agent output as structural vs operational discoveries",
4
+ "rules": [
5
+ {
6
+ "id": "rule_new_namespace",
7
+ "category": "new_resource",
8
+ "target_section": "cluster_details",
9
+ "patterns": [
10
+ "(?:discovered|found|detected)\\s+(?:new\\s+)?namespace\\s+['\"]?([\\w-]+)['\"]?",
11
+ "namespace\\s+['\"]?([\\w-]+)['\"]?\\s+(?:exists|is present|was found)\\s+(?:but\\s+)?not\\s+in\\s+(?:project[- ]?)?context"
12
+ ],
13
+ "negative_patterns": [
14
+ "kubectl\\s+(?:create|delete)\\s+namespace",
15
+ "creating\\s+namespace",
16
+ "deleting\\s+namespace"
17
+ ],
18
+ "confidence_weight": 0.8,
19
+ "extract_fields": {
20
+ "name": "$1"
21
+ }
22
+ },
23
+ {
24
+ "id": "rule_new_service",
25
+ "category": "new_resource",
26
+ "target_section": "application_services",
27
+ "patterns": [
28
+ "(?:discovered|found|detected)\\s+(?:new\\s+)?service\\s+['\"]?([\\w-]+)['\"]?\\s+(?:running|deployed|active)\\s+in\\s+(?:namespace\\s+)?['\"]?([\\w-]+)['\"]?",
29
+ "service\\s+['\"]?([\\w-]+)['\"]?\\s+is\\s+(?:running|active|deployed)\\s+(?:on|at)\\s+port\\s+(\\d+)",
30
+ "(?:new|unknown)\\s+(?:helm\\s+)?release\\s+['\"]?([\\w-]+)['\"]?"
31
+ ],
32
+ "negative_patterns": [
33
+ "kubectl\\s+(?:create|apply|delete)\\s+(?:service|svc)",
34
+ "helm\\s+(?:install|upgrade|uninstall)"
35
+ ],
36
+ "confidence_weight": 0.85,
37
+ "extract_fields": {
38
+ "service_name": "$1",
39
+ "namespace": "$2"
40
+ }
41
+ },
42
+ {
43
+ "id": "rule_new_bucket",
44
+ "category": "new_resource",
45
+ "target_section": "infrastructure_topology",
46
+ "patterns": [
47
+ "(?:discovered|found|detected)\\s+(?:GCS\\s+)?bucket\\s+['\"]?([\\w.-]+)['\"]?",
48
+ "bucket\\s+['\"]?([\\w.-]+)['\"]?\\s+exists\\s+in\\s+(?:region\\s+)?['\"]?([\\w-]+)['\"]?"
49
+ ],
50
+ "negative_patterns": [
51
+ "gsutil\\s+(?:mb|rb)",
52
+ "creating\\s+bucket"
53
+ ],
54
+ "confidence_weight": 0.8,
55
+ "extract_fields": {
56
+ "bucket_name": "$1",
57
+ "region": "$2"
58
+ }
59
+ },
60
+ {
61
+ "id": "rule_iam_binding",
62
+ "category": "new_resource",
63
+ "target_section": "infrastructure_topology",
64
+ "patterns": [
65
+ "(?:IAM|iam)\\s+binding\\s+(?:between|for|on)\\s+['\"]?([\\w@.-]+)['\"]?\\s+(?:and|to|on)\\s+['\"]?([\\w@.-]+)['\"]?",
66
+ "(?:workload\\s+identity|WI)\\s+binding\\s+.*?['\"]?([\\w@.-]+)['\"]?"
67
+ ],
68
+ "negative_patterns": [
69
+ "gcloud\\s+iam",
70
+ "adding\\s+(?:IAM|iam)\\s+binding"
71
+ ],
72
+ "confidence_weight": 0.8,
73
+ "extract_fields": {
74
+ "principal": "$1",
75
+ "resource": "$2"
76
+ }
77
+ },
78
+ {
79
+ "id": "rule_configuration_issue",
80
+ "category": "configuration_issue",
81
+ "target_section": "project_details",
82
+ "patterns": [
83
+ "configuration\\s+issue[:\\s]+(.+?)(?:\\.|$)",
84
+ "(?:misconfigur|wrong|incorrect|broken)(?:ed|ation)?\\s+(.+?)(?:\\.|$)",
85
+ "(?:references|points to)\\s+(?:wrong|incorrect)\\s+(?:project|bucket|service|account)\\s+['\"]?([\\w.-]+)['\"]?",
86
+ "(?:should be|expected)\\s+['\"]?([\\w.-]+)['\"]?\\s+but\\s+(?:is|found|actual)\\s+['\"]?([\\w.-]+)['\"]?"
87
+ ],
88
+ "negative_patterns": [
89
+ "fixing\\s+configuration",
90
+ "applied\\s+fix",
91
+ "reconfiguring"
92
+ ],
93
+ "confidence_weight": 0.85,
94
+ "extract_fields": {
95
+ "description": "$1"
96
+ }
97
+ },
98
+ {
99
+ "id": "rule_drift_detected",
100
+ "category": "drift_detected",
101
+ "target_section": "application_services",
102
+ "patterns": [
103
+ "(?:drift|mismatch)\\s+detected[:\\s]+(.+?)(?:\\.|$)",
104
+ "actual\\s+(?:state|value|config)\\s+differs\\s+from\\s+(?:declared|expected|documented)",
105
+ "(?:running|actual|live)\\s+(?:on|at)\\s+port\\s+(\\d+)\\s+but\\s+(?:context|config|documented)\\s+says\\s+(\\d+)",
106
+ "(?:is|currently)\\s+(?:running|using|set to)\\s+['\"]?([\\w.-]+)['\"]?\\s+but\\s+(?:context|config)\\s+(?:says|shows)\\s+['\"]?([\\w.-]+)['\"]?"
107
+ ],
108
+ "negative_patterns": [
109
+ "resolving\\s+drift",
110
+ "fixing\\s+drift"
111
+ ],
112
+ "confidence_weight": 0.8,
113
+ "extract_fields": {
114
+ "actual": "$1",
115
+ "expected": "$2"
116
+ }
117
+ },
118
+ {
119
+ "id": "rule_dependency_discovered",
120
+ "category": "dependency_discovered",
121
+ "target_section": "application_services",
122
+ "patterns": [
123
+ "(?:service|app)\\s+['\"]?([\\w-]+)['\"]?\\s+depends\\s+on\\s+['\"]?([\\w-]+)['\"]?",
124
+ "(?:discovered|found)\\s+dependency\\s+(?:between|from)\\s+['\"]?([\\w-]+)['\"]?\\s+(?:to|and)\\s+['\"]?([\\w-]+)['\"]?",
125
+ "['\"]?([\\w-]+)['\"]?\\s+(?:calls|connects to|requires)\\s+['\"]?([\\w-]+)['\"]?\\s+via\\s+(?:HTTP|gRPC|TCP)"
126
+ ],
127
+ "negative_patterns": [
128
+ "adding\\s+dependency",
129
+ "installing\\s+dependency"
130
+ ],
131
+ "confidence_weight": 0.75,
132
+ "extract_fields": {
133
+ "source": "$1",
134
+ "target": "$2"
135
+ }
136
+ },
137
+ {
138
+ "id": "rule_ingress_host",
139
+ "category": "topology_change",
140
+ "target_section": "infrastructure_topology",
141
+ "patterns": [
142
+ "(?:new|discovered|found)\\s+ingress\\s+host\\s+['\"]?([\\w.-]+)['\"]?",
143
+ "ingress\\s+['\"]?([\\w.-]+)['\"]?\\s+routes\\s+to\\s+['\"]?([\\w-]+)['\"]?",
144
+ "(?:external|public)\\s+(?:endpoint|URL|host)\\s+['\"]?([\\w.-]+)['\"]?"
145
+ ],
146
+ "negative_patterns": [
147
+ "kubectl\\s+(?:create|apply)\\s+ingress",
148
+ "creating\\s+ingress"
149
+ ],
150
+ "confidence_weight": 0.8,
151
+ "extract_fields": {
152
+ "host": "$1",
153
+ "backend": "$2"
154
+ }
155
+ }
156
+ ],
157
+ "global_negative_patterns": [
158
+ "^\\s*\\$\\s+",
159
+ "^\\s*#\\s+",
160
+ "running\\s+command",
161
+ "executing:",
162
+ "output:",
163
+ "^\\s*(?:NAME|STATUS|READY|AGE|RESTARTS)\\s+",
164
+ "latency.*?\\d+ms",
165
+ "CPU\\s+(?:usage|utilization)",
166
+ "memory.*?\\d+[MGK]i",
167
+ "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}"
168
+ ],
169
+ "confidence_threshold": 0.7
170
+ }