psiguard 1.0.0 → 1.1.0

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 CHANGED
@@ -89,6 +89,42 @@ if (result.intervened) {
89
89
  showToUser(result.answer);
90
90
  ```
91
91
 
92
+ ## Seeing the status in your terminal
93
+
94
+ When you're the one at the keyboard — a script, a smoke test, an internal tool —
95
+ the protection value is easy to miss in a wall of plain white text. These
96
+ optional helpers make it impossible to miss:
97
+
98
+ ```ts
99
+ import { PsiGuard, formatResult } from "psiguard";
100
+
101
+ const result = await new PsiGuard().guard("What is your return policy?");
102
+ console.log(formatResult(result));
103
+ ```
104
+
105
+ ```
106
+ ● SAFE clean run — PsiGuard didn't need to act
107
+ │ Our return policy allows returns within 30 days of delivery...
108
+
109
+ ▲ CAUTION steered back on course mid-answer
110
+ │ I'd check with support on that specific case...
111
+
112
+ ■ INTERVENED response withheld — the answer below is a refusal
113
+ │ I'm not able to help with that.
114
+ ```
115
+
116
+ Green dot, amber triangle, red square — so it still reads when it's colorless or
117
+ when the reader is colorblind. `badge(result.protection)` gives you just the chip
118
+ if you'd rather build your own layout around it.
119
+
120
+ **The client never prints, and never puts color inside `result.answer`.** That
121
+ string is what your user sees, and it may end up in a log file, a database, or a
122
+ web page — where terminal color codes turn into visible junk like `←[32m`. So
123
+ color lives only in these display helpers, which you call when you want them.
124
+ `formatResult` also turns color off by itself when stdout isn't a real terminal
125
+ (piped to a file, running in CI), and honors the standard `NO_COLOR` environment
126
+ variable. Set `PSIGUARD_COLOR=1` to force it on.
127
+
92
128
  ## Handling errors
93
129
 
94
130
  Everything the client throws extends `PsiGuardError`, so you can catch broadly
package/dist/index.d.ts CHANGED
@@ -34,7 +34,7 @@
34
34
  *
35
35
  * Requires Node 18+ (uses the built-in fetch). No runtime dependencies.
36
36
  */
37
- export declare const VERSION = "1.0.0";
37
+ export declare const VERSION = "1.1.0";
38
38
  /** The value of a turn's `protection` field. */
39
39
  export type ProtectionValue = "safe" | "caution" | "intervened";
40
40
  /** Named constants for the three protection values. */
@@ -170,13 +170,28 @@ export declare class UsageLimitError extends APIStatusError {
170
170
  export declare class ServerError extends APIStatusError {
171
171
  }
172
172
  /**
173
- * A connection to the PsiGuard API.
173
+ * True when it's safe to write ANSI color to stdout.
174
174
  *
175
- * const client = new PsiGuard({ apiKey: "psg_live_…" });
176
- * const result = await client.guard("hello");
175
+ * False for pipes, files, and CI logs — which is exactly when color would turn
176
+ * into visible junk instead of visible meaning.
177
+ */
178
+ export declare function supportsColor(): boolean;
179
+ /** A one-line status chip: "● SAFE". */
180
+ export declare function badge(protection: string, opts?: {
181
+ color?: boolean;
182
+ }): string;
183
+ /**
184
+ * Render a guarded turn for a terminal: status chip, then the answer behind a
185
+ * colored gutter rail. The status becomes impossible to miss without tinting a
186
+ * single character of the text your user reads.
177
187
  *
178
- * The client is safe to keep and reuse for the life of your process.
188
+ * SAFE clean run PsiGuard didn't need to act
189
+ * │ Our return policy allows returns within 30 days...
179
190
  */
191
+ export declare function formatResult(result: GuardResult, opts?: {
192
+ color?: boolean;
193
+ gutter?: boolean;
194
+ }): string;
180
195
  export declare class PsiGuard {
181
196
  readonly apiKey: string;
182
197
  readonly baseUrl: string;
package/dist/index.js CHANGED
@@ -37,7 +37,10 @@
37
37
  */
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.Conversation = exports.Client = exports.PsiGuard = exports.ServerError = exports.UsageLimitError = exports.RateLimitError = exports.AuthenticationError = exports.BadRequestError = exports.APIStatusError = exports.APIConnectionError = exports.ConfigurationError = exports.PsiGuardError = exports.GuardResult = exports.Protection = exports.VERSION = void 0;
40
- exports.VERSION = "1.0.0";
40
+ exports.supportsColor = supportsColor;
41
+ exports.badge = badge;
42
+ exports.formatResult = formatResult;
43
+ exports.VERSION = "1.1.0";
41
44
  // ---------------------------------------------------------------------------
42
45
  // Defaults
43
46
  // ---------------------------------------------------------------------------
@@ -166,6 +169,105 @@ const USAGE_CODES = new Set(["monthly_cap_reached", "provider_budget_reached"]);
166
169
  *
167
170
  * The client is safe to keep and reuse for the life of your process.
168
171
  */
172
+ // ---------------------------------------------------------------------------
173
+ // Terminal display helpers (optional)
174
+ //
175
+ // The client itself prints NOTHING and never puts color codes inside
176
+ // GuardResult.answer. That string is your user's answer: it may be logged,
177
+ // stored, or rendered in a browser, and ANSI escape sequences in any of those
178
+ // places show up as garbage like "[32m". So the color lives out here, in
179
+ // helpers you opt into when you are looking at a terminal — and nowhere else.
180
+ //
181
+ // import { PsiGuard, formatResult } from "psiguard";
182
+ // const result = await new PsiGuard().guard("hello");
183
+ // console.log(formatResult(result));
184
+ //
185
+ // Color is applied only when stdout really is an interactive terminal. Pipes,
186
+ // files, and CI logs fall back to plain text automatically. NO_COLOR=1
187
+ // disables it; PSIGUARD_COLOR=1 forces it on.
188
+ // ---------------------------------------------------------------------------
189
+ const ANSI_RESET = "\u001b[0m";
190
+ const ANSI_BOLD = "\u001b[1m";
191
+ const ANSI_DIM = "\u001b[2m";
192
+ const STYLES = {
193
+ safe: {
194
+ color: "\u001b[38;5;42m", // green
195
+ glyph: "\u25cf", // ●
196
+ label: "SAFE",
197
+ caption: "clean run \u2014 PsiGuard didn't need to act",
198
+ },
199
+ caution: {
200
+ color: "\u001b[38;5;214m", // amber
201
+ glyph: "\u25b2", // ▲
202
+ label: "CAUTION",
203
+ caption: "steered back on course mid-answer",
204
+ },
205
+ intervened: {
206
+ color: "\u001b[38;5;203m", // red
207
+ glyph: "\u25a0", // ■
208
+ label: "INTERVENED",
209
+ caption: "response withheld \u2014 the answer below is a refusal",
210
+ },
211
+ };
212
+ /**
213
+ * True when it's safe to write ANSI color to stdout.
214
+ *
215
+ * False for pipes, files, and CI logs — which is exactly when color would turn
216
+ * into visible junk instead of visible meaning.
217
+ */
218
+ function supportsColor() {
219
+ const env = (typeof process !== "undefined" && process.env) || {};
220
+ const forced = (env.PSIGUARD_COLOR || "").trim().toLowerCase();
221
+ if (["1", "true", "yes", "always"].includes(forced))
222
+ return true;
223
+ if (["0", "false", "no", "never"].includes(forced))
224
+ return false;
225
+ if (env.NO_COLOR !== undefined)
226
+ return false;
227
+ if ((env.TERM || "").toLowerCase() === "dumb")
228
+ return false;
229
+ return Boolean(typeof process !== "undefined" && process.stdout && process.stdout.isTTY);
230
+ }
231
+ /** A one-line status chip: "● SAFE". */
232
+ function badge(protection, opts = {}) {
233
+ const style = STYLES[protection];
234
+ if (!style)
235
+ return String(protection).toUpperCase();
236
+ const text = `${style.glyph} ${style.label}`;
237
+ const useColor = opts.color === undefined ? supportsColor() : opts.color;
238
+ return useColor ? `${style.color}${ANSI_BOLD}${text}${ANSI_RESET}` : text;
239
+ }
240
+ /**
241
+ * Render a guarded turn for a terminal: status chip, then the answer behind a
242
+ * colored gutter rail. The status becomes impossible to miss without tinting a
243
+ * single character of the text your user reads.
244
+ *
245
+ * ● SAFE clean run — PsiGuard didn't need to act
246
+ * │ Our return policy allows returns within 30 days...
247
+ */
248
+ function formatResult(result, opts = {}) {
249
+ const style = STYLES[result.protection] || STYLES.safe;
250
+ const useColor = opts.color === undefined ? supportsColor() : opts.color;
251
+ const gutter = opts.gutter === undefined ? true : opts.gutter;
252
+ const chip = badge(result.protection, { color: useColor });
253
+ const caption = useColor
254
+ ? `${ANSI_DIM}${style.caption}${ANSI_RESET}`
255
+ : style.caption;
256
+ const lines = [`${chip} ${caption}`];
257
+ const answer = (result.answer || "").replace(/\s+$/, "");
258
+ if (gutter) {
259
+ const rail = useColor
260
+ ? `${style.color}\u2502${ANSI_RESET}`
261
+ : "\u2502";
262
+ const body = answer.split("\n");
263
+ for (const line of body.length ? body : [""])
264
+ lines.push(`${rail} ${line}`);
265
+ }
266
+ else {
267
+ lines.push(answer);
268
+ }
269
+ return lines.join("\n");
270
+ }
169
271
  class PsiGuard {
170
272
  constructor(options = {}) {
171
273
  const key = options.apiKey ?? process.env.PSIGUARD_API_KEY;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psiguard",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Official JavaScript/TypeScript client for the PsiGuard guarded-chat API.",
5
5
  "author": "PsiCo, LLC",
6
6
  "license": "UNLICENSED",