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 +48 -6
- package/bin/not-manage.js +15 -2
- package/package.json +1 -1
- 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/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
|
-
|
|
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`
|
|
24
|
-
- returning setup: `not-manage` opens
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
@@ -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
|
+
};
|