@dench.com/cli 2.2.5 → 2.2.6
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 +3 -2
- package/crm.ts +141 -9
- package/members.ts +7 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,9 +99,10 @@ memory only when the human asks or when saving a stable workspace fact.
|
|
|
99
99
|
|
|
100
100
|
Tasks live as CRM `task` entries — there is no separate `dench tasks`,
|
|
101
101
|
`dench task`, `dench claim`, or `dench log` command. Create one via
|
|
102
|
-
`dench crm entries create task --
|
|
102
|
+
`dench crm entries create task --data '{"Name":"…","Status":"To Do"}'`
|
|
103
103
|
only when the human assigns durable work or coordination benefits from
|
|
104
|
-
a tracked entry
|
|
104
|
+
a tracked entry (confirm field names with `dench crm fields list task`
|
|
105
|
+
first — unknown fields are rejected). For ad-hoc requests, work directly.
|
|
105
106
|
|
|
106
107
|
Use `dench context` when you need to know who you are, which workspace you
|
|
107
108
|
are using, pending approvals you requested, connected apps, and the best
|
package/crm.ts
CHANGED
|
@@ -289,6 +289,7 @@ async function applyBatchOpsInChunks(
|
|
|
289
289
|
ctx: CrmCliContext,
|
|
290
290
|
ops: CrmBatchOp[],
|
|
291
291
|
idempotencyKey: string | undefined,
|
|
292
|
+
allowUnknown = false,
|
|
292
293
|
): Promise<number> {
|
|
293
294
|
const chunks = chunkArray(ops, CRM_BATCH_CHUNK_SIZE);
|
|
294
295
|
for (const [chunkIndex, chunk] of chunks.entries()) {
|
|
@@ -296,11 +297,110 @@ async function applyBatchOpsInChunks(
|
|
|
296
297
|
await callMutation(ctx, api.batch.apply, {
|
|
297
298
|
ops: chunk,
|
|
298
299
|
...(chunkKey ? { idempotencyKey: chunkKey } : {}),
|
|
300
|
+
...(allowUnknown ? { allowUnknown: true } : {}),
|
|
299
301
|
});
|
|
300
302
|
}
|
|
301
303
|
return chunks.length;
|
|
302
304
|
}
|
|
303
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Fetch the canonical field names for an object. Best-effort: returns
|
|
308
|
+
* `null` (rather than throwing) on any failure so the caller falls back
|
|
309
|
+
* to the server's own unknown-field validation instead of blocking a
|
|
310
|
+
* write on a transient read error.
|
|
311
|
+
*/
|
|
312
|
+
async function fetchFieldNames(
|
|
313
|
+
ctx: CrmCliContext,
|
|
314
|
+
objectName: string,
|
|
315
|
+
): Promise<string[] | null> {
|
|
316
|
+
try {
|
|
317
|
+
const fields = (await callQuery(ctx, api.fields.list, { objectName })) as
|
|
318
|
+
| Array<{ name?: unknown }>
|
|
319
|
+
| null
|
|
320
|
+
| undefined;
|
|
321
|
+
if (!Array.isArray(fields)) return null;
|
|
322
|
+
const names = fields
|
|
323
|
+
.map((field) => (typeof field?.name === "string" ? field.name : null))
|
|
324
|
+
.filter((name): name is string => Boolean(name));
|
|
325
|
+
return names.length > 0 ? names : null;
|
|
326
|
+
} catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function levenshteinDistance(a: string, b: string): number {
|
|
332
|
+
if (a === b) return 0;
|
|
333
|
+
if (a.length === 0) return b.length;
|
|
334
|
+
if (b.length === 0) return a.length;
|
|
335
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
336
|
+
let curr = new Array<number>(b.length + 1);
|
|
337
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
338
|
+
curr[0] = i;
|
|
339
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
340
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
341
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
342
|
+
}
|
|
343
|
+
[prev, curr] = [curr, prev];
|
|
344
|
+
}
|
|
345
|
+
return prev[b.length];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function suggestKnownField(name: string, validNames: string[]): string | null {
|
|
349
|
+
const lower = name.toLowerCase();
|
|
350
|
+
const caseMatch = validNames.find((valid) => valid.toLowerCase() === lower);
|
|
351
|
+
if (caseMatch) return caseMatch;
|
|
352
|
+
let best: string | null = null;
|
|
353
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
354
|
+
for (const valid of validNames) {
|
|
355
|
+
const distance = levenshteinDistance(lower, valid.toLowerCase());
|
|
356
|
+
if (distance < bestDistance) {
|
|
357
|
+
bestDistance = distance;
|
|
358
|
+
best = valid;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const threshold = Math.max(2, Math.floor(name.length / 2));
|
|
362
|
+
return best !== null && bestDistance <= threshold ? best : null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Pre-flight check for `--data` keys against the object's real field
|
|
367
|
+
* schema, so a typo'd field name (e.g. `Name` on a `Title`-shaped task,
|
|
368
|
+
* or `Owner` instead of `Assignee`) fails fast with a "did you mean …"
|
|
369
|
+
* hint instead of silently dropping the value. The server enforces the
|
|
370
|
+
* same rule; this just gives a faster, friendlier local error. No-op
|
|
371
|
+
* when `--allow-unknown` is set or the schema can't be read.
|
|
372
|
+
*/
|
|
373
|
+
async function assertKnownDataKeys(
|
|
374
|
+
ctx: CrmCliContext,
|
|
375
|
+
objectName: string,
|
|
376
|
+
rows: Array<Record<string, unknown>>,
|
|
377
|
+
allowUnknown: boolean,
|
|
378
|
+
): Promise<void> {
|
|
379
|
+
if (allowUnknown) return;
|
|
380
|
+
const validNames = await fetchFieldNames(ctx, objectName);
|
|
381
|
+
if (!validNames) return;
|
|
382
|
+
const validSet = new Set(validNames);
|
|
383
|
+
const unknown = new Set<string>();
|
|
384
|
+
for (const row of rows) {
|
|
385
|
+
for (const key of Object.keys(row)) {
|
|
386
|
+
if (!validSet.has(key)) unknown.add(key);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (unknown.size === 0) return;
|
|
390
|
+
const parts = Array.from(unknown).map((name) => {
|
|
391
|
+
const suggestion = suggestKnownField(name, validNames);
|
|
392
|
+
return suggestion
|
|
393
|
+
? `"${name}" (did you mean "${suggestion}"?)`
|
|
394
|
+
: `"${name}"`;
|
|
395
|
+
});
|
|
396
|
+
throw new CrmCliError(
|
|
397
|
+
`Unknown field${unknown.size > 1 ? "s" : ""} for "${objectName}": ` +
|
|
398
|
+
`${parts.join(", ")}. Valid fields: ${validNames.join(", ")}. ` +
|
|
399
|
+
`Run \`dench crm fields list ${objectName}\` to check the schema, ` +
|
|
400
|
+
`or pass --allow-unknown to write the raw value anyway.`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
304
404
|
async function callQuery(
|
|
305
405
|
ctx: CrmCliContext,
|
|
306
406
|
fn: Parameters<ConvexHttpClient["query"]>[0],
|
|
@@ -1209,6 +1309,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1209
1309
|
case "create": {
|
|
1210
1310
|
const objectName = shift(ctx.args, "object name");
|
|
1211
1311
|
const dataRaw = getDataFlag(ctx.args);
|
|
1312
|
+
const allowUnknown = hasFlag(ctx.args, "--allow-unknown");
|
|
1212
1313
|
assertNoUnknownFlags(ctx.args, "crm entries create");
|
|
1213
1314
|
// Refuse silent blank-row creation. The agent that triggered this
|
|
1214
1315
|
// refactor passed `--fields` (which used to be silently dropped),
|
|
@@ -1226,9 +1327,16 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1226
1327
|
'dench crm entries create: --data must include at least one field. Pass --data \'{"Name":"…"}\' or use cells set after creation.',
|
|
1227
1328
|
);
|
|
1228
1329
|
}
|
|
1330
|
+
// Single-row writes rely on the server's schema validation (which
|
|
1331
|
+
// returns the same "did you mean …" hint the CLI would). No local
|
|
1332
|
+
// pre-fetch — that would add a round-trip to every create.
|
|
1229
1333
|
out(
|
|
1230
1334
|
ctx,
|
|
1231
|
-
await callMutation(ctx, api.entries.create, {
|
|
1335
|
+
await callMutation(ctx, api.entries.create, {
|
|
1336
|
+
objectName,
|
|
1337
|
+
data,
|
|
1338
|
+
...(allowUnknown ? { allowUnknown: true } : {}),
|
|
1339
|
+
}),
|
|
1232
1340
|
);
|
|
1233
1341
|
return;
|
|
1234
1342
|
}
|
|
@@ -1236,6 +1344,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1236
1344
|
const objectName = shift(ctx.args, "object name");
|
|
1237
1345
|
const dataRaw = getDataFlag(ctx.args);
|
|
1238
1346
|
const idempotencyKey = getFlag(ctx.args, "--idempotency-key");
|
|
1347
|
+
const allowUnknown = hasFlag(ctx.args, "--allow-unknown");
|
|
1239
1348
|
const rows = parseCreateManyRows(
|
|
1240
1349
|
await readJsonArrayInput({
|
|
1241
1350
|
args: ctx.args,
|
|
@@ -1244,6 +1353,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1244
1353
|
inlineValue: dataRaw,
|
|
1245
1354
|
}),
|
|
1246
1355
|
);
|
|
1356
|
+
await assertKnownDataKeys(ctx, objectName, rows, allowUnknown);
|
|
1247
1357
|
const chunks = await applyBatchOpsInChunks(
|
|
1248
1358
|
ctx,
|
|
1249
1359
|
rows.map((data) => ({
|
|
@@ -1252,6 +1362,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1252
1362
|
data,
|
|
1253
1363
|
})),
|
|
1254
1364
|
idempotencyKey,
|
|
1365
|
+
allowUnknown,
|
|
1255
1366
|
);
|
|
1256
1367
|
out(ctx, { created: rows.length, chunks });
|
|
1257
1368
|
return;
|
|
@@ -1260,6 +1371,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1260
1371
|
const objectName = shift(ctx.args, "object name");
|
|
1261
1372
|
const entryId = shift(ctx.args, "entry id");
|
|
1262
1373
|
const dataRaw = getDataFlag(ctx.args);
|
|
1374
|
+
const allowUnknown = hasFlag(ctx.args, "--allow-unknown");
|
|
1263
1375
|
assertNoUnknownFlags(ctx.args, "crm entries update");
|
|
1264
1376
|
if (dataRaw === undefined) {
|
|
1265
1377
|
throw new CrmCliError(
|
|
@@ -1272,20 +1384,24 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1272
1384
|
'dench crm entries update: --data must include at least one field to change. Pass --data \'{"Field":"…"}\'.',
|
|
1273
1385
|
);
|
|
1274
1386
|
}
|
|
1387
|
+
// Single-row writes rely on the server's schema validation (same
|
|
1388
|
+
// "did you mean …" hint, no extra round-trip).
|
|
1275
1389
|
out(
|
|
1276
1390
|
ctx,
|
|
1277
1391
|
await callMutation(ctx, api.entries.update, {
|
|
1278
1392
|
objectName,
|
|
1279
1393
|
entryId: entryId as any,
|
|
1280
1394
|
data,
|
|
1395
|
+
...(allowUnknown ? { allowUnknown: true } : {}),
|
|
1281
1396
|
}),
|
|
1282
1397
|
);
|
|
1283
1398
|
return;
|
|
1284
1399
|
}
|
|
1285
1400
|
case "update-many": {
|
|
1286
|
-
const
|
|
1401
|
+
const objectName = shift(ctx.args, "object name");
|
|
1287
1402
|
const updatesRaw = getUpdatesFlag(ctx.args);
|
|
1288
1403
|
const idempotencyKey = getFlag(ctx.args, "--idempotency-key");
|
|
1404
|
+
const allowUnknown = hasFlag(ctx.args, "--allow-unknown");
|
|
1289
1405
|
const updates = parseUpdateManyRows(
|
|
1290
1406
|
await readJsonArrayInput({
|
|
1291
1407
|
args: ctx.args,
|
|
@@ -1294,6 +1410,12 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1294
1410
|
inlineValue: updatesRaw,
|
|
1295
1411
|
}),
|
|
1296
1412
|
);
|
|
1413
|
+
await assertKnownDataKeys(
|
|
1414
|
+
ctx,
|
|
1415
|
+
objectName,
|
|
1416
|
+
updates.map((update) => update.data),
|
|
1417
|
+
allowUnknown,
|
|
1418
|
+
);
|
|
1297
1419
|
const chunks = await applyBatchOpsInChunks(
|
|
1298
1420
|
ctx,
|
|
1299
1421
|
updates.map((update) => ({
|
|
@@ -1302,6 +1424,7 @@ async function runEntriesCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1302
1424
|
data: update.data,
|
|
1303
1425
|
})),
|
|
1304
1426
|
idempotencyKey,
|
|
1427
|
+
allowUnknown,
|
|
1305
1428
|
);
|
|
1306
1429
|
out(ctx, { updated: updates.length, chunks });
|
|
1307
1430
|
return;
|
|
@@ -2772,15 +2895,21 @@ Fields (columns):
|
|
|
2772
2895
|
Entries (rows):
|
|
2773
2896
|
dench crm entries list <object> [--limit N] [--with-meta] [--json]
|
|
2774
2897
|
dench crm entries get <object> <entryId>
|
|
2775
|
-
dench crm entries create <object> --data '{"Field":"Value",...}' (alias: --fields)
|
|
2776
|
-
dench crm entries create-many <object> --data '[{"Field":"Value",...}]' [--file rows.json] [--idempotency-key <key>]
|
|
2777
|
-
dench crm entries update <object> <entryId> --data '{...}' (alias: --fields)
|
|
2778
|
-
dench crm entries update-many <object> --updates '[{"id":"...","data":{...}}]' [--file updates.json] [--idempotency-key <key>]
|
|
2898
|
+
dench crm entries create <object> --data '{"Field":"Value",...}' (alias: --fields) [--allow-unknown]
|
|
2899
|
+
dench crm entries create-many <object> --data '[{"Field":"Value",...}]' [--file rows.json] [--idempotency-key <key>] [--allow-unknown]
|
|
2900
|
+
dench crm entries update <object> <entryId> --data '{...}' (alias: --fields) [--allow-unknown]
|
|
2901
|
+
dench crm entries update-many <object> --updates '[{"id":"...","data":{...}}]' [--file updates.json] [--idempotency-key <key>] [--allow-unknown]
|
|
2779
2902
|
dench crm entries delete <object> <entryId>
|
|
2780
2903
|
dench crm entries bulk-delete <object> --ids id1,id2,id3
|
|
2781
2904
|
Note: --data is REQUIRED on create/update and must include >=1 field.
|
|
2782
2905
|
An empty --data '{}' is rejected — use 'crm cells set' for
|
|
2783
2906
|
single-field tweaks instead of creating a placeholder blank row.
|
|
2907
|
+
Field names are validated against the object schema: an unknown
|
|
2908
|
+
field (e.g. "Name" on a task that uses "Title", or "Owner" instead
|
|
2909
|
+
of "Assignee") is REJECTED with a "did you mean …" hint instead of
|
|
2910
|
+
being silently dropped. Verify names with 'crm fields list <object>'
|
|
2911
|
+
first. Pass --allow-unknown to write the raw value anyway, and on
|
|
2912
|
+
create the object's required fields must be present.
|
|
2784
2913
|
|
|
2785
2914
|
Cells (single-field updates):
|
|
2786
2915
|
dench crm cells get <object> <entryId> <field>
|
|
@@ -2806,9 +2935,12 @@ Workspace members (for user-type fields):
|
|
|
2806
2935
|
List \`{ userId, role, name, email }\` for every member of the
|
|
2807
2936
|
current org. Use the userIds when setting \`Owner\`, \`Assignee\`,
|
|
2808
2937
|
or any other \`user\`-type field so the UI shows a real linked
|
|
2809
|
-
avatar instead of an unlinked literal-string badge
|
|
2810
|
-
|
|
2811
|
-
|
|
2938
|
+
avatar instead of an unlinked literal-string badge. Check the
|
|
2939
|
+
object's actual field names first (\`crm fields list <object>\`) —
|
|
2940
|
+
the \`task\` object uses \`Assignee\`, not \`Owner\`, and some
|
|
2941
|
+
workspaces rename \`Name\` to \`Title\`:
|
|
2942
|
+
dench crm cells set task <entryId> Assignee '"<userId>"'
|
|
2943
|
+
dench crm entries create task --data '{"Name":"X","Assignee":"<userId>"}'
|
|
2812
2944
|
|
|
2813
2945
|
Batch:
|
|
2814
2946
|
dench crm batch --file ops.jsonl [--idempotency-key <key>]
|
package/members.ts
CHANGED
|
@@ -181,12 +181,15 @@ Alias:
|
|
|
181
181
|
Same surface, dispatched from inside the \`dench crm\` namespace
|
|
182
182
|
since the canonical use case is assigning CRM \`user\` fields.
|
|
183
183
|
|
|
184
|
-
After picking a userId
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
After picking a userId (confirm field names with
|
|
185
|
+
\`dench crm fields list <object>\` — the default \`task\` uses
|
|
186
|
+
\`Assignee\`, not \`Owner\`, and some workspaces rename \`Name\` to \`Title\`):
|
|
187
|
+
dench crm cells set task <entryId> Assignee '"<userId>"'
|
|
188
|
+
dench crm entries create task --data '{"Name":"Ship CRM","Assignee":"<userId>"}'
|
|
187
189
|
|
|
188
190
|
The server best-effort-resolves a name / email passed to a \`user\`
|
|
189
191
|
field when the match against the roster is unique, but the explicit
|
|
190
|
-
lookup via \`dench members list\` is the correct pattern.
|
|
192
|
+
lookup via \`dench members list\` is the correct pattern. Unknown field
|
|
193
|
+
names are rejected on write (pass --allow-unknown to bypass).
|
|
191
194
|
`);
|
|
192
195
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.6",
|
|
4
4
|
"description": "Dench agent workspace CLI. v2 unifies auth behind `dench signin`; the legacy `dench login` / `dench onboard` / `dench setup` / `dench what-can-i-do` / `dench register` commands now error with a redirect.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|