not-manage 0.2.1 → 0.2.3

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
@@ -16,13 +16,18 @@ This project is a terminal CLI from [Not Operations](https://notoperations.com/l
16
16
  npm i -g not-manage && not-manage
17
17
  ```
18
18
 
19
- This is the recommended install flow because npm does not always show postinstall output consistently.
19
+ The package does not run install-time scripts. `not-manage` opens in help-first mode so you can inspect commands before changing local state.
20
20
 
21
21
  What happens next:
22
22
 
23
- - first-time setup: `not-manage` starts guided setup
24
- - returning setup: `not-manage` opens normally and can verify the saved connection
25
- - install/setup warning: the CLI reminds you that output may contain confidential client data and that redaction is best-effort only
23
+ - first-time setup: run `not-manage auth setup` or `not-manage setup` when you are ready
24
+ - returning setup: `not-manage` opens command help and you can verify the saved connection explicitly
25
+ - setup warning: the CLI reminds you that output may contain confidential client data and that redaction is best-effort only
26
+
27
+ Network behavior:
28
+
29
+ - the CLI uses HTTPS requests to Clio API/auth hosts for your selected region (`app.clio.com`, `ca.app.clio.com`, `eu.app.clio.com`, or `au.app.clio.com`)
30
+ - during OAuth login it also accepts a local loopback callback on `127.0.0.1`
26
31
 
27
32
  You can also run the command separately after install:
28
33
 
@@ -36,11 +41,6 @@ or:
36
41
  not-manage setup
37
42
  ```
38
43
 
39
- To suppress the install-time prompt entirely:
40
-
41
- ```bash
42
- NOT_MANAGE_SKIP_POSTINSTALL_SETUP=1 npm i -g not-manage && not-manage
43
- ```
44
44
 
45
45
  For local development:
46
46
 
@@ -60,7 +60,7 @@ node bin/not-manage.js --help
60
60
  not-manage auth setup
61
61
  ```
62
62
 
63
- 2. Choose your Clio region in the CLI.
63
+ 2. Choose your Clio region in the CLI, or pass it directly with `--region`.
64
64
  3. When the CLI opens the Clio developer portal, sign in there.
65
65
  4. Open your Clio developer app there, or create one first if you do not have one yet.
66
66
  5. Fill out the Clio app form:
@@ -88,6 +88,12 @@ During setup, the CLI asks you to acknowledge that:
88
88
  - `--redacted` is best-effort only and may miss identifiers in labels, custom fields, or free text
89
89
  - you must review output before sharing it with AI tools or other third parties
90
90
 
91
+ For non-interactive setup, pass the required values directly:
92
+
93
+ ```bash
94
+ not-manage auth setup --confirm-confidentiality --region us --client-id <app-key> --client-secret <app-secret>
95
+ ```
96
+
91
97
  ## Local-only docs
92
98
 
93
99
  - [PRIVACY.md](PRIVACY.md)
@@ -106,13 +112,37 @@ During setup, the CLI asks you to acknowledge that:
106
112
 
107
113
  ```bash
108
114
  not-manage setup
115
+ not-manage doctor
116
+ not-manage agent-context
109
117
  not-manage auth setup
110
118
  not-manage auth login
111
119
  not-manage auth status
112
120
  not-manage whoami
113
- not-manage auth revoke
121
+ not-manage auth revoke --dry-run
122
+ not-manage auth revoke --yes
114
123
  ```
115
124
 
125
+ ## Agent-friendly output
126
+
127
+ Use `--agent` with any command to apply machine-friendly defaults:
128
+
129
+ ```bash
130
+ not-manage --agent contacts list --query "acme"
131
+ ```
132
+
133
+ `--agent` implies `--json`, `--compact`, `--no-input`, `--no-color`, and `--yes`. Use `--compact` by itself when you want reduced JSON fields without changing confirmation behavior.
134
+
135
+ `not-manage agent-context` emits a machine-readable command map for agents. `not-manage doctor --json` reports local setup, token state, and Clio connectivity; add `--fail-on warn` for CI-style checks.
136
+
137
+ Agents can also pass structured inputs without shell-escaping every filter:
138
+
139
+ ```bash
140
+ not-manage --agent matters list --options-file filters.json
141
+ printf "123\n456\n" | not-manage --agent contacts get --stdin
142
+ ```
143
+
144
+ `--options-file <path|->` merges a JSON object into resource command options, with explicit CLI flags taking precedence. `get` commands also accept `--ids-file <path|->` or `--stdin` for bulk read-only lookups.
145
+
116
146
  ## Resource command reference
117
147
 
118
148
  <!-- GENERATED:CLI_REFERENCE:start -->
@@ -218,8 +248,14 @@ not-manage matter get 456 --redacted
218
248
  - Supported data commands are redacted by default.
219
249
  - Add `--unredacted` to show raw output.
220
250
  - `--redacted` is still accepted for compatibility.
221
- - The first version redacts client/contact names, emails, phone numbers, and common PII patterns that appear inside free-text fields such as matter descriptions, activity notes, bill memos, and bill subjects.
251
+ - Redaction covers:
252
+ - client/contact names (full names, individual first and last names), emails, and phone numbers from structured fields
253
+ - pattern-based detection of emails, phone numbers, SSNs (dash and space-separated), tax IDs, and credit card numbers (standard 16-digit and American Express 15-digit formats) in all string fields
254
+ - person client surnames derived from matter labels and used to redact matter numbers, file names, and summaries
255
+ - significant tokens from company client names (filtering out noise like LLC, Inc, Corp) used to redact matter labels
256
+ - heuristic detection of bare 2-3 word person names in free-text and label fields
222
257
  - Internal staff fields such as `user`, `responsible_attorney`, `responsible_staff`, and `originating_attorney` remain visible.
258
+ - API error messages sanitize URLs to prevent leaking query parameters that may contain PII.
223
259
  - Redaction is best-effort only. Review output before sharing it outside your firm or with any AI or third-party service.
224
260
  - High-risk commands such as contacts, matters, activities, bills, invoices, tasks, and billable client or matter views emit additional review warnings.
225
261
  - `--unredacted` on those high-risk commands emits a stronger warning because raw output may include client-identifying, confidential, or privileged information.
package/bin/not-manage.js CHANGED
@@ -1,8 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { run } = require("../src/cli");
4
+ const { CliError, reportError } = require("../src/cli-errors");
4
5
 
5
- run(process.argv.slice(2)).catch((_error) => {
6
- console.error("Error: command failed.");
6
+ const args = process.argv.slice(2);
7
+ const jsonMode = args.includes("--agent") ||
8
+ args.includes("--json") ||
9
+ args.some((arg) => arg === "--json=true" || arg === "--agent=true");
10
+
11
+ run(args).catch((error) => {
12
+ if (error instanceof CliError) {
13
+ const exitCode = reportError(error, { json: jsonMode });
14
+ process.exit(exitCode);
15
+ return;
16
+ }
17
+
18
+ const message = error && error.message ? error.message : "Error: command failed.";
19
+ console.error(message);
7
20
  process.exit(1);
8
21
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "not-manage",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Unofficial command-line tool for Clio Manage integrations",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -39,7 +39,6 @@
39
39
  "docs:generate": "node scripts/generate-readme-cli-reference.js",
40
40
  "hooks:install": "node scripts/install-git-hooks.js",
41
41
  "pack:check": "npm pack --dry-run",
42
- "postinstall": "node src/postinstall.js",
43
42
  "seed:handbook-imports": "node scripts/generate-handbook-import-csvs.js",
44
43
  "seed:historical": "node scripts/seed-historical-data.js",
45
44
  "smoke:live": "node scripts/smoke-read-only-live.js",
@@ -0,0 +1,101 @@
1
+ const fs = require("node:fs/promises");
2
+
3
+ const { UsageError } = require("./cli-errors");
4
+
5
+ async function readTextInput(source, label) {
6
+ if (source === "-") {
7
+ return new Promise((resolve, reject) => {
8
+ let body = "";
9
+ process.stdin.setEncoding("utf8");
10
+ process.stdin.on("data", (chunk) => {
11
+ body += chunk;
12
+ });
13
+ process.stdin.on("end", () => resolve(body));
14
+ process.stdin.on("error", reject);
15
+ });
16
+ }
17
+
18
+ try {
19
+ return await fs.readFile(source, "utf8");
20
+ } catch (error) {
21
+ throw new UsageError(`Could not read ${label} from ${source}: ${error.message}`, {
22
+ code: "input_file_unreadable",
23
+ hint: "Use a readable file path, or `-` to read from stdin.",
24
+ });
25
+ }
26
+ }
27
+
28
+ async function readJsonInput(source, label) {
29
+ const text = await readTextInput(source, label);
30
+ try {
31
+ return JSON.parse(text);
32
+ } catch (error) {
33
+ throw new UsageError(`${label} must contain valid JSON: ${error.message}`, {
34
+ code: "invalid_json_input",
35
+ hint: "Pass a JSON object, for example `{ \"limit\": 5 }`.",
36
+ });
37
+ }
38
+ }
39
+
40
+ function collectIdsFromValue(value, ids) {
41
+ if (value === undefined || value === null) {
42
+ return;
43
+ }
44
+
45
+ if (Array.isArray(value)) {
46
+ value.forEach((item) => collectIdsFromValue(item, ids));
47
+ return;
48
+ }
49
+
50
+ if (typeof value === "object") {
51
+ if (value.id !== undefined && value.id !== null) {
52
+ collectIdsFromValue(value.id, ids);
53
+ }
54
+ return;
55
+ }
56
+
57
+ const text = String(value).trim();
58
+ if (text) {
59
+ ids.push(text);
60
+ }
61
+ }
62
+
63
+ function parseIdsText(text) {
64
+ const trimmed = String(text || "").trim();
65
+ if (!trimmed) {
66
+ return [];
67
+ }
68
+
69
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
70
+ const ids = [];
71
+ try {
72
+ collectIdsFromValue(JSON.parse(trimmed), ids);
73
+ return [...new Set(ids)];
74
+ } catch (_error) {
75
+ throw new UsageError("ID input must be newline-delimited IDs or JSON containing IDs.", {
76
+ code: "invalid_ids_input",
77
+ hint: "Use one ID per line, a JSON array of IDs, or objects with an `id` field.",
78
+ });
79
+ }
80
+ }
81
+
82
+ return [
83
+ ...new Set(
84
+ trimmed
85
+ .split(/[\r\n,]+/)
86
+ .map((line) => line.trim())
87
+ .filter(Boolean)
88
+ ),
89
+ ];
90
+ }
91
+
92
+ async function readIdsInput(source) {
93
+ return parseIdsText(await readTextInput(source, "ID input"));
94
+ }
95
+
96
+ module.exports = {
97
+ parseIdsText,
98
+ readIdsInput,
99
+ readJsonInput,
100
+ readTextInput,
101
+ };
@@ -0,0 +1,109 @@
1
+ const EXIT_CODES = {
2
+ unknown: 1,
3
+ usage: 2,
4
+ auth: 3,
5
+ network: 4,
6
+ api: 5,
7
+ redaction: 6,
8
+ };
9
+
10
+ class CliError extends Error {
11
+ constructor(message, options = {}) {
12
+ super(message);
13
+ this.name = "CliError";
14
+ this.category = options.category || "unknown";
15
+ this.code = options.code || this.category;
16
+ this.exitCode = options.exitCode || EXIT_CODES[this.category] || EXIT_CODES.unknown;
17
+ this.hint = options.hint;
18
+ this.docsUrl = options.docsUrl;
19
+ if (options.details && typeof options.details === "object") {
20
+ this.details = options.details;
21
+ }
22
+ }
23
+ }
24
+
25
+ class UsageError extends CliError {
26
+ constructor(message, options = {}) {
27
+ super(message, { ...options, category: "usage", exitCode: EXIT_CODES.usage });
28
+ this.name = "UsageError";
29
+ }
30
+ }
31
+
32
+ class AuthError extends CliError {
33
+ constructor(message, options = {}) {
34
+ super(message, { ...options, category: "auth", exitCode: EXIT_CODES.auth });
35
+ this.name = "AuthError";
36
+ }
37
+ }
38
+
39
+ class NetworkError extends CliError {
40
+ constructor(message, options = {}) {
41
+ super(message, { ...options, category: "network", exitCode: EXIT_CODES.network });
42
+ this.name = "NetworkError";
43
+ }
44
+ }
45
+
46
+ class ApiError extends CliError {
47
+ constructor(message, options = {}) {
48
+ super(message, { ...options, category: "api", exitCode: EXIT_CODES.api });
49
+ this.name = "ApiError";
50
+ }
51
+ }
52
+
53
+ class RedactionError extends CliError {
54
+ constructor(message, options = {}) {
55
+ super(message, { ...options, category: "redaction", exitCode: EXIT_CODES.redaction });
56
+ this.name = "RedactionError";
57
+ }
58
+ }
59
+
60
+ function toErrorEnvelope(error) {
61
+ const isCliError = error instanceof CliError;
62
+ const message = (error && error.message ? String(error.message) : "Command failed.").trim();
63
+ const category = isCliError ? error.category : "unknown";
64
+ const code = isCliError ? error.code : "unknown";
65
+ const exitCode = isCliError ? error.exitCode : EXIT_CODES.unknown;
66
+ const envelope = {
67
+ error: {
68
+ code,
69
+ category,
70
+ message,
71
+ exit_code: exitCode,
72
+ },
73
+ };
74
+ if (isCliError && error.hint) {
75
+ envelope.error.hint = error.hint;
76
+ }
77
+ if (isCliError && error.docsUrl) {
78
+ envelope.error.docs_url = error.docsUrl;
79
+ }
80
+ if (isCliError && error.details) {
81
+ envelope.error.details = error.details;
82
+ }
83
+ return envelope;
84
+ }
85
+
86
+ function reportError(error, { json = false, stderr = process.stderr } = {}) {
87
+ const envelope = toErrorEnvelope(error);
88
+ if (json) {
89
+ stderr.write(`${JSON.stringify(envelope)}\n`);
90
+ } else {
91
+ stderr.write(`${envelope.error.message}\n`);
92
+ if (envelope.error.hint) {
93
+ stderr.write(`Hint: ${envelope.error.hint}\n`);
94
+ }
95
+ }
96
+ return envelope.error.exit_code;
97
+ }
98
+
99
+ module.exports = {
100
+ ApiError,
101
+ AuthError,
102
+ CliError,
103
+ EXIT_CODES,
104
+ NetworkError,
105
+ RedactionError,
106
+ UsageError,
107
+ reportError,
108
+ toErrorEnvelope,
109
+ };