not-manage 0.2.2 → 0.2.4

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
@@ -12,16 +12,22 @@ This project is a terminal CLI from [Not Operations](https://notoperations.com/l
12
12
 
13
13
  ## Install
14
14
 
15
+ Before you run the install command, make sure your computer has:
16
+
17
+ - **Node.js 20 or newer**. Node.js is not installed by default on most computers. Install it from [nodejs.org](https://nodejs.org/); it includes the `npm` command used below.
18
+ - A working OS keychain: macOS Keychain, Windows Credential Manager, or a Linux Secret Service/keyring.
19
+ - A browser and internet access for the Clio login flow.
20
+
15
21
  ```bash
16
22
  npm i -g not-manage && not-manage
17
23
  ```
18
24
 
19
- The package does not run install-time scripts. Setup starts when you launch `not-manage`.
25
+ `not-manage` itself does not run install-time scripts. npm may still perform normal dependency installation, including the secure keychain dependency used to store credentials locally. `not-manage` opens in help-first mode so you can inspect commands before changing local state.
20
26
 
21
27
  What happens next:
22
28
 
23
- - first-time setup: `not-manage` starts guided setup
24
- - returning setup: `not-manage` opens normally and can verify the saved connection
29
+ - first-time setup: run `not-manage auth setup` or `not-manage setup` when you are ready
30
+ - returning setup: `not-manage` opens command help and you can verify the saved connection explicitly
25
31
  - setup warning: the CLI reminds you that output may contain confidential client data and that redaction is best-effort only
26
32
 
27
33
  Network behavior:
@@ -60,7 +66,7 @@ node bin/not-manage.js --help
60
66
  not-manage auth setup
61
67
  ```
62
68
 
63
- 2. Choose your Clio region in the CLI.
69
+ 2. Choose your Clio region in the CLI, or pass it directly with `--region`.
64
70
  3. When the CLI opens the Clio developer portal, sign in there.
65
71
  4. Open your Clio developer app there, or create one first if you do not have one yet.
66
72
  5. Fill out the Clio app form:
@@ -88,6 +94,12 @@ During setup, the CLI asks you to acknowledge that:
88
94
  - `--redacted` is best-effort only and may miss identifiers in labels, custom fields, or free text
89
95
  - you must review output before sharing it with AI tools or other third parties
90
96
 
97
+ For non-interactive setup, pass the required values directly:
98
+
99
+ ```bash
100
+ not-manage auth setup --confirm-confidentiality --region us --client-id <app-key> --client-secret <app-secret>
101
+ ```
102
+
91
103
  ## Local-only docs
92
104
 
93
105
  - [PRIVACY.md](PRIVACY.md)
@@ -106,13 +118,37 @@ During setup, the CLI asks you to acknowledge that:
106
118
 
107
119
  ```bash
108
120
  not-manage setup
121
+ not-manage doctor
122
+ not-manage agent-context
109
123
  not-manage auth setup
110
124
  not-manage auth login
111
125
  not-manage auth status
112
126
  not-manage whoami
113
- not-manage auth revoke
127
+ not-manage auth revoke --dry-run
128
+ not-manage auth revoke --yes
129
+ ```
130
+
131
+ ## Agent-friendly output
132
+
133
+ Use `--agent` with any command to apply machine-friendly defaults:
134
+
135
+ ```bash
136
+ not-manage --agent contacts list --query "acme"
114
137
  ```
115
138
 
139
+ `--agent` implies `--json`, `--compact`, `--no-input`, `--no-color`, and `--yes`. Use `--compact` by itself when you want reduced JSON fields without changing confirmation behavior.
140
+
141
+ `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.
142
+
143
+ Agents can also pass structured inputs without shell-escaping every filter:
144
+
145
+ ```bash
146
+ not-manage --agent matters list --options-file filters.json
147
+ printf "123\n456\n" | not-manage --agent contacts get --stdin
148
+ ```
149
+
150
+ `--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.
151
+
116
152
  ## Resource command reference
117
153
 
118
154
  <!-- GENERATED:CLI_REFERENCE:start -->
@@ -218,8 +254,14 @@ not-manage matter get 456 --redacted
218
254
  - Supported data commands are redacted by default.
219
255
  - Add `--unredacted` to show raw output.
220
256
  - `--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.
257
+ - Redaction covers:
258
+ - client/contact names (full names, individual first and last names), emails, and phone numbers from structured fields
259
+ - 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
260
+ - person client surnames derived from matter labels and used to redact matter numbers, file names, and summaries
261
+ - significant tokens from company client names (filtering out noise like LLC, Inc, Corp) used to redact matter labels
262
+ - heuristic detection of bare 2-3 word person names in free-text and label fields
222
263
  - Internal staff fields such as `user`, `responsible_attorney`, `responsible_staff`, and `originating_attorney` remain visible.
264
+ - API error messages sanitize URLs to prevent leaking query parameters that may contain PII.
223
265
  - Redaction is best-effort only. Review output before sharing it outside your firm or with any AI or third-party service.
224
266
  - High-risk commands such as contacts, matters, activities, bills, invoices, tasks, and billable client or matter views emit additional review warnings.
225
267
  - `--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.2",
3
+ "version": "0.2.4",
4
4
  "description": "Unofficial command-line tool for Clio Manage integrations",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -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
+ };