getprismo 0.1.11 → 0.1.14
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/README.md +114 -1
- package/lib/prismo-dev/mcp.js +264 -0
- package/lib/prismo-dev/shield.js +384 -0
- package/lib/prismo-dev-scan.js +126 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,17 +22,21 @@ prismodev catches it before, during, and after.
|
|
|
22
22
|
|
|
23
23
|
## the loop
|
|
24
24
|
|
|
25
|
-
prismodev
|
|
25
|
+
prismodev covers the full AI coding session:
|
|
26
26
|
|
|
27
27
|
```
|
|
28
28
|
before you code npx getprismo doctor
|
|
29
29
|
while you code npx getprismo watch
|
|
30
|
+
noisy commands npx getprismo shield -- npm test
|
|
30
31
|
after you code npx getprismo cc timeline
|
|
32
|
+
agent-native npx getprismo mcp
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
**doctor** diagnoses the repo, applies safe fixes, and shows the before/after score.
|
|
34
36
|
**watch** monitors context pressure live and warns when things go wrong.
|
|
35
37
|
**cc timeline** reconstructs what happened in the session so you learn from it.
|
|
38
|
+
**shield** runs noisy commands without dumping full output back into the agent context.
|
|
39
|
+
**mcp** exposes PrismoDev as local tools so compatible agents can scan, search shield output, and request scoped context directly.
|
|
36
40
|
|
|
37
41
|
---
|
|
38
42
|
|
|
@@ -137,6 +141,51 @@ watch caught lockfiles entering context, a file being read 286 times, and tool o
|
|
|
137
141
|
|
|
138
142
|
---
|
|
139
143
|
|
|
144
|
+
## new: context shield
|
|
145
|
+
|
|
146
|
+
if you know a command may dump huge output, run it through prismo:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
npx getprismo shield -- npm test
|
|
150
|
+
npx getprismo shield -- pytest -q
|
|
151
|
+
npx getprismo shield -- npm run build
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
shield executes the command locally, stores full stdout/stderr under `.prismo/shield/runs/`, indexes the output in `.prismo/shield/shield.sqlite` using SQLite FTS5 when available, and prints only a compact summary plus useful error lines.
|
|
155
|
+
|
|
156
|
+
this is the lightweight context-sandbox layer: the full output stays on disk until you explicitly inspect it, instead of being pasted into the model context and re-sent every turn.
|
|
157
|
+
|
|
158
|
+
example:
|
|
159
|
+
|
|
160
|
+
```text
|
|
161
|
+
Prismo Shield
|
|
162
|
+
|
|
163
|
+
Command: npm test
|
|
164
|
+
Exit: 1
|
|
165
|
+
Captured: 186 KB (~46,500 tokens kept out of chat)
|
|
166
|
+
|
|
167
|
+
Full Output Stored:
|
|
168
|
+
- .prismo/shield/runs/2026-05-20T.../stdout.txt
|
|
169
|
+
- .prismo/shield/runs/2026-05-20T.../stderr.txt
|
|
170
|
+
- .prismo/shield/shield.sqlite
|
|
171
|
+
|
|
172
|
+
Summary Returned To Context:
|
|
173
|
+
- ERROR: auth.test.ts expected 200 received 401
|
|
174
|
+
- FAIL src/auth/session.test.ts
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
search previous shield output without reloading whole logs:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
npx getprismo shield last
|
|
181
|
+
npx getprismo shield search "auth expected 200"
|
|
182
|
+
npx getprismo shield search "AUTH_FAILURE" --json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
this is intentionally not magic interception yet. it is a safe local-first primitive you can tell agents to use for noisy commands.
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
140
189
|
## new: live guardrails mode
|
|
141
190
|
|
|
142
191
|
the easiest proactive mode is:
|
|
@@ -485,6 +534,8 @@ no install needed. npx runs it directly.
|
|
|
485
534
|
| `scan --ci` | fail CI when token-risk gates fail |
|
|
486
535
|
| `optimize` | generate `.prismo/` context packs |
|
|
487
536
|
| `context` | print paste-ready prompt for agents |
|
|
537
|
+
| `shield` | run noisy commands while keeping full output out of chat |
|
|
538
|
+
| `mcp` | expose PrismoDev tools over local MCP stdio |
|
|
488
539
|
| `setup` | detect tools, logs, proxy readiness |
|
|
489
540
|
| `usage` | show raw session token usage |
|
|
490
541
|
| `init` | add npm scripts and .prismo/README.md |
|
|
@@ -526,6 +577,63 @@ npx getprismo watch codex # only codex sessions
|
|
|
526
577
|
npx getprismo watch claude # only claude code sessions
|
|
527
578
|
```
|
|
528
579
|
|
|
580
|
+
### shield mode
|
|
581
|
+
|
|
582
|
+
```bash
|
|
583
|
+
npx getprismo shield -- npm test
|
|
584
|
+
npx getprismo shield -- pytest -q
|
|
585
|
+
npx getprismo shield --json -- npm run build
|
|
586
|
+
npx getprismo shield last
|
|
587
|
+
npx getprismo shield search "auth failure"
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### mcp mode
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
npx getprismo mcp
|
|
594
|
+
npx getprismo mcp /path/to/repo
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
`mcp` starts a local stdio MCP server for agent clients. It exposes:
|
|
598
|
+
|
|
599
|
+
- `prismo_scan`
|
|
600
|
+
- `prismo_doctor_dry_run`
|
|
601
|
+
- `prismo_watch_snapshot`
|
|
602
|
+
- `prismo_shield_run`
|
|
603
|
+
- `prismo_shield_search`
|
|
604
|
+
- `prismo_shield_last`
|
|
605
|
+
- `prismo_context_pack`
|
|
606
|
+
- `prismo_firewall`
|
|
607
|
+
- `prismo_cc_timeline`
|
|
608
|
+
|
|
609
|
+
This lets an MCP-compatible agent search prior shielded test/build output, request scoped context packs, or inspect token-waste signals without pasting giant logs into the conversation.
|
|
610
|
+
|
|
611
|
+
Generic MCP client config:
|
|
612
|
+
|
|
613
|
+
```json
|
|
614
|
+
{
|
|
615
|
+
"mcpServers": {
|
|
616
|
+
"prismodev": {
|
|
617
|
+
"command": "npx",
|
|
618
|
+
"args": ["-y", "getprismo", "mcp", "/path/to/your/repo"]
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
For local development from this repo:
|
|
625
|
+
|
|
626
|
+
```json
|
|
627
|
+
{
|
|
628
|
+
"mcpServers": {
|
|
629
|
+
"prismodev": {
|
|
630
|
+
"command": "node",
|
|
631
|
+
"args": ["/path/to/prismodev/bin/prismo.js", "mcp", "/path/to/your/repo"]
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
529
637
|
---
|
|
530
638
|
|
|
531
639
|
## cc modes
|
|
@@ -671,8 +779,10 @@ lib/prismo-dev/constants.js shared defaults, pricing, patterns
|
|
|
671
779
|
lib/prismo-dev/context-optimize.js context packs, scoped prompts
|
|
672
780
|
lib/prismo-dev/doctor.js doctor/dev/init orchestration
|
|
673
781
|
lib/prismo-dev/fixes.js safe ignore/template generation
|
|
782
|
+
lib/prismo-dev/mcp.js local MCP server and Prismo tool bindings
|
|
674
783
|
lib/prismo-dev/report.js terminal, markdown, ci reports
|
|
675
784
|
lib/prismo-dev/scan.js repo scanning, scoring, readiness
|
|
785
|
+
lib/prismo-dev/shield.js local command shield and searchable output index
|
|
676
786
|
lib/prismo-dev/usage-watch.js local logs, watch, cost, timeline
|
|
677
787
|
```
|
|
678
788
|
|
|
@@ -682,8 +792,11 @@ lib/prismo-dev/usage-watch.js local logs, watch, cost, timeline
|
|
|
682
792
|
|
|
683
793
|
```bash
|
|
684
794
|
npx getprismo --help
|
|
795
|
+
npx getprismo --version
|
|
685
796
|
npx getprismo doctor --help
|
|
686
797
|
npx getprismo watch --help
|
|
798
|
+
npx getprismo shield --help
|
|
799
|
+
npx getprismo mcp --help
|
|
687
800
|
npx getprismo cc --help
|
|
688
801
|
npx getprismo scan --help
|
|
689
802
|
```
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
function createTextResult(payload) {
|
|
2
|
+
const text = typeof payload === "string" ? payload : JSON.stringify(payload, null, 2);
|
|
3
|
+
return {
|
|
4
|
+
content: [{ type: "text", text }],
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function normalizeArgs(args) {
|
|
9
|
+
return args && typeof args === "object" ? args : {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function makeTool(name, description, properties = {}, required = []) {
|
|
13
|
+
return {
|
|
14
|
+
name,
|
|
15
|
+
description,
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties,
|
|
19
|
+
required,
|
|
20
|
+
additionalProperties: false,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createMcpTools(deps) {
|
|
26
|
+
const {
|
|
27
|
+
rootDir,
|
|
28
|
+
scanRepo,
|
|
29
|
+
toJsonPayload,
|
|
30
|
+
runDoctor,
|
|
31
|
+
toDoctorJsonPayload,
|
|
32
|
+
getUsageSummary,
|
|
33
|
+
getClaudeCodeCostSummary,
|
|
34
|
+
runOptimize,
|
|
35
|
+
createOptimizeContext,
|
|
36
|
+
renderStarterPrompt,
|
|
37
|
+
runFirewall,
|
|
38
|
+
runShield,
|
|
39
|
+
runShieldLast,
|
|
40
|
+
runShieldSearch,
|
|
41
|
+
} = deps;
|
|
42
|
+
|
|
43
|
+
const pathProperty = {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "Repository path. Defaults to the repo used when the MCP server started.",
|
|
46
|
+
};
|
|
47
|
+
const limitProperty = {
|
|
48
|
+
type: "number",
|
|
49
|
+
description: "Maximum number of recent sessions or runs to inspect.",
|
|
50
|
+
};
|
|
51
|
+
const scopeProperty = {
|
|
52
|
+
type: "string",
|
|
53
|
+
description: "Optional scope such as frontend, backend, auth, tests, billing, or routing.",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const tools = [
|
|
57
|
+
makeTool("prismo_scan", "Scan a repo for AI coding token/context waste.", {
|
|
58
|
+
path: pathProperty,
|
|
59
|
+
includeUsage: { type: "boolean", description: "Include local Claude/Codex usage logs when available." },
|
|
60
|
+
limit: limitProperty,
|
|
61
|
+
}),
|
|
62
|
+
makeTool("prismo_doctor_dry_run", "Preview PrismoDev doctor fixes and before/after payoff without writing files.", {
|
|
63
|
+
path: pathProperty,
|
|
64
|
+
scope: scopeProperty,
|
|
65
|
+
limit: limitProperty,
|
|
66
|
+
}),
|
|
67
|
+
makeTool("prismo_watch_snapshot", "Return a one-shot local session/context pressure snapshot.", {
|
|
68
|
+
path: pathProperty,
|
|
69
|
+
tool: { type: "string", enum: ["all", "codex", "claude"], description: "Which local session logs to inspect." },
|
|
70
|
+
limit: limitProperty,
|
|
71
|
+
}),
|
|
72
|
+
makeTool("prismo_shield_run", "Run a noisy command through Prismo shield and store full output locally.", {
|
|
73
|
+
path: pathProperty,
|
|
74
|
+
command: {
|
|
75
|
+
type: "array",
|
|
76
|
+
items: { type: "string" },
|
|
77
|
+
description: "Command argv array, for example [\"npm\", \"test\"].",
|
|
78
|
+
},
|
|
79
|
+
}, ["command"]),
|
|
80
|
+
makeTool("prismo_shield_search", "Search stored shield stdout/stderr without loading full logs into context.", {
|
|
81
|
+
path: pathProperty,
|
|
82
|
+
query: { type: "string", description: "Text to search for in stored shield output." },
|
|
83
|
+
limit: limitProperty,
|
|
84
|
+
}, ["query"]),
|
|
85
|
+
makeTool("prismo_shield_last", "List recent shielded command runs.", {
|
|
86
|
+
path: pathProperty,
|
|
87
|
+
limit: limitProperty,
|
|
88
|
+
}),
|
|
89
|
+
makeTool("prismo_context_pack", "Generate or preview a scoped Prismo context pack/starter prompt.", {
|
|
90
|
+
path: pathProperty,
|
|
91
|
+
scope: scopeProperty,
|
|
92
|
+
dryRun: { type: "boolean", description: "When true, do not write .prismo files." },
|
|
93
|
+
}),
|
|
94
|
+
makeTool("prismo_firewall", "Generate a scoped context firewall policy for a task.", {
|
|
95
|
+
path: pathProperty,
|
|
96
|
+
task: { type: "string", description: "Task description such as auth-bug or frontend-test-failure." },
|
|
97
|
+
scope: scopeProperty,
|
|
98
|
+
dryRun: { type: "boolean", description: "When true, preview allowed/blocked context without writing files." },
|
|
99
|
+
}),
|
|
100
|
+
makeTool("prismo_cc_timeline", "Return the latest Claude Code session timeline/postmortem data.", {
|
|
101
|
+
path: pathProperty,
|
|
102
|
+
limit: limitProperty,
|
|
103
|
+
}),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
function resolveRoot(args) {
|
|
107
|
+
return args.path || rootDir || process.cwd();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function callTool(name, rawArgs) {
|
|
111
|
+
const args = normalizeArgs(rawArgs);
|
|
112
|
+
const target = resolveRoot(args);
|
|
113
|
+
|
|
114
|
+
if (name === "prismo_scan") {
|
|
115
|
+
return createTextResult(toJsonPayload(scanRepo(target, {
|
|
116
|
+
includeUsage: Boolean(args.includeUsage),
|
|
117
|
+
usageLimit: Number(args.limit) || 5,
|
|
118
|
+
})));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (name === "prismo_doctor_dry_run") {
|
|
122
|
+
const result = runDoctor(target, {
|
|
123
|
+
dryRun: true,
|
|
124
|
+
scope: args.scope || null,
|
|
125
|
+
limit: Number(args.limit) || 3,
|
|
126
|
+
});
|
|
127
|
+
return createTextResult(toDoctorJsonPayload(result));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (name === "prismo_watch_snapshot") {
|
|
131
|
+
const summary = getUsageSummary({
|
|
132
|
+
cwd: target,
|
|
133
|
+
limit: Number(args.limit) || 3,
|
|
134
|
+
usageTool: args.tool || "all",
|
|
135
|
+
});
|
|
136
|
+
return createTextResult(summary);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (name === "prismo_shield_run") {
|
|
140
|
+
return createTextResult(runShield(target, args.command));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (name === "prismo_shield_search") {
|
|
144
|
+
return createTextResult(runShieldSearch(target, args.query, { limit: Number(args.limit) || 5 }));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (name === "prismo_shield_last") {
|
|
148
|
+
return createTextResult(runShieldLast(target, { limit: Number(args.limit) || 5 }));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (name === "prismo_context_pack") {
|
|
152
|
+
const scope = args.scope || null;
|
|
153
|
+
const result = runOptimize(target, { scope, dryRun: args.dryRun !== false });
|
|
154
|
+
const context = createOptimizeContext(target, scope);
|
|
155
|
+
return createTextResult({
|
|
156
|
+
...result,
|
|
157
|
+
starterPrompt: renderStarterPrompt(context, scope),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (name === "prismo_firewall") {
|
|
162
|
+
return createTextResult(runFirewall(target, {
|
|
163
|
+
task: args.task || args.scope || "general",
|
|
164
|
+
scope: args.scope || null,
|
|
165
|
+
dryRun: args.dryRun !== false,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (name === "prismo_cc_timeline") {
|
|
170
|
+
return createTextResult(getClaudeCodeCostSummary({
|
|
171
|
+
cwd: target,
|
|
172
|
+
limit: Number(args.limit) || 1,
|
|
173
|
+
mode: "timeline",
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
throw new Error(`Unknown MCP tool: ${name}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return { tools, callTool };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function runMcpServer(deps) {
|
|
184
|
+
const readline = require("readline");
|
|
185
|
+
const { packageVersion = "0.0.0" } = deps;
|
|
186
|
+
const { tools, callTool } = createMcpTools(deps);
|
|
187
|
+
const rl = readline.createInterface({
|
|
188
|
+
input: process.stdin,
|
|
189
|
+
output: process.stdout,
|
|
190
|
+
terminal: false,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
function send(message) {
|
|
194
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function handle(message) {
|
|
198
|
+
if (!message || !message.method) return;
|
|
199
|
+
if (!Object.prototype.hasOwnProperty.call(message, "id")) return;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
if (message.method === "initialize") {
|
|
203
|
+
send({
|
|
204
|
+
jsonrpc: "2.0",
|
|
205
|
+
id: message.id,
|
|
206
|
+
result: {
|
|
207
|
+
protocolVersion: "2024-11-05",
|
|
208
|
+
capabilities: { tools: {} },
|
|
209
|
+
serverInfo: { name: "prismodev", version: packageVersion },
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (message.method === "tools/list") {
|
|
216
|
+
send({ jsonrpc: "2.0", id: message.id, result: { tools } });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (message.method === "tools/call") {
|
|
221
|
+
const params = normalizeArgs(message.params);
|
|
222
|
+
const result = await callTool(params.name, params.arguments);
|
|
223
|
+
send({ jsonrpc: "2.0", id: message.id, result });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
send({
|
|
228
|
+
jsonrpc: "2.0",
|
|
229
|
+
id: message.id,
|
|
230
|
+
error: { code: -32601, message: `Method not found: ${message.method}` },
|
|
231
|
+
});
|
|
232
|
+
} catch (error) {
|
|
233
|
+
send({
|
|
234
|
+
jsonrpc: "2.0",
|
|
235
|
+
id: message.id,
|
|
236
|
+
error: {
|
|
237
|
+
code: -32000,
|
|
238
|
+
message: error && error.message ? error.message : String(error),
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
rl.on("line", (line) => {
|
|
245
|
+
if (!line.trim()) return;
|
|
246
|
+
try {
|
|
247
|
+
handle(JSON.parse(line));
|
|
248
|
+
} catch (error) {
|
|
249
|
+
send({
|
|
250
|
+
jsonrpc: "2.0",
|
|
251
|
+
id: null,
|
|
252
|
+
error: {
|
|
253
|
+
code: -32700,
|
|
254
|
+
message: error && error.message ? error.message : String(error),
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = {
|
|
262
|
+
createMcpTools,
|
|
263
|
+
runMcpServer,
|
|
264
|
+
};
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
module.exports = function createShield(deps) {
|
|
2
|
+
const {
|
|
3
|
+
fs,
|
|
4
|
+
path,
|
|
5
|
+
estimateTokens,
|
|
6
|
+
formatBytes,
|
|
7
|
+
color,
|
|
8
|
+
} = deps;
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const SHIELD_SCHEMA_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
function nowId() {
|
|
14
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ensureDir(dir) {
|
|
18
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function pickInterestingLines(text, limit = 24) {
|
|
22
|
+
const lines = String(text || "").split(/\r?\n/).filter(Boolean);
|
|
23
|
+
const interesting = [];
|
|
24
|
+
const patterns = [
|
|
25
|
+
/\b(error|failed|failure|exception|traceback|fatal|panic|segmentation fault)\b/i,
|
|
26
|
+
/\b(warn|warning|deprecated|timeout|denied|unauthorized|forbidden)\b/i,
|
|
27
|
+
/\b(assert|expected|received|diff|not found|cannot find|module not found)\b/i,
|
|
28
|
+
];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
if (patterns.some((pattern) => pattern.test(line))) interesting.push(line.slice(0, 500));
|
|
31
|
+
if (interesting.length >= limit) break;
|
|
32
|
+
}
|
|
33
|
+
if (interesting.length) return interesting;
|
|
34
|
+
return lines.slice(Math.max(0, lines.length - Math.min(limit, 12))).map((line) => line.slice(0, 500));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function summarizeOutput(stdout, stderr) {
|
|
38
|
+
const stdoutText = String(stdout || "");
|
|
39
|
+
const stderrText = String(stderr || "");
|
|
40
|
+
const combined = [stderrText, stdoutText].filter(Boolean).join("\n");
|
|
41
|
+
const totalBytes = Buffer.byteLength(stdoutText) + Buffer.byteLength(stderrText);
|
|
42
|
+
const totalTokens = estimateTokens(combined);
|
|
43
|
+
return {
|
|
44
|
+
stdoutBytes: Buffer.byteLength(stdoutText),
|
|
45
|
+
stderrBytes: Buffer.byteLength(stderrText),
|
|
46
|
+
totalBytes,
|
|
47
|
+
estimatedTokens: totalTokens,
|
|
48
|
+
interestingLines: pickInterestingLines(combined),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function renderStoredReadCommand(pathValue) {
|
|
53
|
+
return `sed -n '1,160p' ${JSON.stringify(pathValue)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function shieldRoot(root) {
|
|
57
|
+
return path.join(root, ".prismo", "shield");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shieldDbPath(root) {
|
|
61
|
+
return path.join(shieldRoot(root), "shield.sqlite");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function indexPath(root) {
|
|
65
|
+
return path.join(shieldRoot(root), "index.jsonl");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sqlString(value) {
|
|
69
|
+
return `'${String(value == null ? "" : value).replace(/'/g, "''")}'`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sqliteAvailable() {
|
|
73
|
+
const result = spawnSync("sqlite3", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
74
|
+
return result.status === 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sqliteExec(dbPath, sql, json = false) {
|
|
78
|
+
const args = json ? ["-json", dbPath, sql] : [dbPath, sql];
|
|
79
|
+
const result = spawnSync("sqlite3", args, { encoding: "utf8", maxBuffer: 20 * 1024 * 1024 });
|
|
80
|
+
if (result.status !== 0) {
|
|
81
|
+
throw new Error((result.stderr || result.error?.message || "sqlite3 failed").trim());
|
|
82
|
+
}
|
|
83
|
+
if (!json) return null;
|
|
84
|
+
const out = String(result.stdout || "").trim();
|
|
85
|
+
return out ? JSON.parse(out) : [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function ensureSqliteIndex(root) {
|
|
89
|
+
if (!sqliteAvailable()) return { available: false, reason: "sqlite3-not-found" };
|
|
90
|
+
ensureDir(shieldRoot(root));
|
|
91
|
+
const dbPath = shieldDbPath(root);
|
|
92
|
+
sqliteExec(dbPath, `
|
|
93
|
+
PRAGMA journal_mode=WAL;
|
|
94
|
+
CREATE TABLE IF NOT EXISTS shield_runs (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
command TEXT NOT NULL,
|
|
97
|
+
cwd TEXT NOT NULL,
|
|
98
|
+
exit_code INTEGER NOT NULL,
|
|
99
|
+
started_at TEXT NOT NULL,
|
|
100
|
+
finished_at TEXT NOT NULL,
|
|
101
|
+
duration_ms INTEGER NOT NULL,
|
|
102
|
+
stdout_path TEXT NOT NULL,
|
|
103
|
+
stderr_path TEXT NOT NULL,
|
|
104
|
+
total_bytes INTEGER NOT NULL,
|
|
105
|
+
estimated_tokens INTEGER NOT NULL,
|
|
106
|
+
summary_json TEXT NOT NULL
|
|
107
|
+
);
|
|
108
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS shield_output USING fts5(
|
|
109
|
+
run_id UNINDEXED,
|
|
110
|
+
command,
|
|
111
|
+
stream UNINDEXED,
|
|
112
|
+
content,
|
|
113
|
+
tokenize='unicode61'
|
|
114
|
+
);
|
|
115
|
+
`);
|
|
116
|
+
return { available: true, path: path.relative(root, dbPath).replace(/\\/g, "/") };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function indexShieldRun(root, payload, stdout, stderr) {
|
|
120
|
+
const sqlite = ensureSqliteIndex(root);
|
|
121
|
+
if (!sqlite.available) return { mode: "jsonl", sqlite };
|
|
122
|
+
const dbPath = shieldDbPath(root);
|
|
123
|
+
const runId = path.basename(payload.stored.directory);
|
|
124
|
+
sqliteExec(dbPath, `
|
|
125
|
+
INSERT OR REPLACE INTO shield_runs (
|
|
126
|
+
id, command, cwd, exit_code, started_at, finished_at, duration_ms,
|
|
127
|
+
stdout_path, stderr_path, total_bytes, estimated_tokens, summary_json
|
|
128
|
+
) VALUES (
|
|
129
|
+
${sqlString(runId)},
|
|
130
|
+
${sqlString(payload.command)},
|
|
131
|
+
${sqlString(payload.cwd)},
|
|
132
|
+
${Number(payload.exitCode || 0)},
|
|
133
|
+
${sqlString(payload.startedAt)},
|
|
134
|
+
${sqlString(payload.finishedAt)},
|
|
135
|
+
${Number(payload.durationMs || 0)},
|
|
136
|
+
${sqlString(payload.stored.stdout)},
|
|
137
|
+
${sqlString(payload.stored.stderr)},
|
|
138
|
+
${Number(payload.output.totalBytes || 0)},
|
|
139
|
+
${Number(payload.output.estimatedTokens || 0)},
|
|
140
|
+
${sqlString(JSON.stringify(payload))}
|
|
141
|
+
);
|
|
142
|
+
DELETE FROM shield_output WHERE run_id = ${sqlString(runId)};
|
|
143
|
+
INSERT INTO shield_output (run_id, command, stream, content)
|
|
144
|
+
VALUES (${sqlString(runId)}, ${sqlString(payload.command)}, 'stdout', ${sqlString(stdout)});
|
|
145
|
+
INSERT INTO shield_output (run_id, command, stream, content)
|
|
146
|
+
VALUES (${sqlString(runId)}, ${sqlString(payload.command)}, 'stderr', ${sqlString(stderr)});
|
|
147
|
+
`);
|
|
148
|
+
return { mode: "sqlite-fts5", sqlite };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readJsonlIndex(root) {
|
|
152
|
+
const filePath = indexPath(root);
|
|
153
|
+
if (!fs.existsSync(filePath)) return [];
|
|
154
|
+
return fs.readFileSync(filePath, "utf8")
|
|
155
|
+
.split(/\r?\n/)
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.map((line) => {
|
|
158
|
+
try {
|
|
159
|
+
return JSON.parse(line);
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
.filter(Boolean);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function readStoredText(root, relPath) {
|
|
168
|
+
const fullPath = path.join(root, relPath);
|
|
169
|
+
try {
|
|
170
|
+
return fs.readFileSync(fullPath, "utf8");
|
|
171
|
+
} catch {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function fallbackSearch(root, query, limit = 10) {
|
|
177
|
+
const needle = String(query || "").toLowerCase();
|
|
178
|
+
if (!needle) return [];
|
|
179
|
+
const rows = readJsonlIndex(root).reverse();
|
|
180
|
+
const results = [];
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
for (const stream of ["stderr", "stdout"]) {
|
|
183
|
+
const relPath = row.stored?.[stream];
|
|
184
|
+
const text = readStoredText(root, relPath || "");
|
|
185
|
+
const lower = text.toLowerCase();
|
|
186
|
+
const index = lower.indexOf(needle);
|
|
187
|
+
if (index < 0) continue;
|
|
188
|
+
const start = Math.max(0, index - 180);
|
|
189
|
+
const end = Math.min(text.length, index + needle.length + 320);
|
|
190
|
+
results.push({
|
|
191
|
+
runId: path.basename(row.stored.directory),
|
|
192
|
+
command: row.command,
|
|
193
|
+
stream,
|
|
194
|
+
path: relPath,
|
|
195
|
+
exitCode: row.exitCode,
|
|
196
|
+
finishedAt: row.finishedAt,
|
|
197
|
+
snippet: text.slice(start, end).replace(/\s+/g, " ").trim(),
|
|
198
|
+
});
|
|
199
|
+
if (results.length >= limit) return results;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return results;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function runShieldSearch(rootDir = process.cwd(), query = "", options = {}) {
|
|
206
|
+
const root = path.resolve(rootDir);
|
|
207
|
+
const limit = options.limit || 10;
|
|
208
|
+
if (!String(query || "").trim()) throw new Error("No search query provided. Use: prismo shield search \"error text\"");
|
|
209
|
+
const sqlite = ensureSqliteIndex(root);
|
|
210
|
+
if (sqlite.available && fs.existsSync(shieldDbPath(root))) {
|
|
211
|
+
try {
|
|
212
|
+
const phrase = `"${String(query).replace(/"/g, '""')}"`;
|
|
213
|
+
const rows = sqliteExec(shieldDbPath(root), `
|
|
214
|
+
SELECT
|
|
215
|
+
shield_output.run_id AS runId,
|
|
216
|
+
shield_output.command AS command,
|
|
217
|
+
shield_output.stream AS stream,
|
|
218
|
+
CASE shield_output.stream
|
|
219
|
+
WHEN 'stderr' THEN shield_runs.stderr_path
|
|
220
|
+
ELSE shield_runs.stdout_path
|
|
221
|
+
END AS path,
|
|
222
|
+
shield_runs.exit_code AS exitCode,
|
|
223
|
+
shield_runs.finished_at AS finishedAt,
|
|
224
|
+
snippet(shield_output, 3, '[', ']', ' ... ', 24) AS snippet
|
|
225
|
+
FROM shield_output
|
|
226
|
+
JOIN shield_runs ON shield_runs.id = shield_output.run_id
|
|
227
|
+
WHERE shield_output MATCH ${sqlString(phrase)}
|
|
228
|
+
ORDER BY rank
|
|
229
|
+
LIMIT ${Number(limit)};
|
|
230
|
+
`, true);
|
|
231
|
+
return { query, mode: "sqlite-fts5", results: rows };
|
|
232
|
+
} catch {
|
|
233
|
+
// FTS queries can reject punctuation-heavy input; fall back to JSONL/plain text scan.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return { query, mode: "jsonl-fallback", results: fallbackSearch(root, query, limit) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function runShieldLast(rootDir = process.cwd(), options = {}) {
|
|
240
|
+
const root = path.resolve(rootDir);
|
|
241
|
+
const limit = options.limit || 5;
|
|
242
|
+
return {
|
|
243
|
+
mode: fs.existsSync(shieldDbPath(root)) ? "sqlite-fts5" : "jsonl-fallback",
|
|
244
|
+
runs: readJsonlIndex(root).reverse().slice(0, limit),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function runShield(rootDir = process.cwd(), commandArgs = [], options = {}) {
|
|
249
|
+
const root = path.resolve(rootDir);
|
|
250
|
+
if (!commandArgs.length) {
|
|
251
|
+
throw new Error("No command provided. Use: prismo shield -- npm test");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const startedAt = new Date();
|
|
255
|
+
const id = nowId();
|
|
256
|
+
const runsDir = path.join(root, ".prismo", "shield", "runs", id);
|
|
257
|
+
ensureDir(runsDir);
|
|
258
|
+
|
|
259
|
+
const command = commandArgs[0];
|
|
260
|
+
const args = commandArgs.slice(1);
|
|
261
|
+
const result = spawnSync(command, args, {
|
|
262
|
+
cwd: root,
|
|
263
|
+
encoding: "utf8",
|
|
264
|
+
maxBuffer: options.maxBuffer || 30 * 1024 * 1024,
|
|
265
|
+
shell: false,
|
|
266
|
+
env: process.env,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const finishedAt = new Date();
|
|
270
|
+
const stdout = result.stdout || "";
|
|
271
|
+
const stderr = result.stderr || "";
|
|
272
|
+
const stdoutPath = path.join(runsDir, "stdout.txt");
|
|
273
|
+
const stderrPath = path.join(runsDir, "stderr.txt");
|
|
274
|
+
const summary = summarizeOutput(stdout, stderr);
|
|
275
|
+
fs.writeFileSync(stdoutPath, stdout, "utf8");
|
|
276
|
+
fs.writeFileSync(stderrPath, stderr, "utf8");
|
|
277
|
+
|
|
278
|
+
const payload = {
|
|
279
|
+
schemaVersion: 1,
|
|
280
|
+
command: commandArgs.join(" "),
|
|
281
|
+
cwd: root,
|
|
282
|
+
exitCode: typeof result.status === "number" ? result.status : 1,
|
|
283
|
+
signal: result.signal || null,
|
|
284
|
+
error: result.error ? result.error.message : null,
|
|
285
|
+
startedAt: startedAt.toISOString(),
|
|
286
|
+
finishedAt: finishedAt.toISOString(),
|
|
287
|
+
durationMs: finishedAt.getTime() - startedAt.getTime(),
|
|
288
|
+
output: summary,
|
|
289
|
+
stored: {
|
|
290
|
+
stdout: path.relative(root, stdoutPath).replace(/\\/g, "/"),
|
|
291
|
+
stderr: path.relative(root, stderrPath).replace(/\\/g, "/"),
|
|
292
|
+
directory: path.relative(root, runsDir).replace(/\\/g, "/"),
|
|
293
|
+
},
|
|
294
|
+
next: [
|
|
295
|
+
"Feed the summary to the agent first; inspect full output only if needed.",
|
|
296
|
+
`Read stdout: ${renderStoredReadCommand(path.relative(root, stdoutPath).replace(/\\/g, "/"))}`,
|
|
297
|
+
`Read stderr: ${renderStoredReadCommand(path.relative(root, stderrPath).replace(/\\/g, "/"))}`,
|
|
298
|
+
],
|
|
299
|
+
};
|
|
300
|
+
fs.writeFileSync(path.join(runsDir, "summary.json"), JSON.stringify(payload, null, 2), "utf8");
|
|
301
|
+
fs.mkdirSync(path.join(root, ".prismo", "shield"), { recursive: true });
|
|
302
|
+
fs.appendFileSync(path.join(root, ".prismo", "shield", "index.jsonl"), `${JSON.stringify(payload)}\n`, "utf8");
|
|
303
|
+
payload.index = indexShieldRun(root, payload, stdout, stderr);
|
|
304
|
+
fs.writeFileSync(path.join(runsDir, "summary.json"), JSON.stringify(payload, null, 2), "utf8");
|
|
305
|
+
return payload;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function renderShieldTerminal(result) {
|
|
309
|
+
const lines = [];
|
|
310
|
+
const exitTone = result.exitCode === 0 ? "green" : "red";
|
|
311
|
+
lines.push("");
|
|
312
|
+
lines.push(color("Prismo Shield", "bold"));
|
|
313
|
+
lines.push("");
|
|
314
|
+
lines.push(`Command: ${result.command}`);
|
|
315
|
+
lines.push(`Exit: ${color(String(result.exitCode), exitTone)}`);
|
|
316
|
+
lines.push(`Duration: ${result.durationMs}ms`);
|
|
317
|
+
lines.push(`Captured: ${formatBytes(result.output.totalBytes)} (~${result.output.estimatedTokens.toLocaleString()} tokens kept out of chat)`);
|
|
318
|
+
lines.push("");
|
|
319
|
+
lines.push("Full Output Stored:");
|
|
320
|
+
lines.push(`- ${result.stored.stdout}`);
|
|
321
|
+
lines.push(`- ${result.stored.stderr}`);
|
|
322
|
+
lines.push(`- ${result.stored.directory}/summary.json`);
|
|
323
|
+
lines.push("");
|
|
324
|
+
lines.push("Summary Returned To Context:");
|
|
325
|
+
result.output.interestingLines.slice(0, 24).forEach((line) => lines.push(`- ${line}`));
|
|
326
|
+
lines.push("");
|
|
327
|
+
lines.push("Next:");
|
|
328
|
+
lines.push("1. Give the agent this summary first.");
|
|
329
|
+
lines.push("2. Inspect the stored full output only if the summary is not enough.");
|
|
330
|
+
lines.push(`3. ${renderStoredReadCommand(result.stored.stderr)}`);
|
|
331
|
+
return lines.join("\n");
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function renderShieldSearchTerminal(result) {
|
|
335
|
+
const lines = [];
|
|
336
|
+
lines.push("");
|
|
337
|
+
lines.push(color("Prismo Shield Search", "bold"));
|
|
338
|
+
lines.push("");
|
|
339
|
+
lines.push(`Query: ${result.query}`);
|
|
340
|
+
lines.push(`Index: ${result.mode}`);
|
|
341
|
+
lines.push(`Results: ${result.results.length}`);
|
|
342
|
+
lines.push("");
|
|
343
|
+
if (!result.results.length) {
|
|
344
|
+
lines.push("No matching shield output found.");
|
|
345
|
+
return lines.join("\n");
|
|
346
|
+
}
|
|
347
|
+
result.results.forEach((item, index) => {
|
|
348
|
+
lines.push(`${index + 1}. ${item.path} (${item.stream}, exit ${item.exitCode})`);
|
|
349
|
+
lines.push(` ${item.command}`);
|
|
350
|
+
lines.push(` ${String(item.snippet || "").slice(0, 700)}`);
|
|
351
|
+
});
|
|
352
|
+
lines.push("");
|
|
353
|
+
lines.push("Next:");
|
|
354
|
+
lines.push("Use the stored path above only if the snippet is not enough.");
|
|
355
|
+
return lines.join("\n");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function renderShieldLastTerminal(result) {
|
|
359
|
+
const lines = [];
|
|
360
|
+
lines.push("");
|
|
361
|
+
lines.push(color("Prismo Shield Last", "bold"));
|
|
362
|
+
lines.push("");
|
|
363
|
+
lines.push(`Index: ${result.mode}`);
|
|
364
|
+
if (!result.runs.length) {
|
|
365
|
+
lines.push("No shield runs found.");
|
|
366
|
+
return lines.join("\n");
|
|
367
|
+
}
|
|
368
|
+
result.runs.forEach((run, index) => {
|
|
369
|
+
lines.push(`${index + 1}. ${run.finishedAt} exit ${run.exitCode} ${run.command}`);
|
|
370
|
+
lines.push(` ${run.stored.directory}/summary.json`);
|
|
371
|
+
lines.push(` ${formatBytes(run.output.totalBytes)} (~${run.output.estimatedTokens.toLocaleString()} tokens kept out of chat)`);
|
|
372
|
+
});
|
|
373
|
+
return lines.join("\n");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
renderShieldLastTerminal,
|
|
378
|
+
renderShieldSearchTerminal,
|
|
379
|
+
renderShieldTerminal,
|
|
380
|
+
runShieldLast,
|
|
381
|
+
runShieldSearch,
|
|
382
|
+
runShield,
|
|
383
|
+
};
|
|
384
|
+
};
|
package/lib/prismo-dev-scan.js
CHANGED
|
@@ -3,6 +3,7 @@ const http = require("http");
|
|
|
3
3
|
const https = require("https");
|
|
4
4
|
const os = require("os");
|
|
5
5
|
const path = require("path");
|
|
6
|
+
const { version: PACKAGE_VERSION } = require("../package.json");
|
|
6
7
|
|
|
7
8
|
const {
|
|
8
9
|
HIGH_RISK_DIRS,
|
|
@@ -267,14 +268,36 @@ const {
|
|
|
267
268
|
writeGeneratedFile,
|
|
268
269
|
});
|
|
269
270
|
|
|
271
|
+
const {
|
|
272
|
+
renderShieldLastTerminal,
|
|
273
|
+
renderShieldSearchTerminal,
|
|
274
|
+
renderShieldTerminal,
|
|
275
|
+
runShieldLast,
|
|
276
|
+
runShieldSearch,
|
|
277
|
+
runShield,
|
|
278
|
+
} = require("./prismo-dev/shield")({
|
|
279
|
+
fs,
|
|
280
|
+
path,
|
|
281
|
+
estimateTokens,
|
|
282
|
+
formatBytes: (...args) => formatBytes(...args),
|
|
283
|
+
color,
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const { runMcpServer } = require("./prismo-dev/mcp");
|
|
287
|
+
|
|
270
288
|
function printHelp() {
|
|
271
289
|
console.log(`Prismo CLI
|
|
272
290
|
|
|
273
291
|
Usage:
|
|
292
|
+
prismo --version
|
|
274
293
|
prismo dev [path]
|
|
275
294
|
prismo init [--json] [--dry-run] [path]
|
|
276
295
|
prismo doctor [--json] [--dry-run] [--apply-ignores-only] [--no-context-packs] [--limit N] [path]
|
|
277
296
|
prismo firewall [task] [--json] [--dry-run] [path]
|
|
297
|
+
prismo shield [--json] [path] -- <command ...>
|
|
298
|
+
prismo shield last [--json] [--limit N] [path]
|
|
299
|
+
prismo shield search <query> [--json] [--limit N] [path]
|
|
300
|
+
prismo mcp [path]
|
|
278
301
|
prismo setup [--json] [--proxy-url URL] [path]
|
|
279
302
|
prismo scan [--fix] [--ci] [--json] [--usage] [--simple] [--no-report] [path]
|
|
280
303
|
prismo optimize [scope] [--json] [path]
|
|
@@ -289,6 +312,8 @@ Commands:
|
|
|
289
312
|
init Add local PrismoDev helper docs and npm scripts when package.json exists.
|
|
290
313
|
doctor Diagnose, safely optimize, re-scan, and show before/after payoff.
|
|
291
314
|
firewall Generate allowed/blocked context policy files for an AI coding task.
|
|
315
|
+
shield Run a noisy command, store full output locally, and return a compact summary.
|
|
316
|
+
mcp Start a local MCP server exposing Prismo tools over stdio.
|
|
292
317
|
scan Run PrismoDev for Claude Code, Codex, Cursor, and AI coding workflows.
|
|
293
318
|
optimize Generate lightweight AI-readable project context files in .prismo/.
|
|
294
319
|
context Print a copy-pasteable compact context prompt for AI coding tools.
|
|
@@ -475,6 +500,45 @@ Examples:
|
|
|
475
500
|
Output:
|
|
476
501
|
Writes .prismo/context-firewall.md, .prismo/allowed-context.txt, .prismo/blocked-context.txt, and .prismo/firewall-prompt.md.
|
|
477
502
|
The firewall is a local policy file for the agent to follow before reading files; it does not enforce filesystem access by itself.`,
|
|
503
|
+
shield: `Prismo Shield
|
|
504
|
+
|
|
505
|
+
Usage:
|
|
506
|
+
prismo shield [--json] [path] -- <command ...>
|
|
507
|
+
prismo shield last [--json] [--limit N] [path]
|
|
508
|
+
prismo shield search <query> [--json] [--limit N] [path]
|
|
509
|
+
|
|
510
|
+
Examples:
|
|
511
|
+
prismo shield -- npm test
|
|
512
|
+
prismo shield -- pytest -q
|
|
513
|
+
prismo shield --json -- npm run build
|
|
514
|
+
prismo shield last
|
|
515
|
+
prismo shield search "auth failure"
|
|
516
|
+
|
|
517
|
+
Output:
|
|
518
|
+
Runs the command locally, stores full stdout/stderr under .prismo/shield/runs/, indexes output in SQLite FTS5 when available, and prints only a compact summary plus useful error lines.
|
|
519
|
+
Search and last retrieve prior shield runs without re-feeding full output into agent context.`,
|
|
520
|
+
mcp: `Prismo MCP Server
|
|
521
|
+
|
|
522
|
+
Usage:
|
|
523
|
+
prismo mcp [path]
|
|
524
|
+
|
|
525
|
+
Examples:
|
|
526
|
+
prismo mcp
|
|
527
|
+
prismo mcp /path/to/repo
|
|
528
|
+
|
|
529
|
+
Tools exposed:
|
|
530
|
+
prismo_scan
|
|
531
|
+
prismo_doctor_dry_run
|
|
532
|
+
prismo_watch_snapshot
|
|
533
|
+
prismo_shield_run
|
|
534
|
+
prismo_shield_search
|
|
535
|
+
prismo_shield_last
|
|
536
|
+
prismo_context_pack
|
|
537
|
+
prismo_firewall
|
|
538
|
+
prismo_cc_timeline
|
|
539
|
+
|
|
540
|
+
Output:
|
|
541
|
+
Starts a local JSON-RPC MCP server over stdio. Use it from MCP-compatible clients so agents can scan context waste, search shielded command output, and request scoped context without loading huge logs into chat.`,
|
|
478
542
|
ci: `Prismo CI
|
|
479
543
|
|
|
480
544
|
Usage:
|
|
@@ -513,6 +577,10 @@ Good first command for people who want to see the value before scanning a repo.`
|
|
|
513
577
|
|
|
514
578
|
async function runCli(argv) {
|
|
515
579
|
const [command, ...rest] = argv;
|
|
580
|
+
if (command === "--version" || command === "-v" || command === "version") {
|
|
581
|
+
console.log(PACKAGE_VERSION);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
516
584
|
if (!command || command === "--help" || command === "-h") {
|
|
517
585
|
printHelp();
|
|
518
586
|
return;
|
|
@@ -521,8 +589,8 @@ async function runCli(argv) {
|
|
|
521
589
|
printCommandHelp(command);
|
|
522
590
|
return;
|
|
523
591
|
}
|
|
524
|
-
if (!["dev", "init", "doctor", "firewall", "setup", "scan", "optimize", "context", "cc", "usage", "watch", "demo"].includes(command)) {
|
|
525
|
-
throw new Error(`Unknown command: ${command}. Try: prismo doctor, prismo watch, prismo firewall, prismo init, prismo scan, prismo optimize, prismo context, prismo cc, or prismo usage`);
|
|
592
|
+
if (!["dev", "init", "doctor", "firewall", "shield", "mcp", "setup", "scan", "optimize", "context", "cc", "usage", "watch", "demo"].includes(command)) {
|
|
593
|
+
throw new Error(`Unknown command: ${command}. Try: prismo doctor, prismo watch, prismo shield, prismo mcp, prismo firewall, prismo init, prismo scan, prismo optimize, prismo context, prismo cc, or prismo usage`);
|
|
526
594
|
}
|
|
527
595
|
|
|
528
596
|
if (command === "demo") {
|
|
@@ -604,6 +672,62 @@ async function runCli(argv) {
|
|
|
604
672
|
return;
|
|
605
673
|
}
|
|
606
674
|
|
|
675
|
+
if (command === "shield") {
|
|
676
|
+
const json = rest.includes("--json");
|
|
677
|
+
const limitIndex = rest.indexOf("--limit");
|
|
678
|
+
const limit = parsePositiveInt(limitIndex >= 0 ? rest[limitIndex + 1] : null, 5);
|
|
679
|
+
const positional = getPositionals(rest, new Set(["--limit"]));
|
|
680
|
+
if (positional[0] === "last") {
|
|
681
|
+
const target = positional[1] || process.cwd();
|
|
682
|
+
const result = runShieldLast(target, { limit });
|
|
683
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
684
|
+
else console.log(renderShieldLastTerminal(result));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (positional[0] === "search") {
|
|
688
|
+
const query = positional[1];
|
|
689
|
+
const target = positional[2] || process.cwd();
|
|
690
|
+
const result = runShieldSearch(target, query, { limit });
|
|
691
|
+
if (json) console.log(JSON.stringify(result, null, 2));
|
|
692
|
+
else console.log(renderShieldSearchTerminal(result));
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const separatorIndex = rest.indexOf("--");
|
|
696
|
+
const beforeSeparator = separatorIndex >= 0 ? rest.slice(0, separatorIndex) : [];
|
|
697
|
+
const commandArgs = separatorIndex >= 0 ? rest.slice(separatorIndex + 1) : getPositionals(rest);
|
|
698
|
+
const target = getPositionals(beforeSeparator)[0] || process.cwd();
|
|
699
|
+
const result = runShield(target, commandArgs);
|
|
700
|
+
if (json) {
|
|
701
|
+
console.log(JSON.stringify(result, null, 2));
|
|
702
|
+
} else {
|
|
703
|
+
console.log(renderShieldTerminal(result));
|
|
704
|
+
}
|
|
705
|
+
process.exitCode = result.exitCode;
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (command === "mcp") {
|
|
710
|
+
const target = getPositionals(rest)[0] || process.cwd();
|
|
711
|
+
runMcpServer({
|
|
712
|
+
rootDir: path.resolve(target),
|
|
713
|
+
packageVersion: PACKAGE_VERSION,
|
|
714
|
+
scanRepo,
|
|
715
|
+
toJsonPayload,
|
|
716
|
+
runDoctor,
|
|
717
|
+
toDoctorJsonPayload,
|
|
718
|
+
getUsageSummary,
|
|
719
|
+
getClaudeCodeCostSummary,
|
|
720
|
+
runOptimize,
|
|
721
|
+
createOptimizeContext,
|
|
722
|
+
renderStarterPrompt,
|
|
723
|
+
runFirewall,
|
|
724
|
+
runShield,
|
|
725
|
+
runShieldLast,
|
|
726
|
+
runShieldSearch,
|
|
727
|
+
});
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
607
731
|
if (command === "setup") {
|
|
608
732
|
const json = rest.includes("--json");
|
|
609
733
|
const limitIndex = rest.indexOf("--limit");
|
package/package.json
CHANGED