not-manage 0.1.18 → 0.2.1

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
@@ -48,6 +48,7 @@ For local development:
48
48
  git clone https://github.com/Not-Operations/not-manage.git
49
49
  cd not-manage
50
50
  npm install
51
+ npm run hooks:install
51
52
  node bin/not-manage.js --help
52
53
  ```
53
54
 
@@ -95,6 +96,11 @@ During setup, the CLI asks you to acknowledge that:
95
96
  - [SECURITY.md](SECURITY.md)
96
97
  - [TERMS.md](TERMS.md)
97
98
  - [OPERATIONS.md](OPERATIONS.md)
99
+ - [SUPPORT.md](SUPPORT.md)
100
+ - [CUSTOMER-TESTING.md](CUSTOMER-TESTING.md)
101
+ - [SUBPROCESSORS.md](SUBPROCESSORS.md)
102
+ - [MVSP-SELF-ASSESSMENT.md](MVSP-SELF-ASSESSMENT.md)
103
+ - [CLIO-APP-DIRECTORY-CHECKLIST.md](CLIO-APP-DIRECTORY-CHECKLIST.md)
98
104
 
99
105
  ## Core commands
100
106
 
@@ -103,34 +109,63 @@ not-manage setup
103
109
  not-manage auth setup
104
110
  not-manage auth login
105
111
  not-manage auth status
106
- not-manage activities list
107
- not-manage activity get 123
108
- not-manage tasks list
109
- not-manage task get 789
110
- not-manage contacts list
111
- not-manage contact get 12345
112
- not-manage time-entries list
113
- not-manage billable-clients list
114
- not-manage billable-matters list
115
- not-manage bills list
116
- not-manage bill get 987
117
- not-manage invoices list
118
- not-manage matters list
119
- not-manage matter get 456
120
- not-manage users list
121
- not-manage user get 123
122
- not-manage practice-areas list
123
- not-manage practice-area get 45
124
- not-manage matters list --status open --limit 50
125
- not-manage matters list --all --json
126
112
  not-manage whoami
127
113
  not-manage auth revoke
128
114
  ```
129
115
 
116
+ ## Resource command reference
117
+
118
+ <!-- GENERATED:CLI_REFERENCE:start -->
119
+ This table is generated from resource metadata. Global auth/setup commands stay hand-written.
120
+
121
+ | Command | Operations | Aliases | Required list filters |
122
+ | --- | --- | --- | --- |
123
+ | `activities` | `list`, `get` | `activity` | - |
124
+ | `calendar-entries` | `list`, `get` | `calendar-entry` | - |
125
+ | `reminders` | `list`, `get` | `reminder` | - |
126
+ | `tasks` | `list`, `get` | `task` | - |
127
+ | `contacts` | `list`, `get` | `contact` | - |
128
+ | `communications` | `list`, `get` | `communication` | - |
129
+ | `conversations` | `list`, `get` | `conversation` | - |
130
+ | `conversation-messages` | `list`, `get` | `conversation-message` | `--conversation-id` |
131
+ | `notes` | `list`, `get` | `note` | `--type` |
132
+ | `custom-fields` | `list`, `get` | `custom-field` | - |
133
+ | `time-entries` | `list`, `get` | `time-entry` | - |
134
+ | `billable-clients` | `list` | `billable-client` | - |
135
+ | `billable-matters` | `list` | `billable-matter` | - |
136
+ | `bills` | `list`, `get` | `bill` | - |
137
+ | `invoices` | `list`, `get` | `invoice` | - |
138
+ | `outstanding-client-balances` | `list` | `outstanding-client-balance` | - |
139
+ | `matters` | `list`, `get` | `matter` | - |
140
+ | `matter-dockets` | `list`, `get` | `matter-docket` | - |
141
+ | `users` | `list`, `get` | `user` | - |
142
+ | `practice-areas` | `list`, `get` | `practice-area` | - |
143
+ | `my-events` | `list` | `my-event` | - |
144
+
145
+ Required list filters are enforced by the CLI before it calls Clio.
146
+ <!-- GENERATED:CLI_REFERENCE:end -->
147
+
130
148
  Plural commands still work. Singular aliases are accepted for the single-record flows so `contact get 12345` and `contacts get 12345` both work.
131
149
 
132
150
  Every data command also accepts `--fields <comma-separated-list>` to override the default response shape. If you pass `--fields` with no value, the CLI prints the current default field list for that command.
133
151
 
152
+ ## Live smoke checks
153
+
154
+ In a local checkout, if you already have an authenticated test account, you can run a safe read-only smoke pass:
155
+
156
+ ```bash
157
+ npm run smoke:live
158
+ ```
159
+
160
+ This exercises a small set of real CLI reads against the currently authenticated Clio account:
161
+
162
+ - `auth status`
163
+ - `whoami`
164
+ - `users list`
165
+ - `notes list --type Matter`
166
+ - `outstanding-client-balances list`
167
+ - `calendar-entries list`
168
+
134
169
  ## Read-only examples
135
170
 
136
171
  ```bash
@@ -165,6 +200,14 @@ not-manage practice-areas list --name "Family"
165
200
  not-manage practice-areas list --matter-id 456
166
201
  not-manage practice-area get 45
167
202
 
203
+ not-manage calendar-entries list --from 2026-03-01T00:00:00Z --to 2026-03-31T23:59:59Z
204
+ not-manage reminders list --state pending
205
+ not-manage notes list --type Matter --limit 25
206
+ not-manage conversation-messages list --conversation-id 123
207
+ not-manage outstanding-client-balances list --limit 25
208
+ not-manage matter-dockets list --matter-id 456
209
+ not-manage my-events list --limit 25
210
+
168
211
  not-manage billable-matters list --client-id 999
169
212
  not-manage billable-clients list --start-date 2026-03-01
170
213
  not-manage matter get 456 --redacted
@@ -201,6 +244,13 @@ not-manage matter get 456 --redacted
201
244
  - Dependabot is configured for both npm dependencies and GitHub Actions.
202
245
  - npm release publishing is set up for provenance-backed trusted publishing from GitHub Actions using the `npm` environment.
203
246
 
247
+ ## Support
248
+
249
+ - Public support page: [SUPPORT.md](SUPPORT.md)
250
+ - Public support contact: `hello@notoperations.com`
251
+ - Bug reports and feature requests: `https://github.com/Not-Operations/not-manage/issues`
252
+ - Security reports: see [SECURITY.md](SECURITY.md)
253
+
204
254
  ## Troubleshooting
205
255
 
206
256
  - `OS keychain ... failed`: re-enable your OS keychain service or run the CLI in a supported desktop session with keychain access.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "not-manage",
3
- "version": "0.1.18",
3
+ "version": "0.2.1",
4
4
  "description": "Unofficial command-line tool for Clio Manage integrations",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -31,14 +31,18 @@
31
31
  "cli"
32
32
  ],
33
33
  "scripts": {
34
- "check": "npm run check:syntax && npm test",
34
+ "check": "npm run check:syntax && npm test && npm run docs:check",
35
35
  "check:security": "npm audit --audit-level=high",
36
- "check:syntax": "node --check bin/not-manage.js && node --check src/*.js && node --check test/*.test.js && node --check test/helpers/*.js",
36
+ "check:syntax": "node --check bin/not-manage.js && node --check src/*.js && node --check scripts/*.js && node --check test/*.test.js && node --check test/helpers/*.js",
37
37
  "dev": "node bin/not-manage.js",
38
+ "docs:check": "node scripts/generate-readme-cli-reference.js --check",
39
+ "docs:generate": "node scripts/generate-readme-cli-reference.js",
40
+ "hooks:install": "node scripts/install-git-hooks.js",
38
41
  "pack:check": "npm pack --dry-run",
39
42
  "postinstall": "node src/postinstall.js",
40
43
  "seed:handbook-imports": "node scripts/generate-handbook-import-csvs.js",
41
44
  "seed:historical": "node scripts/seed-historical-data.js",
45
+ "smoke:live": "node scripts/smoke-read-only-live.js",
42
46
  "start": "node bin/not-manage.js",
43
47
  "test": "node --test test/*.test.js"
44
48
  },
@@ -0,0 +1,472 @@
1
+ function hasOwn(object, key) {
2
+ return Object.prototype.hasOwnProperty.call(object, key);
3
+ }
4
+
5
+ function isPlainObject(value) {
6
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7
+ }
8
+
9
+ function normalizeOptionName(name) {
10
+ return String(name).replace(/^--/, "");
11
+ }
12
+
13
+ function toSnakeCase(name) {
14
+ return normalizeOptionName(name).replace(/-/g, "_");
15
+ }
16
+
17
+ function toKebabCase(name) {
18
+ return normalizeOptionName(name).replace(/_/g, "-");
19
+ }
20
+
21
+ function optionKeyCandidates(name) {
22
+ const normalized = normalizeOptionName(name);
23
+ return [...new Set([normalized, toKebabCase(normalized), toSnakeCase(normalized)])];
24
+ }
25
+
26
+ function hasFlag(args, ...flags) {
27
+ return flags.some((flag) => args.includes(flag));
28
+ }
29
+
30
+ function appendParsedValue(parsed, key, value) {
31
+ if (!hasOwn(parsed, key)) {
32
+ parsed[key] = value;
33
+ return;
34
+ }
35
+
36
+ if (Array.isArray(parsed[key])) {
37
+ parsed[key].push(value);
38
+ return;
39
+ }
40
+
41
+ parsed[key] = [parsed[key], value];
42
+ }
43
+
44
+ function parseOptions(args) {
45
+ const parsed = {};
46
+ const positional = [];
47
+
48
+ for (let index = 0; index < args.length; index += 1) {
49
+ const token = args[index];
50
+
51
+ if (!token.startsWith("--")) {
52
+ positional.push(token);
53
+ continue;
54
+ }
55
+
56
+ const [rawKey, ...rest] = token.slice(2).split("=");
57
+ const inlineValue = rest.length > 0 ? rest.join("=") : null;
58
+
59
+ if (inlineValue !== null) {
60
+ appendParsedValue(parsed, rawKey, inlineValue);
61
+ continue;
62
+ }
63
+
64
+ const next = args[index + 1];
65
+ if (next && !next.startsWith("--")) {
66
+ appendParsedValue(parsed, rawKey, next);
67
+ index += 1;
68
+ continue;
69
+ }
70
+
71
+ appendParsedValue(parsed, rawKey, true);
72
+ }
73
+
74
+ return { parsed, positional };
75
+ }
76
+
77
+ function normalizeOptionValues(value) {
78
+ if (value === undefined) {
79
+ return [];
80
+ }
81
+
82
+ return Array.isArray(value) ? value : [value];
83
+ }
84
+
85
+ function readOptionValues(parsedOptions, name) {
86
+ return optionKeyCandidates(name).reduce((values, candidate) => {
87
+ if (!hasOwn(parsedOptions, candidate)) {
88
+ return values;
89
+ }
90
+
91
+ return values.concat(normalizeOptionValues(parsedOptions[candidate]));
92
+ }, []);
93
+ }
94
+
95
+ function readOption(parsedOptions, name) {
96
+ const values = readOptionValues(parsedOptions, name);
97
+ if (values.length === 0) {
98
+ return undefined;
99
+ }
100
+
101
+ return values.length === 1 ? values[0] : values;
102
+ }
103
+
104
+ function readLastOptionValue(parsedOptions, name) {
105
+ const values = readOptionValues(parsedOptions, name);
106
+ if (values.length === 0) {
107
+ return undefined;
108
+ }
109
+
110
+ return values[values.length - 1];
111
+ }
112
+
113
+ function readStringOption(parsedOptions, name) {
114
+ const value = readLastOptionValue(parsedOptions, name);
115
+ if (value === undefined) {
116
+ return undefined;
117
+ }
118
+
119
+ if (value === true) {
120
+ throw new Error(`\`--${toKebabCase(name)}\` requires a value.`);
121
+ }
122
+
123
+ return String(value);
124
+ }
125
+
126
+ function splitCommaSeparatedValues(text) {
127
+ return String(text)
128
+ .split(",")
129
+ .map((value) => value.trim())
130
+ .filter(Boolean);
131
+ }
132
+
133
+ function readStringArrayOption(parsedOptions, name) {
134
+ const values = readOptionValues(parsedOptions, name);
135
+ if (values.length === 0) {
136
+ return undefined;
137
+ }
138
+
139
+ const output = [];
140
+
141
+ values.forEach((value) => {
142
+ if (value === true) {
143
+ throw new Error(`\`--${toKebabCase(name)}\` requires a value.`);
144
+ }
145
+
146
+ splitCommaSeparatedValues(value).forEach((item) => {
147
+ output.push(item);
148
+ });
149
+ });
150
+
151
+ return output.length > 0 ? output : undefined;
152
+ }
153
+
154
+ function parseBooleanValue(value, name) {
155
+ if (value === true || value === false) {
156
+ return value;
157
+ }
158
+
159
+ if (typeof value === "string") {
160
+ const normalized = value.trim().toLowerCase();
161
+ if (normalized === "true") {
162
+ return true;
163
+ }
164
+ if (normalized === "false") {
165
+ return false;
166
+ }
167
+ }
168
+
169
+ throw new Error(`\`--${toKebabCase(name)}\` must be \`true\` or \`false\`.`);
170
+ }
171
+
172
+ function readBooleanOption(parsedOptions, name) {
173
+ const value = readLastOptionValue(parsedOptions, name);
174
+ if (value === undefined) {
175
+ return undefined;
176
+ }
177
+
178
+ return parseBooleanValue(value, name);
179
+ }
180
+
181
+ function readFlagOption(parsedOptions, name) {
182
+ const value = readBooleanOption(parsedOptions, name);
183
+ return value === undefined ? false : value;
184
+ }
185
+
186
+ function validateIsoDate(value, name) {
187
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
188
+ if (!match) {
189
+ throw new Error(`\`--${toKebabCase(name)}\` must be an ISO date like \`2026-03-13\`.`);
190
+ }
191
+
192
+ const isoValue = `${match[1]}-${match[2]}-${match[3]}`;
193
+ const parsed = new Date(`${isoValue}T00:00:00Z`);
194
+ if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== isoValue) {
195
+ throw new Error(`\`--${toKebabCase(name)}\` must be an ISO date like \`2026-03-13\`.`);
196
+ }
197
+
198
+ return value;
199
+ }
200
+
201
+ function validateIsoDateTime(value, name) {
202
+ const isoDateTime =
203
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/;
204
+ if (!isoDateTime.test(value) || Number.isNaN(Date.parse(value))) {
205
+ throw new Error(
206
+ `\`--${toKebabCase(name)}\` must be an ISO datetime like \`2026-03-13T15:00:00Z\`.`
207
+ );
208
+ }
209
+
210
+ return value;
211
+ }
212
+
213
+ function readIsoDateOption(parsedOptions, name) {
214
+ const value = readStringOption(parsedOptions, name);
215
+ if (value === undefined) {
216
+ return undefined;
217
+ }
218
+
219
+ return validateIsoDate(value, name);
220
+ }
221
+
222
+ function readIsoDateTimeOption(parsedOptions, name) {
223
+ const value = readStringOption(parsedOptions, name);
224
+ if (value === undefined) {
225
+ return undefined;
226
+ }
227
+
228
+ return validateIsoDateTime(value, name);
229
+ }
230
+
231
+ function readIsoDateArrayOption(parsedOptions, name) {
232
+ const values = readStringArrayOption(parsedOptions, name);
233
+ if (values === undefined) {
234
+ return undefined;
235
+ }
236
+
237
+ return values.map((value) => validateIsoDate(value, name));
238
+ }
239
+
240
+ function readIsoDateTimeArrayOption(parsedOptions, name) {
241
+ const values = readStringArrayOption(parsedOptions, name);
242
+ if (values === undefined) {
243
+ return undefined;
244
+ }
245
+
246
+ return values.map((value) => validateIsoDateTime(value, name));
247
+ }
248
+
249
+ function splitTopLevel(text, delimiter = ",") {
250
+ const segments = [];
251
+ let current = "";
252
+ let depth = 0;
253
+ let quote = null;
254
+ let escapeNext = false;
255
+
256
+ for (const char of String(text)) {
257
+ if (escapeNext) {
258
+ current += char;
259
+ escapeNext = false;
260
+ continue;
261
+ }
262
+
263
+ if (quote) {
264
+ current += char;
265
+ if (char === "\\") {
266
+ escapeNext = true;
267
+ } else if (char === quote) {
268
+ quote = null;
269
+ }
270
+ continue;
271
+ }
272
+
273
+ if (char === "'" || char === '"') {
274
+ quote = char;
275
+ current += char;
276
+ continue;
277
+ }
278
+
279
+ if (char === "[" || char === "{") {
280
+ depth += 1;
281
+ current += char;
282
+ continue;
283
+ }
284
+
285
+ if (char === "]" || char === "}") {
286
+ depth = Math.max(0, depth - 1);
287
+ current += char;
288
+ continue;
289
+ }
290
+
291
+ if (char === delimiter && depth === 0) {
292
+ if (current.trim()) {
293
+ segments.push(current.trim());
294
+ }
295
+ current = "";
296
+ continue;
297
+ }
298
+
299
+ current += char;
300
+ }
301
+
302
+ if (current.trim()) {
303
+ segments.push(current.trim());
304
+ }
305
+
306
+ return segments;
307
+ }
308
+
309
+ function unwrapQuotedValue(value) {
310
+ const text = String(value).trim();
311
+ if (
312
+ (text.startsWith('"') && text.endsWith('"')) ||
313
+ (text.startsWith("'") && text.endsWith("'"))
314
+ ) {
315
+ return text.slice(1, -1);
316
+ }
317
+ return text;
318
+ }
319
+
320
+ function parseStructuredValue(rawValue) {
321
+ const text = String(rawValue).trim();
322
+ if (!text) {
323
+ return "";
324
+ }
325
+
326
+ if (
327
+ (text.startsWith("{") && text.endsWith("}")) ||
328
+ (text.startsWith("[") && text.endsWith("]")) ||
329
+ (text.startsWith('"') && text.endsWith('"'))
330
+ ) {
331
+ try {
332
+ return JSON.parse(text);
333
+ } catch (_error) {
334
+ if (text.startsWith("[") && text.endsWith("]")) {
335
+ return splitTopLevel(text.slice(1, -1)).map((item) => unwrapQuotedValue(item));
336
+ }
337
+ }
338
+ }
339
+
340
+ return unwrapQuotedValue(text);
341
+ }
342
+
343
+ function mergeStructuredValues(existingValue, nextValue) {
344
+ if (existingValue === undefined) {
345
+ return nextValue;
346
+ }
347
+
348
+ if (isPlainObject(existingValue) && isPlainObject(nextValue)) {
349
+ return Object.entries(nextValue).reduce((merged, [key, value]) => {
350
+ merged[key] = mergeStructuredValues(merged[key], value);
351
+ return merged;
352
+ }, { ...existingValue });
353
+ }
354
+
355
+ const existingItems = Array.isArray(existingValue) ? existingValue : [existingValue];
356
+ const nextItems = Array.isArray(nextValue) ? nextValue : [nextValue];
357
+ return existingItems.concat(nextItems);
358
+ }
359
+
360
+ function parseObjectEntries(rawValue, name) {
361
+ const text = String(rawValue).trim();
362
+ if (!text) {
363
+ throw new Error(`\`--${toKebabCase(name)}\` requires a value.`);
364
+ }
365
+
366
+ if (text.startsWith("{")) {
367
+ const parsed = parseStructuredValue(text);
368
+ if (!isPlainObject(parsed)) {
369
+ throw new Error(`\`--${toKebabCase(name)}\` must be an object or key=value pairs.`);
370
+ }
371
+ return parsed;
372
+ }
373
+
374
+ return splitTopLevel(text).reduce((output, entry) => {
375
+ const separatorIndex = entry.search(/[:=]/);
376
+ if (separatorIndex <= 0) {
377
+ throw new Error(
378
+ `\`--${toKebabCase(name)}\` entries must look like \`field=value\` or JSON.`
379
+ );
380
+ }
381
+
382
+ const key = entry.slice(0, separatorIndex).trim();
383
+ const value = entry.slice(separatorIndex + 1).trim();
384
+ if (!key) {
385
+ throw new Error(
386
+ `\`--${toKebabCase(name)}\` entries must include a field identifier before \`=\` or \`:\`.`
387
+ );
388
+ }
389
+
390
+ output[key] = mergeStructuredValues(output[key], parseStructuredValue(value));
391
+ return output;
392
+ }, {});
393
+ }
394
+
395
+ function readObjectOption(parsedOptions, name) {
396
+ const values = readOptionValues(parsedOptions, name);
397
+ if (values.length === 0) {
398
+ return undefined;
399
+ }
400
+
401
+ return values.reduce((output, value) => {
402
+ if (value === true) {
403
+ throw new Error(`\`--${toKebabCase(name)}\` requires a value.`);
404
+ }
405
+
406
+ return mergeStructuredValues(output, parseObjectEntries(value, name));
407
+ }, undefined);
408
+ }
409
+
410
+ function readCommandOptions(parsedOptions, schema, positional = [], baseOptions = {}, fixed = {}) {
411
+ const options = { ...baseOptions };
412
+
413
+ Object.entries(schema || {}).forEach(([propertyName, optionDef]) => {
414
+ if (optionDef.positional !== undefined) {
415
+ options[propertyName] = positional[optionDef.positional];
416
+ return;
417
+ }
418
+
419
+ switch (optionDef.kind) {
420
+ case "boolean":
421
+ options[propertyName] = readBooleanOption(parsedOptions, optionDef.option);
422
+ return;
423
+ case "flag":
424
+ options[propertyName] = readFlagOption(parsedOptions, optionDef.option);
425
+ return;
426
+ case "iso-date":
427
+ options[propertyName] = readIsoDateOption(parsedOptions, optionDef.option);
428
+ return;
429
+ case "iso-date-array":
430
+ options[propertyName] = readIsoDateArrayOption(parsedOptions, optionDef.option);
431
+ return;
432
+ case "iso-datetime":
433
+ options[propertyName] = readIsoDateTimeOption(parsedOptions, optionDef.option);
434
+ return;
435
+ case "iso-datetime-array":
436
+ options[propertyName] = readIsoDateTimeArrayOption(parsedOptions, optionDef.option);
437
+ return;
438
+ case "object":
439
+ options[propertyName] = readObjectOption(parsedOptions, optionDef.option);
440
+ return;
441
+ case "string-array":
442
+ options[propertyName] = readStringArrayOption(parsedOptions, optionDef.option);
443
+ return;
444
+ case "string":
445
+ default:
446
+ options[propertyName] = readStringOption(parsedOptions, optionDef.option);
447
+ }
448
+ });
449
+
450
+ return {
451
+ ...options,
452
+ ...fixed,
453
+ };
454
+ }
455
+
456
+ module.exports = {
457
+ hasFlag,
458
+ parseOptions,
459
+ readBooleanOption,
460
+ readCommandOptions,
461
+ readFlagOption,
462
+ readIsoDateOption,
463
+ readIsoDateArrayOption,
464
+ readIsoDateTimeOption,
465
+ readIsoDateTimeArrayOption,
466
+ readObjectOption,
467
+ readOption,
468
+ readOptionValues,
469
+ readStringOption,
470
+ readStringArrayOption,
471
+ toKebabCase,
472
+ };