@occasiolabs/occasio 0.8.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.
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +216 -0
- package/bin/occasio-mcp.js +5 -0
- package/bin/occasio.js +2 -0
- package/bin/supervisor/README.md +90 -0
- package/bin/supervisor/com.occasio.proxy.plist.template +36 -0
- package/bin/supervisor/install-windows-task.ps1 +48 -0
- package/bin/supervisor/occasio.service +18 -0
- package/docs/AUDIT.md +120 -0
- package/docs/attest_verify.py +283 -0
- package/docs/audit_walker.py +65 -0
- package/docs/canonicalize.py +99 -0
- package/docs/compliance-mapping.md +93 -0
- package/docs/demos/mcp-block.md +148 -0
- package/docs/edr-calibration.md +73 -0
- package/docs/edr-demo.md +83 -0
- package/docs/python-verifier.md +74 -0
- package/docs/reference-pipeline.md +140 -0
- package/package.json +69 -0
- package/policy-templates/dev-default.yml +84 -0
- package/policy-templates/finance.yml +61 -0
- package/policy-templates/strict.yml +49 -0
- package/schemas/agent-attestation-v1.json +190 -0
- package/schemas/occasio-policy.schema.json +99 -0
- package/spec/agent-attestation/v1/README.md +137 -0
- package/src/adapters/claude-code.js +518 -0
- package/src/adapters/cline.js +161 -0
- package/src/adapters/computer-use-cli.js +198 -0
- package/src/adapters/computer-use.js +227 -0
- package/src/analyzer.js +170 -0
- package/src/anomaly/cli.js +143 -0
- package/src/anomaly/detectors/deny-rate.js +84 -0
- package/src/anomaly/detectors/file-read-volume.js +109 -0
- package/src/anomaly/detectors/secret-redact-rate.js +107 -0
- package/src/anomaly/detectors/unknown-tool-input.js +83 -0
- package/src/anomaly/index.js +169 -0
- package/src/attest/canonicalize.js +97 -0
- package/src/attest/index.js +355 -0
- package/src/attest/run-slice.js +57 -0
- package/src/attest/sign.js +186 -0
- package/src/attest/verify.js +192 -0
- package/src/audit/errors.js +21 -0
- package/src/audit/input-normalizer.js +121 -0
- package/src/audit/jsonl-auditor.js +178 -0
- package/src/audit/verifier.js +152 -0
- package/src/baseline.js +507 -0
- package/src/boundary.js +238 -0
- package/src/budget.js +42 -0
- package/src/classifier.js +115 -0
- package/src/context-budget.js +77 -0
- package/src/core/boundary-event.js +75 -0
- package/src/core/decision.js +61 -0
- package/src/core/pipeline.js +66 -0
- package/src/core/tool-names.js +105 -0
- package/src/dashboard.js +892 -0
- package/src/demo/README.md +31 -0
- package/src/demo/anomalies-demo.js +211 -0
- package/src/demo/attest-demo.js +198 -0
- package/src/distiller.js +155 -0
- package/src/embeddings.json +72 -0
- package/src/executor/dispatcher.js +230 -0
- package/src/harness.js +817 -0
- package/src/index.js +1711 -0
- package/src/inspect.js +329 -0
- package/src/interceptor.js +1198 -0
- package/src/lao.js +185 -0
- package/src/lao_prep.py +119 -0
- package/src/ledger.js +209 -0
- package/src/mcp-experiment.js +140 -0
- package/src/mcp-normalize.js +139 -0
- package/src/mcp-server.js +320 -0
- package/src/outbound-policy.js +433 -0
- package/src/policy/built-in-classifiers.js +78 -0
- package/src/policy/doctor.js +226 -0
- package/src/policy/engine.js +339 -0
- package/src/policy/init.js +153 -0
- package/src/policy/loader.js +448 -0
- package/src/policy/rules-default.js +36 -0
- package/src/policy/shell-path.js +135 -0
- package/src/policy/show.js +196 -0
- package/src/policy/validate.js +310 -0
- package/src/preflight/cli.js +164 -0
- package/src/preflight/miner.js +329 -0
- package/src/proxy/agent-router.js +93 -0
- package/src/redteam.js +428 -0
- package/src/replay.js +446 -0
- package/src/report/index.js +224 -0
- package/src/runtime.js +595 -0
- package/src/scanner/index.js +49 -0
- package/src/selftest.js +192 -0
- package/src/session.js +36 -0
package/src/inspect.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* inspect.js โ Cloud-boundary manifest for Occasio log entries.
|
|
5
|
+
*
|
|
6
|
+
* Answers per-request:
|
|
7
|
+
* - What exactly reached Anthropic?
|
|
8
|
+
* - What ran locally and why?
|
|
9
|
+
* - What was trimmed or distilled before send?
|
|
10
|
+
* - What was blocked and never sent?
|
|
11
|
+
*
|
|
12
|
+
* All display is derived from existing JSONL log fields plus two new ones:
|
|
13
|
+
* lao_dropped (string[]) โ files LAO removed before send
|
|
14
|
+
* outbound_message_count (number) โ messages in the forwarded request
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const { readDayLog, readSessionEntries } = require('./ledger');
|
|
21
|
+
|
|
22
|
+
const LOG_DIR = path.join(os.homedir(), '.occasio');
|
|
23
|
+
const SESSION_FILE = path.join(LOG_DIR, 'session.json');
|
|
24
|
+
|
|
25
|
+
const col = {
|
|
26
|
+
r: s => `\x1b[31m${s}\x1b[0m`, g: s => `\x1b[32m${s}\x1b[0m`,
|
|
27
|
+
y: s => `\x1b[33m${s}\x1b[0m`, c: s => `\x1b[36m${s}\x1b[0m`,
|
|
28
|
+
d: s => `\x1b[2m${s}\x1b[0m`, b: s => `\x1b[1m${s}\x1b[0m`,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function todayStr() {
|
|
32
|
+
const d = new Date();
|
|
33
|
+
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function fmtN(n) {
|
|
37
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
38
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
|
|
39
|
+
return String(n || 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract structured cloud-boundary facts from a raw log entry.
|
|
44
|
+
* Pure function โ no I/O, fully testable.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} entry A JSONL log entry
|
|
47
|
+
* @returns {BoundaryFacts}
|
|
48
|
+
*/
|
|
49
|
+
function buildBoundaryFacts(entry) {
|
|
50
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
51
|
+
|
|
52
|
+
const et = entry.event_type || (entry.intercepted ? 'local_only' : 'cloud_sent');
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
event_type: et,
|
|
56
|
+
// true for any event type where something reached Anthropic (even after trim/intercept)
|
|
57
|
+
reached_anthropic: et !== 'blocked' && et !== 'budget_exceeded',
|
|
58
|
+
blocked_entirely: et === 'blocked' || et === 'budget_exceeded',
|
|
59
|
+
|
|
60
|
+
// Tokens Anthropic actually reported (exact โ from provider response)
|
|
61
|
+
input_tokens: entry.input_tokens || 0,
|
|
62
|
+
output_tokens: entry.output_tokens || 0,
|
|
63
|
+
cost: entry.cost || 0,
|
|
64
|
+
cache_read_tokens: entry.cache_read_tokens || 0,
|
|
65
|
+
cache_write_tokens: entry.cache_write_tokens || 0,
|
|
66
|
+
cache_savings: entry.cache_savings || 0,
|
|
67
|
+
|
|
68
|
+
// Files in context (names + approximate token counts โ estimated by Occasio analyzer)
|
|
69
|
+
files_in_context: entry.file_tokens || [],
|
|
70
|
+
|
|
71
|
+
// How many messages were in the outbound request (exact โ from request body)
|
|
72
|
+
outbound_message_count: entry.outbound_message_count || null,
|
|
73
|
+
|
|
74
|
+
// What LAO removed before forwarding (exact โ recorded at trim time)
|
|
75
|
+
lao_dropped: entry.lao_dropped || [],
|
|
76
|
+
lao_tokens_saved: entry.lao_tokens_saved || 0,
|
|
77
|
+
lao_cost_saved: entry.lao_cost_saved || 0,
|
|
78
|
+
|
|
79
|
+
// Tool runs executed locally (exact โ from interceptor)
|
|
80
|
+
tools_local: (entry.tools || []),
|
|
81
|
+
tools_distilled: (entry.tools || []).filter(t => t.distilled),
|
|
82
|
+
distill_tokens_saved: entry.distill_tokens_saved || 0,
|
|
83
|
+
|
|
84
|
+
// Blocked by secret detection (exact โ from analyzer)
|
|
85
|
+
secrets_detected: entry.secrets || [],
|
|
86
|
+
files_rule_blocked: entry.blocked || [],
|
|
87
|
+
|
|
88
|
+
// Budget enforcement
|
|
89
|
+
budget_limit: entry.budget_limit ?? null,
|
|
90
|
+
budget_spent: entry.budget_spent ?? null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// โโ Boundary section renderers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
95
|
+
|
|
96
|
+
const RULE = col.d('โ'.repeat(56));
|
|
97
|
+
|
|
98
|
+
function sectionHeader(icon, label, note) {
|
|
99
|
+
const noteStr = note ? col.d(` (${note})`) : '';
|
|
100
|
+
return `\n${icon} ${col.b(label)}${noteStr}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderCloudSent(facts) {
|
|
104
|
+
// LAO trim section (if applicable)
|
|
105
|
+
if (facts.lao_dropped.length > 0 || facts.lao_tokens_saved > 0) {
|
|
106
|
+
console.log(sectionHeader(col.y('โ'), 'TRIMMED BEFORE SEND', 'LAO context optimizer'));
|
|
107
|
+
for (const f of facts.lao_dropped) {
|
|
108
|
+
const name = f.split(/[/\\]/).slice(-2).join('/');
|
|
109
|
+
console.log(` ${col.d(name)} ${col.y('removed')}`);
|
|
110
|
+
}
|
|
111
|
+
if (facts.lao_dropped.length === 0 && facts.lao_tokens_saved > 0) {
|
|
112
|
+
console.log(col.d(` (file list unavailable โ log entry predates v0.5.3)`));
|
|
113
|
+
}
|
|
114
|
+
if (facts.lao_tokens_saved > 0) {
|
|
115
|
+
console.log(col.y(` Tokens saved: ~${fmtN(facts.lao_tokens_saved)}`
|
|
116
|
+
+ (facts.lao_cost_saved > 0 ? col.g(` ($${facts.lao_cost_saved.toFixed(4)})`) : '')));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Cloud payload
|
|
121
|
+
console.log(sectionHeader(col.c('โ'), 'SENT TO ANTHROPIC', 'provider-reported token counts'));
|
|
122
|
+
console.log(` Tokens: ${col.y(fmtN(facts.input_tokens) + ' in')} / ${col.y(fmtN(facts.output_tokens) + ' out')} ${col.g('$' + facts.cost.toFixed(4))}`);
|
|
123
|
+
if (facts.outbound_message_count) {
|
|
124
|
+
console.log(col.d(` Messages: ${facts.outbound_message_count} in request`));
|
|
125
|
+
}
|
|
126
|
+
if (facts.cache_read_tokens > 0) {
|
|
127
|
+
console.log(col.d(` Cache: ${fmtN(facts.cache_read_tokens)} tokens read`
|
|
128
|
+
+ (facts.cache_savings > 0 ? ` (saved $${facts.cache_savings.toFixed(4)})` : '')));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const fts = facts.files_in_context;
|
|
132
|
+
if (fts.length > 0) {
|
|
133
|
+
console.log(col.d(` Files in context (names from request body โ token counts are estimates):`));
|
|
134
|
+
const show = fts.slice(0, 8);
|
|
135
|
+
for (const f of show) {
|
|
136
|
+
const name = (f.name || '?').split(/[/\\]/).slice(-2).join('/');
|
|
137
|
+
console.log(` ${name.padEnd(40)} ${col.d('~' + fmtN(f.tokens || 0) + ' tokens')}`);
|
|
138
|
+
}
|
|
139
|
+
if (fts.length > 8) console.log(col.d(` โฆ and ${fts.length - 8} more files`));
|
|
140
|
+
} else if (facts.input_tokens > 0) {
|
|
141
|
+
console.log(col.d(` (no per-file breakdown โ file analysis not available for this request)`));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Distilled tool results note
|
|
145
|
+
const dist = facts.tools_distilled;
|
|
146
|
+
if (dist.length > 0) {
|
|
147
|
+
console.log(sectionHeader(col.y('โ'), 'DISTILLED BEFORE RE-ENTERING MODEL', 'tool output reduced'));
|
|
148
|
+
for (const t of dist) {
|
|
149
|
+
const cmd = (t.cmd || '?').slice(0, 48);
|
|
150
|
+
console.log(` ${cmd.padEnd(50)} ${col.y('โ ' + (t.distillLabel || 'distilled'))}`);
|
|
151
|
+
}
|
|
152
|
+
console.log(col.d(` Full output saved locally. Run: ${col.b('occasio distill')} to view.`));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function renderLocalOnly(facts) {
|
|
157
|
+
const tools = facts.tools_local;
|
|
158
|
+
|
|
159
|
+
if (tools.length > 0) {
|
|
160
|
+
console.log(sectionHeader(col.g('โก'), 'EXECUTED LOCALLY', 'tool results forwarded to Anthropic in follow-up call'));
|
|
161
|
+
for (const t of tools) {
|
|
162
|
+
const cmd = (t.cmd || '?').slice(0, 46);
|
|
163
|
+
const sz = t.bytes != null ? `${(t.bytes / 1024).toFixed(1)} KB`.padStart(9) : ''.padStart(9);
|
|
164
|
+
const nat = t.native ? col.g(' native') : col.d(' exec ');
|
|
165
|
+
const dis = t.distilled ? col.y(` โ ${(t.distillLabel || 'distilled').slice(0, 30)}`) : '';
|
|
166
|
+
console.log(` ${cmd.padEnd(48)} ${col.d(sz)}${nat}${dis}`);
|
|
167
|
+
}
|
|
168
|
+
console.log(col.d(`\n Note: "local" means execution happened on this machine (not in Claude Code's`));
|
|
169
|
+
console.log(col.d(` subprocess). Tool results were forwarded to Anthropic in Occasio's follow-up call.`));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (facts.input_tokens > 0 || facts.output_tokens > 0) {
|
|
173
|
+
console.log(sectionHeader(col.c('โ'), 'SENT TO ANTHROPIC', 'initial + follow-up calls combined'));
|
|
174
|
+
console.log(` Tokens: ${col.y(fmtN(facts.input_tokens) + ' in')} / ${col.y(fmtN(facts.output_tokens) + ' out')} ${col.g('$' + facts.cost.toFixed(4))}`);
|
|
175
|
+
if (facts.cache_read_tokens > 0) {
|
|
176
|
+
console.log(col.d(` Cache: ${fmtN(facts.cache_read_tokens)} tokens read (saved $${facts.cache_savings.toFixed(4)})`));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const dist = facts.tools_distilled;
|
|
181
|
+
if (dist.length > 0) {
|
|
182
|
+
console.log(sectionHeader(col.y('โ'), 'DISTILLED BEFORE FORWARDING', 'tool output reduced before re-entering model'));
|
|
183
|
+
for (const t of dist) {
|
|
184
|
+
console.log(` ${(t.cmd || '?').slice(0, 48).padEnd(50)} ${col.y('โ ' + (t.distillLabel || ''))}`);
|
|
185
|
+
}
|
|
186
|
+
console.log(col.d(` Run: ${col.b('occasio distill')} to view raw output.`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function renderBlocked(facts) {
|
|
191
|
+
console.log(`\n${col.r('๐ BLOCKED')} ${col.d('โ nothing was sent to Anthropic')}`);
|
|
192
|
+
const secs = facts.secrets_detected;
|
|
193
|
+
if (secs.length > 0) {
|
|
194
|
+
console.log(col.r(`\n Detected secrets:`));
|
|
195
|
+
for (const s of secs) {
|
|
196
|
+
const line = s.line ? col.d(` line ${s.line}`) : '';
|
|
197
|
+
console.log(` ${col.r('ร')} ${s.label || 'secret'}${line}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const blk = facts.files_rule_blocked;
|
|
201
|
+
if (blk.length > 0) {
|
|
202
|
+
console.log(col.r(`\n Rule-blocked files:`));
|
|
203
|
+
for (const f of blk) console.log(` ${col.r('ร')} ${f}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function renderBudgetExceeded(facts) {
|
|
208
|
+
console.log(`\n${col.r('๐ซ BUDGET EXCEEDED')} ${col.d('โ nothing was sent to Anthropic')}`);
|
|
209
|
+
if (facts.budget_limit != null) {
|
|
210
|
+
console.log(` Budget limit: ${col.y('$' + facts.budget_limit.toFixed(4))}`);
|
|
211
|
+
console.log(` Spent so far: ${col.r('$' + (facts.budget_spent || 0).toFixed(4))}`);
|
|
212
|
+
const overage = (facts.budget_spent || 0) - facts.budget_limit;
|
|
213
|
+
if (overage > 0) console.log(col.d(` Over by: $${overage.toFixed(4)}`));
|
|
214
|
+
}
|
|
215
|
+
console.log(col.d(` Reset: occasio clear | Increase: restart with --budget N`));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// โโ Main entry renderer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
219
|
+
|
|
220
|
+
const ET_LABEL = {
|
|
221
|
+
cloud_sent: col.c('[cloud_sent]'),
|
|
222
|
+
local_only: col.g('[local_only]'),
|
|
223
|
+
trimmed: col.y('[trimmed]'),
|
|
224
|
+
blocked: col.r('[blocked]'),
|
|
225
|
+
budget_exceeded: col.r('[budget_exceeded]'),
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Print a full boundary manifest for one log entry.
|
|
230
|
+
* @param {object} entry Raw log entry
|
|
231
|
+
* @param {number} idxLabel 1-based display index
|
|
232
|
+
* @param {number} total Total entries in the view (for header)
|
|
233
|
+
*/
|
|
234
|
+
function printBoundaryEntry(entry, idxLabel, total) {
|
|
235
|
+
const facts = buildBoundaryFacts(entry);
|
|
236
|
+
if (!facts) return;
|
|
237
|
+
|
|
238
|
+
const ts = entry.ts || (entry.iso ? new Date(entry.iso).toTimeString().slice(0, 8) : '?');
|
|
239
|
+
const model = (entry.model || '').replace('claude-', '').replace(/-\d{8}$/, '');
|
|
240
|
+
const label = ET_LABEL[facts.event_type] || col.d(`[${facts.event_type}]`);
|
|
241
|
+
const ofStr = total > 1 ? ` of ${total}` : '';
|
|
242
|
+
|
|
243
|
+
console.log(`\n${col.b('โก Request ' + idxLabel + ofStr)} ${label} ${col.d(ts)} ${col.d(model)}`);
|
|
244
|
+
console.log(RULE);
|
|
245
|
+
|
|
246
|
+
switch (facts.event_type) {
|
|
247
|
+
case 'cloud_sent':
|
|
248
|
+
case 'trimmed':
|
|
249
|
+
renderCloudSent(facts);
|
|
250
|
+
break;
|
|
251
|
+
case 'local_only':
|
|
252
|
+
renderLocalOnly(facts);
|
|
253
|
+
break;
|
|
254
|
+
case 'blocked':
|
|
255
|
+
renderBlocked(facts);
|
|
256
|
+
break;
|
|
257
|
+
case 'budget_exceeded':
|
|
258
|
+
renderBudgetExceeded(facts);
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
console.log(col.d(` (unknown event type: ${facts.event_type})`));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log('\n' + RULE);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// โโ CLI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
268
|
+
|
|
269
|
+
function runInspectCli(args) {
|
|
270
|
+
let session = null;
|
|
271
|
+
try { session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8')); } catch {}
|
|
272
|
+
|
|
273
|
+
const todayEntries = readDayLog(todayStr());
|
|
274
|
+
|
|
275
|
+
const scopeIdx = args.indexOf('--scope');
|
|
276
|
+
const scope = scopeIdx >= 0 ? (args[scopeIdx + 1] || 'session') : 'session';
|
|
277
|
+
const entries = scope === 'session'
|
|
278
|
+
? readSessionEntries(todayEntries, session?.start)
|
|
279
|
+
: todayEntries;
|
|
280
|
+
|
|
281
|
+
if (!entries.length) {
|
|
282
|
+
console.log(col.d(`\n No log entries yet. Run: occasio claude\n`));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// --entry N: single entry by 0-based index
|
|
287
|
+
const entryFlagIdx = args.indexOf('--entry');
|
|
288
|
+
if (entryFlagIdx >= 0) {
|
|
289
|
+
const n = parseInt(args[entryFlagIdx + 1]);
|
|
290
|
+
if (isNaN(n) || n < 0 || n >= entries.length) {
|
|
291
|
+
console.log(col.r(` No entry at index ${n} (${entries.length} total in scope).`));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
console.log(col.b(`\nโก Occasio Inspect โ Entry ${n + 1} of ${entries.length}`));
|
|
295
|
+
printBoundaryEntry(entries[n], n + 1, entries.length);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --run <id-prefix>: all entries matching run_id prefix
|
|
300
|
+
const runFlagIdx = args.indexOf('--run');
|
|
301
|
+
if (runFlagIdx >= 0) {
|
|
302
|
+
const runFilter = args[runFlagIdx + 1] || '';
|
|
303
|
+
const runEntries = entries.filter(e => e.run_id && e.run_id.startsWith(runFilter));
|
|
304
|
+
if (!runEntries.length) {
|
|
305
|
+
console.log(col.d(`\n No entries for run: ${runFilter}\n`));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const shortId = runFilter.slice(0, 8);
|
|
309
|
+
console.log(col.b(`\nโก Occasio Inspect โ Run ${shortId} (${runEntries.length} event${runEntries.length === 1 ? '' : 's'})`));
|
|
310
|
+
runEntries.forEach((e, i) => printBoundaryEntry(e, i + 1, runEntries.length));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// default / --last N: last N entries (default 1)
|
|
315
|
+
const lastFlagIdx = args.indexOf('--last');
|
|
316
|
+
const limit = lastFlagIdx >= 0 ? (parseInt(args[lastFlagIdx + 1]) || 1) : 1;
|
|
317
|
+
const slice = entries.slice(-limit);
|
|
318
|
+
const offset = entries.length - slice.length;
|
|
319
|
+
|
|
320
|
+
console.log(col.b(`\nโก Occasio Inspect`));
|
|
321
|
+
console.log(col.d(` scope: ${scope} ยท ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'} ยท showing last ${slice.length}\n`));
|
|
322
|
+
|
|
323
|
+
slice.forEach((e, i) => printBoundaryEntry(e, offset + i + 1, entries.length));
|
|
324
|
+
|
|
325
|
+
console.log(col.d(`\n --last N show last N entries --entry N inspect by index`));
|
|
326
|
+
console.log(col.d(` --run <id> inspect a specific run --scope today full-day scope\n`));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = { buildBoundaryFacts, printBoundaryEntry, runInspectCli };
|