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 +48 -12
- package/bin/not-manage.js +15 -2
- package/package.json +1 -2
- package/src/agent-input.js +101 -0
- package/src/cli-errors.js +109 -0
- package/src/cli.js +835 -64
- package/src/clio-api.js +108 -6
- package/src/commands-agent-context.js +119 -0
- package/src/commands-auth.js +336 -148
- package/src/commands-doctor.js +178 -0
- package/src/commands-request.js +144 -0
- package/src/compact-output.js +89 -0
- package/src/prompt.js +48 -0
- package/src/redaction.js +62 -25
- package/src/resource-command-runner.js +75 -4
- package/src/resource-handlers.js +15 -6
- package/src/resource-metadata.js +18 -0
- package/src/store.js +1 -1
- package/src/postinstall.js +0 -140
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
|
-
|
|
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`
|
|
24
|
-
- returning setup: `not-manage` opens
|
|
25
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
6
|
-
|
|
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.
|
|
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
|
+
};
|