dazzer-connect 0.3.0 → 0.4.0

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.
Files changed (3) hide show
  1. package/README.md +20 -6
  2. package/dist/index.js +271 -51
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -69,12 +69,16 @@ Merge either one with any `mcpServers` entries you already have, save, and resta
69
69
 
70
70
  ## Put files into Dazzer
71
71
 
72
+ Dazzer takes a new file only with a description of it. A single new file needs an entry under its own name in a descriptions file, passed with `--described-in`, just as a folder does. The descriptions file is shown below.
73
+
72
74
  ```bash
73
75
  dazzer add report.pdf
74
76
  dazzer add ./backlog
75
77
  ```
76
78
 
77
- A folder is walked with its subfolders. Hidden files are left alone, and nothing outside the folder is ever sent. The first time, you are asked which space things go in: run the same command again with `--space "<name>"`, or with `--keep-to-myself`. A folder run ends with a count of what went in and what was skipped. Add `--why` to see each file that was skipped, and why.
79
+ A folder is walked with its subfolders. Hidden files are left alone, and nothing outside the folder is ever sent. The first time, you are asked which space things go in: run the same command again with `--space "<name>"`, or with `--keep-to-myself`. A folder run ends with a count of what went in and what was skipped. Add `--why` to see each file that was skipped, and why. A run that left a file out exits with a failure code. A file already in Dazzer was not left out. In a folder run, neither was a file held while you are asked whether it is a newer version of one already there.
80
+
81
+ A folder run without a descriptions file sends nothing and says so.
78
82
 
79
83
  ### With what your assistant wrote about each file
80
84
 
@@ -104,7 +108,7 @@ Your own AI assistant can read the files and say what each one is. It writes tha
104
108
  }
105
109
  ```
106
110
 
107
- Save it beside the folder, never inside it — for example as `backlog-descriptions.json` next to `backlog`. Inside the folder, a later `dazzer add ./backlog` without `--described-in` would send it like any other file. Then:
111
+ Save it beside the folder, never inside it — for example as `backlog-descriptions.json` next to `backlog`. Inside the folder, it is one more file a run has to tell apart from the documents. Then:
108
112
 
109
113
  ```bash
110
114
  dazzer add ./backlog --described-in ./backlog-descriptions.json
@@ -113,14 +117,24 @@ dazzer add ./backlog --described-in ./backlog-descriptions.json
113
117
  Each file goes with its own entry. The descriptions file named by `--described-in` is not sent with them, even when it sits inside the folder, and the run says to move it.
114
118
 
115
119
  - Each entry's form is checked before its file is sent. An entry in a form Dazzer refuses, such as a date written as "March 2026", holds its file back, and a line names the field and the form it takes. What the entry says is judged by Dazzer once the file has arrived.
116
- - A file with no entry goes in as usual, and the count names it.
117
- - An entry that names no file being added is reported, and nothing is sent for it. If no entry names a file being added, nothing is sent at all.
118
- - A file Dazzer refuses although it had an entry is named, in Dazzer's own words.
119
- - Whatever Dazzer did not keep of an entry is printed file by file, with what to change.
120
+ - A file with no entry, or whose entry has no description, is not sent. The count says how many, and names them while there are few.
121
+ - An entry that names no file being added is reported, and nothing is sent for it. If no entry names a file being added, or none has a description, nothing is sent at all.
122
+ - A file Dazzer refuses is named, with why it was not added. A file refused for its kind or its size is only counted, and `--why` names it.
123
+ - Whatever Dazzer did not keep of an entry is printed file by file, in plain words.
120
124
  - Sent again, a folder already in Dazzer says how many files took the description sent now, how many already had one, and how many did not take the one sent.
121
125
 
122
126
  An assistant that can run commands runs this one itself. One that cannot gives you the descriptions file to save beside the folder, and the line to run.
123
127
 
128
+ ## Keep it up to date
129
+
130
+ Dazzer takes files only from a recent enough copy of this tool. An older copy is told so on its first call, before it sends anything, with the one command that updates it:
131
+
132
+ ```bash
133
+ npm install -g dazzer-connect@latest
134
+ ```
135
+
136
+ Once a day, the tool also tells you when a newer version is out. It looks at most once a day, never makes a command wait for the answer, and stays quiet if it cannot find out. What it remembers sits in the same settings folder as the rest of its state, below.
137
+
124
138
  ## Other commands
125
139
 
126
140
  ```bash
package/dist/index.js CHANGED
@@ -8,6 +8,10 @@ import { Command } from "commander";
8
8
  import { open, stat as statPath } from "node:fs/promises";
9
9
  import { basename, extname } from "node:path";
10
10
 
11
+ // src/lib/api-client.ts
12
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
13
+ import { join } from "node:path";
14
+
11
15
  // src/lib/output.ts
12
16
  function writeOut(message) {
13
17
  process.stdout.write(`${message}
@@ -19,6 +23,8 @@ function writeErr(message) {
19
23
  }
20
24
  var CANNOT_REACH_DAZZER = "I can't reach Dazzer right now \u2014 nothing was sent. Try again shortly.";
21
25
  var SIGN_IN_NO_LONGER_VALID = "Your Dazzer sign-in is no longer valid. Reconnect your client (or run `dazzer auth login`).";
26
+ var UPDATE_THE_TOOL = "npm install -g dazzer-connect@latest";
27
+ var newerToolIsOut = (version2) => `Version ${version2} of dazzer is out. Update it with: ${UPDATE_THE_TOOL}`;
22
28
  function printDeviceInstructions(deviceAuth) {
23
29
  writeOut("");
24
30
  if (deviceAuth.verification_uri_complete) {
@@ -315,6 +321,31 @@ function refusalPlaces(body) {
315
321
  ...typeof keep === "boolean" ? { keepToYourself: keep } : {}
316
322
  };
317
323
  }
324
+ var wordsIn = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
325
+ function refusalAbout(body) {
326
+ if (typeof body !== "object" || body === null) return {};
327
+ const flat = body;
328
+ const enveloped = typeof flat.error === "object" && flat.error !== null ? flat.error.details : void 0;
329
+ const fields = [
330
+ ...wordsIn(flat.field) === void 0 ? [] : [flat.field],
331
+ ...Array.isArray(enveloped) ? enveloped.flatMap((detail) => {
332
+ const named2 = typeof detail === "object" && detail !== null ? wordsIn(detail.field) : void 0;
333
+ return named2 === void 0 ? [] : [named2];
334
+ }) : []
335
+ ];
336
+ const dropped = Array.isArray(flat.dropped) ? flat.dropped.flatMap((entry) => {
337
+ if (typeof entry !== "object" || entry === null) return [];
338
+ const line = wordsIn(entry.line);
339
+ const reason = wordsIn(entry.reason);
340
+ return line === void 0 || reason === void 0 ? [] : [{ line, reason }];
341
+ }) : [];
342
+ const howMuch = typeof flat.how_much_was_read === "object" && flat.how_much_was_read !== null ? wordsIn(flat.how_much_was_read.reason) : void 0;
343
+ return {
344
+ ...fields.length > 0 ? { fields } : {},
345
+ ...dropped.length > 0 ? { dropped } : {},
346
+ ...howMuch === void 0 ? {} : { howMuchWasRead: howMuch }
347
+ };
348
+ }
318
349
  async function resolveCredential(auth2) {
319
350
  let held;
320
351
  try {
@@ -384,15 +415,99 @@ async function callDazzer(request) {
384
415
  httpStatus: res.status,
385
416
  ...detail === void 0 ? {} : { detail },
386
417
  ...code === void 0 ? {} : { code },
387
- ...refusalPlaces(body)
418
+ ...refusalPlaces(body),
419
+ ...refusalAbout(body)
388
420
  };
389
421
  }
390
422
  return { status: "ok", httpStatus: res.status, body };
391
423
  }
424
+ var REGISTRY = "https://registry.npmjs.org";
425
+ var LOOK_AT_MOST_MS = 3e3;
426
+ var A_DAY_MS = 24 * 60 * 60 * 1e3;
427
+ var NEWER_TOOL_FILE = "newer-version.json";
428
+ function releaseOf(version2) {
429
+ if (typeof version2 !== "string") return null;
430
+ const said = /^(\d{1,6})\.(\d{1,6})\.(\d{1,6})$/.exec(version2);
431
+ return said === null ? null : [Number(said[1]), Number(said[2]), Number(said[3])];
432
+ }
433
+ function isLater(published, mine) {
434
+ const theirs = releaseOf(published);
435
+ const own = /^(\d{1,6}\.\d{1,6}\.\d{1,6})(-[^+]+)?(\+.+)?$/.exec(mine);
436
+ const ours = own === null ? null : releaseOf(own[1]);
437
+ if (theirs === null || own === null || ours === null) return false;
438
+ for (let at = 0; at < 3; at += 1) {
439
+ if (theirs[at] !== ours[at]) return (theirs[at] ?? 0) > (ours[at] ?? 0);
440
+ }
441
+ return own[2] !== void 0;
442
+ }
443
+ async function recall(file) {
444
+ try {
445
+ const parsed = JSON.parse(await readFile(file, "utf8"));
446
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
447
+ } catch {
448
+ return {};
449
+ }
450
+ }
451
+ async function remember(folder, file, what) {
452
+ try {
453
+ await mkdir(folder, { recursive: true });
454
+ await writeFile(file, `${JSON.stringify(what)}
455
+ `, "utf8");
456
+ return true;
457
+ } catch {
458
+ return false;
459
+ }
460
+ }
461
+ var withinADay = (then, now) => typeof then === "number" && Number.isFinite(then) && then <= now && now - then < A_DAY_MS;
462
+ function lookForNewerTool(check) {
463
+ const now = check.now ?? Date.now;
464
+ const file = join(check.folder, NEWER_TOOL_FILE);
465
+ const stop = new AbortController();
466
+ const looked = (async () => {
467
+ try {
468
+ const before = await recall(file);
469
+ if (withinADay(before.looked_at, now()) || stop.signal.aborted) return;
470
+ const signal = AbortSignal.any([
471
+ stop.signal,
472
+ AbortSignal.timeout(check.lookAtMostMs ?? LOOK_AT_MOST_MS)
473
+ ]);
474
+ const res = await (check.fetchFn ?? fetch)(
475
+ `${REGISTRY}/${encodeURIComponent(check.name)}/latest`,
476
+ { headers: { accept: "application/json" }, signal }
477
+ );
478
+ const published = res.ok ? (await res.json()).version : void 0;
479
+ if (stop.signal.aborted) return;
480
+ const latest = releaseOf(published) === null ? void 0 : published;
481
+ await remember(check.folder, file, {
482
+ ...await recall(file),
483
+ looked_at: now(),
484
+ ...latest === void 0 ? {} : { latest }
485
+ });
486
+ } catch {
487
+ }
488
+ })();
489
+ return {
490
+ looked,
491
+ finish: async () => {
492
+ stop.abort();
493
+ try {
494
+ await looked;
495
+ const known = await recall(file);
496
+ const latest = known.latest;
497
+ if (typeof latest !== "string" || !isLater(latest, check.version)) return null;
498
+ if (withinADay(known.told_at, now())) return null;
499
+ const kept = await remember(check.folder, file, { ...known, told_at: now() });
500
+ return kept ? newerToolIsOut(latest) : null;
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+ };
506
+ }
392
507
 
393
508
  // src/lib/config-store.ts
394
- import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
395
- import { join } from "node:path";
509
+ import { mkdir as mkdir2, readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
510
+ import { join as join2 } from "node:path";
396
511
  import envPaths from "env-paths";
397
512
 
398
513
  // src/lib/discovery.ts
@@ -456,9 +571,9 @@ async function discoverEndpoints(issuer, fetchFn = fetch) {
456
571
 
457
572
  // src/lib/config-store.ts
458
573
  var paths = envPaths("dazzer", { suffix: "" });
459
- var CONFIG_FILE = join(paths.config, "auth.json");
574
+ var CONFIG_FILE = join2(paths.config, "auth.json");
460
575
  async function ensureConfigDir() {
461
- await mkdir(paths.config, { recursive: true });
576
+ await mkdir2(paths.config, { recursive: true });
462
577
  }
463
578
  var MAX_CLIENT_ID_LENGTH = 512;
464
579
  async function saveAuthMetadata(meta) {
@@ -471,12 +586,12 @@ async function saveAuthMetadata(meta) {
471
586
  const savedToStore = typeof meta.savedToStore === "boolean" ? meta.savedToStore : void 0;
472
587
  await ensureConfigDir();
473
588
  const checked = { issuer, clientId, expiresAt, savedToStore };
474
- await writeFile(CONFIG_FILE, `${JSON.stringify(checked, null, 2)}
589
+ await writeFile2(CONFIG_FILE, `${JSON.stringify(checked, null, 2)}
475
590
  `, "utf8");
476
591
  }
477
592
  async function loadAuthMetadata() {
478
593
  try {
479
- const raw = await readFile(CONFIG_FILE, "utf8");
594
+ const raw = await readFile2(CONFIG_FILE, "utf8");
480
595
  const parsed = JSON.parse(raw);
481
596
  if (typeof parsed.issuer !== "string" || typeof parsed.clientId !== "string") {
482
597
  return null;
@@ -642,7 +757,7 @@ function createTokenStore() {
642
757
 
643
758
  // src/lib/walk.ts
644
759
  import { readdir, realpath, stat } from "node:fs/promises";
645
- import { join as join2, sep } from "node:path";
760
+ import { join as join3, sep } from "node:path";
646
761
  function inside(base, candidate) {
647
762
  return candidate === base || candidate.startsWith(base.endsWith(sep) ? base : `${base}${sep}`);
648
763
  }
@@ -667,7 +782,7 @@ async function walkFolder(root) {
667
782
  entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
668
783
  for (const entry of entries) {
669
784
  if (entry.name.startsWith(".")) continue;
670
- const full = join2(dir, entry.name);
785
+ const full = join3(dir, entry.name);
671
786
  const relative = `${prefix}${entry.name}`;
672
787
  if (entry.isSymbolicLink()) {
673
788
  await followShortcut(full, relative);
@@ -774,6 +889,10 @@ function namingNoFile(keys) {
774
889
  var noEntryMatches = (name2, example) => `No entry in ${name2} names a file being added \u2014 nothing was sent.` + (example === void 0 ? "" : ` A key looks like ${JSON.stringify(asPrinted(example))}.`);
775
890
  var keepItBeside = "The descriptions file is inside the folder. Keep it beside the folder instead, so no later run sends it as a document.";
776
891
  var itHadNoDescription = "It had no description.";
892
+ var NOTHING_DESCRIBED = "Nothing was sent. No file here has a description.";
893
+ var DESCRIBE_THEM = "Write what each file is in a descriptions file. Then run this again with --described-in.";
894
+ var notSentNoDescription = (name2) => `${name2} was not sent. It has no description.`;
895
+ var notAdded = (name2, because2) => [`${name2} was not added.`, ...because2].join(" ");
777
896
  function alreadyThereDescriptions(counted) {
778
897
  const parts = [
779
898
  ...counted.filed > 0 ? [`${counted.filed} took the description sent now`] : [],
@@ -933,6 +1052,7 @@ async function settleSpace(options, auth2, deps, voice, name2) {
933
1052
  var HELD_FOR_AN_ANSWER = "same_name_different_contents";
934
1053
  function fromAnswer(answer) {
935
1054
  if (answer.status === "refused") {
1055
+ if (aboutThisTool(answer)) return { kind: "stop", answer };
936
1056
  if (answer.code === HELD_FOR_AN_ANSWER && answer.detail !== void 0) {
937
1057
  return { kind: "held", question: answer.detail };
938
1058
  }
@@ -943,11 +1063,17 @@ function fromAnswer(answer) {
943
1063
  // Carried, not filled in: absent stays absent, so nothing downstream can print an offer the
944
1064
  // service did not make.
945
1065
  ...answer.choices === void 0 ? {} : { choices: answer.choices },
946
- ...answer.keepToYourself === void 0 ? {} : { keepToYourself: answer.keepToYourself }
1066
+ ...answer.keepToYourself === void 0 ? {} : { keepToYourself: answer.keepToYourself },
1067
+ ...answer.fields === void 0 ? {} : { fields: answer.fields },
1068
+ ...answer.dropped === void 0 ? {} : { dropped: answer.dropped },
1069
+ ...answer.howMuchWasRead === void 0 ? {} : { howMuchWasRead: answer.howMuchWasRead }
947
1070
  };
948
1071
  }
949
1072
  return { kind: "stop", answer };
950
1073
  }
1074
+ function aboutThisTool(answer) {
1075
+ return answer.status === "refused" && answer.fields?.includes(CLIENT_HEADER) === true;
1076
+ }
951
1077
  async function sendOne(input) {
952
1078
  const { deps, handle, options, voice } = input;
953
1079
  const described = await callPaced(
@@ -1208,8 +1334,22 @@ var LINE_DROPPED = {
1208
1334
  "line-verbatim-from-document": "its words are copied out of the document",
1209
1335
  "quote-empty": "it quotes nothing",
1210
1336
  "quote-too-long": "its quote is too long",
1337
+ "unstorable-characters": "it holds a character that cannot be stored",
1211
1338
  "quote-not-in-document": "the words it quotes are not in the document"
1212
1339
  };
1340
+ var A_LINE = {
1341
+ what_it_is: "The line saying what it is",
1342
+ what_it_covers: "The line saying what it covers",
1343
+ what_it_decides: "The line saying what it decides"
1344
+ };
1345
+ var HOW_MUCH_NOT_KEPT = {
1346
+ "not-said": "Its description does not say how much of the file was read.",
1347
+ "which-part-empty": "Its description says only part of the file was read, and not which part.",
1348
+ "which-part-unstorable-characters": "The part it says was read holds a character that cannot be stored.",
1349
+ "which-part-too-long": "The part it says was read is too long. Say it in one short sentence.",
1350
+ "which-part-line-break-or-marker": "The part it says was read is not one plain line."
1351
+ };
1352
+ var HOW_MUCH_NOT_KEPT_OTHERWISE = "Its description does not say how much of the file was read in a form Dazzer keeps.";
1213
1353
  var FACT_DROPPED = {
1214
1354
  "fact-empty": "it has no words",
1215
1355
  "fact-too-long": "it is too long",
@@ -1231,8 +1371,44 @@ var TYPING_NOT_USED = {
1231
1371
  not_used_file_has_its_own_text: "the file has words of its own",
1232
1372
  not_used_document_already_held: "the document was already here"
1233
1373
  };
1234
- var inPlainWords = (words, name2) => words[name2] ?? name2;
1235
- var SHORT_EXAMPLE = 120;
1374
+ var because = (words, name2) => {
1375
+ const said = words[name2];
1376
+ return said === void 0 ? ` (${name2}).` : `, because ${said}.`;
1377
+ };
1378
+ var lineNotKept = (dropped) => `${A_LINE[dropped.line] ?? "A line of its description"} was not kept${because(LINE_DROPPED, dropped.reason)}`;
1379
+ var howMuchNotKept = (reason) => HOW_MUCH_NOT_KEPT[reason] ?? HOW_MUCH_NOT_KEPT_OTHERWISE;
1380
+ function whatToDoAbout(refused) {
1381
+ return refused.code === "description_missing" ? DESCRIBE_THEM : void 0;
1382
+ }
1383
+ var ADD_THE_TEXT = "Add the text you read in it to its entry.";
1384
+ function notAddedBecause(refused) {
1385
+ const dropped = (refused.dropped ?? []).map(lineNotKept);
1386
+ const howMuch = refused.howMuchWasRead === void 0 ? [] : [howMuchNotKept(refused.howMuchWasRead)];
1387
+ switch (refused.code) {
1388
+ case "description_missing":
1389
+ return ["It has no description."];
1390
+ case "description_not_kept": {
1391
+ const noLineKept = refused.fields?.includes("description") === true;
1392
+ const said = [...noLineKept ? ["No line of its description was kept."] : [], ...dropped];
1393
+ const all = [...said, ...howMuch];
1394
+ return all.length > 0 ? all : ["Its description was not kept."];
1395
+ }
1396
+ case "nothing_to_check_it_against":
1397
+ return [
1398
+ "Dazzer holds no words of it to check its description against.",
1399
+ ADD_THE_TEXT,
1400
+ ...howMuch
1401
+ ];
1402
+ case "transcription_too_long":
1403
+ return ["The text typed out of it is longer than Dazzer keeps for one file."];
1404
+ case "try_again_shortly":
1405
+ return ["It could not be read just now. Try again shortly."];
1406
+ case "could_not_read_it":
1407
+ return ["Dazzer could not read its words.", ADD_THE_TEXT];
1408
+ default:
1409
+ return void 0;
1410
+ }
1411
+ }
1236
1412
  function whatBecameOfIt(stored, alreadyHere, descriptionSent) {
1237
1413
  const lines = [];
1238
1414
  let filing;
@@ -1240,34 +1416,25 @@ function whatBecameOfIt(stored, alreadyHere, descriptionSent) {
1240
1416
  if (description !== null) {
1241
1417
  const state = wordsOf(description.state);
1242
1418
  const what = wordsOf(description.what);
1243
- const how = wordsOf(description.how);
1244
1419
  if (descriptionSent && state !== void 0) {
1245
1420
  filing = state === "filed" ? "filed" : alreadyHere && state === "not_filed" && what === void 0 ? "already-described" : "not-kept";
1246
1421
  }
1247
1422
  if (state === "not_filed" && what !== void 0) {
1248
- lines.push(how === void 0 ? what : `${what} ${how}`);
1423
+ lines.push("Its description was not kept.");
1249
1424
  } else if (state !== void 0 && state !== "filed" && state !== "none_supplied" && !(alreadyHere && state === "not_filed")) {
1250
1425
  lines.push(
1251
- state === "not_filed" ? "description not filed." : `description not filed: ${inPlainWords(DESCRIPTION_NOT_FILED, state)}`
1426
+ state === "not_filed" ? "Its description was not filed." : `Its description was not filed${because(DESCRIPTION_NOT_FILED, state)}`
1252
1427
  );
1253
1428
  }
1254
1429
  for (const dropped of listOf(description.dropped)) {
1255
1430
  if (!isPlainObject(dropped)) continue;
1256
1431
  const line = wordsOf(dropped.line);
1257
1432
  const reason = wordsOf(dropped.reason);
1258
- const what2 = wordsOf(dropped.what);
1259
- const how2 = wordsOf(dropped.how);
1260
- const why = what2 === void 0 ? reason === void 0 ? void 0 : inPlainWords(LINE_DROPPED, reason) : how2 === void 0 ? what2 : `${what2} ${how2}`;
1261
- if (line !== void 0 && why !== void 0) lines.push(`${line} dropped: ${why}`);
1433
+ if (line !== void 0 && reason !== void 0) lines.push(lineNotKept({ line, reason }));
1262
1434
  }
1263
1435
  const howMuch = isPlainObject(description.how_much_was_read) ? description.how_much_was_read : null;
1264
- const howMuchWhat = howMuch === null ? void 0 : wordsOf(howMuch.what);
1265
- if (howMuch !== null && howMuchWhat !== void 0) {
1266
- const howMuchHow = wordsOf(howMuch.how);
1267
- const example = howMuch.example === void 0 ? "" : JSON.stringify(howMuch.example);
1268
- lines.push(
1269
- `how_much_was_read not kept: ${howMuchWhat}` + (howMuchHow === void 0 ? "" : ` ${howMuchHow}`) + (example !== "" && example.length <= SHORT_EXAMPLE ? ` Example: ${example}` : "")
1270
- );
1436
+ if (howMuch !== null && wordsOf(howMuch.what) !== void 0) {
1437
+ lines.push(howMuchNotKept(wordsOf(howMuch.reason) ?? ""));
1271
1438
  }
1272
1439
  }
1273
1440
  const facts = isPlainObject(stored.facts) ? stored.facts : null;
@@ -1275,16 +1442,16 @@ function whatBecameOfIt(stored, alreadyHere, descriptionSent) {
1275
1442
  if (!isPlainObject(dropped)) continue;
1276
1443
  const reason = wordsOf(dropped.reason);
1277
1444
  if (typeof dropped.fact === "number" && reason !== void 0) {
1278
- lines.push(`fact ${dropped.fact} dropped: ${inPlainWords(FACT_DROPPED, reason)}`);
1445
+ lines.push(`Fact ${dropped.fact} was not kept${because(FACT_DROPPED, reason)}`);
1279
1446
  }
1280
1447
  }
1281
1448
  const date = isPlainObject(stored.date_not_kept) ? wordsOf(stored.date_not_kept.reason) : void 0;
1282
1449
  if (date !== void 0 && !(alreadyHere && date === "document-already-held")) {
1283
- lines.push(`document_date not kept: ${inPlainWords(DATE_NOT_KEPT, date)}`);
1450
+ lines.push(`Its date was not kept${because(DATE_NOT_KEPT, date)}`);
1284
1451
  }
1285
1452
  const typed = wordsOf(stored.transcription);
1286
1453
  if (typed !== void 0 && typed !== "used" && !(alreadyHere && typed === "not_used_document_already_held")) {
1287
- lines.push(`transcription not used: ${inPlainWords(TYPING_NOT_USED, typed)}`);
1454
+ lines.push(`The text typed out of it was not used${because(TYPING_NOT_USED, typed)}`);
1288
1455
  }
1289
1456
  return {
1290
1457
  notKept: lines.map(asPrinted),
@@ -1305,6 +1472,7 @@ async function openOne(path) {
1305
1472
  return err?.code === "ENOENT" ? "missing" : "cannot-open";
1306
1473
  }
1307
1474
  }
1475
+ var LEAVES_A_FILE_OUT = (skip) => skip.reason !== "already-here" && skip.reason !== "the-descriptions-file";
1308
1476
  var REASON_ORDER = [
1309
1477
  "type-not-readable",
1310
1478
  "too-large",
@@ -1314,6 +1482,7 @@ var REASON_ORDER = [
1314
1482
  "did-not-finish",
1315
1483
  "leads-out",
1316
1484
  "entry-wrong-shape",
1485
+ "no-description",
1317
1486
  "the-descriptions-file"
1318
1487
  ];
1319
1488
  var NAMED_ALWAYS = /* @__PURE__ */ new Set(["entry-wrong-shape"]);
@@ -1331,6 +1500,7 @@ var COUNTED = {
1331
1500
  "did-not-finish": (many) => `${many} that didn't finish`,
1332
1501
  "leads-out": (many) => `${many} shortcut${many === 1 ? "" : "s"} out of the folder`,
1333
1502
  "entry-wrong-shape": (many) => `${many} with an entry in the wrong shape`,
1503
+ "no-description": (many) => `${many} with no description`,
1334
1504
  "the-descriptions-file": (many) => `${many} that ${many === 1 ? "is" : "are"} the descriptions file`
1335
1505
  };
1336
1506
  var IN_DETAIL = {
@@ -1342,10 +1512,11 @@ var IN_DETAIL = {
1342
1512
  "did-not-finish": "the send didn't finish",
1343
1513
  "leads-out": "a shortcut out of the folder",
1344
1514
  "entry-wrong-shape": "not sent: its entry is in the wrong shape.",
1515
+ "no-description": "not sent: it has no description.",
1345
1516
  "the-descriptions-file": "the descriptions file itself, never sent"
1346
1517
  };
1347
- function countLine(added, space, skipped, why, undescribed = []) {
1348
- const documents = `${added} document${added === 1 ? "" : "s"} in ${named(space)}.` + (undescribed.length === 0 ? "" : ` ${undescribed.length} had no description${undescribed.length <= NAMED_WHEN_AT_MOST ? ` \u2014 ${listed(undescribed.map(asPrinted))}` : ""}.`);
1518
+ function countLine(added, space, skipped, why) {
1519
+ const documents = `${added} document${added === 1 ? "" : "s"} in ${named(space)}.`;
1349
1520
  if (skipped.length === 0) return documents;
1350
1521
  const reasons = REASON_ORDER.flatMap((reason) => {
1351
1522
  const many = skipped.filter((skip) => skip.reason === reason).length;
@@ -1355,8 +1526,9 @@ function countLine(added, space, skipped, why, undescribed = []) {
1355
1526
  return `${documents} ${skipped.length} skipped \u2014 ${reasons}.${pointAtWhy ? " Run with --why to see each." : ""}`;
1356
1527
  }
1357
1528
  function whyLine(skip) {
1358
- const because = skip.said === void 0 ? IN_DETAIL[skip.reason] : skip.reason === "refused" ? `refused: ${skip.said}` : skip.said;
1359
- return `${asPrinted(skip.relative)} \u2014 ${because}`;
1529
+ if (skip.line !== void 0) return skip.line;
1530
+ const because2 = skip.said === void 0 ? IN_DETAIL[skip.reason] : skip.reason === "refused" ? `refused: ${skip.said}` : skip.said;
1531
+ return `${asPrinted(skip.relative)} \u2014 ${because2}`;
1360
1532
  }
1361
1533
  var FROM_THE_WALK = {
1362
1534
  "leads-out": "leads-out",
@@ -1447,7 +1619,7 @@ async function addOneFile(options, deps, voice, descriptions) {
1447
1619
  );
1448
1620
  }
1449
1621
  if (outcome.kind === "added" || outcome.kind === "already-here") {
1450
- for (const line of outcome.notKept) voice.write(`${name2} \u2014 ${line}`);
1622
+ for (const line of outcome.notKept) voice.write(`${name2}: ${line}`);
1451
1623
  }
1452
1624
  return result;
1453
1625
  } finally {
@@ -1472,11 +1644,18 @@ function answerForOneFile(outcome, name2, voice) {
1472
1644
  return { ok: false };
1473
1645
  // ONE FILE, ONE REFUSAL, AND THE PLACES IT OFFERED. The sentence is the service's and is
1474
1646
  // printed as sent; the places under it are the service's too, and without them a refusal
1475
- // that says to name one of them is a dead end for the person holding this one file.
1476
- case "refused":
1477
- voice.write(outcome.said ?? wentWrong(name2));
1647
+ // that says to name one of them is a dead end for the person holding this one file. A refusal
1648
+ // whose sentence is written for whoever makes the call is said in this command's own words.
1649
+ case "refused": {
1650
+ const because2 = notAddedBecause(outcome);
1651
+ voice.write(
1652
+ because2 === void 0 ? outcome.said ?? wentWrong(name2) : notAdded(name2, because2)
1653
+ );
1654
+ const next = whatToDoAbout(outcome);
1655
+ if (next !== void 0) voice.write(next);
1478
1656
  offerThePlaces(voice.write, outcome);
1479
1657
  return { ok: false };
1658
+ }
1480
1659
  case "did-not-finish":
1481
1660
  voice.write(didNotFinish(name2));
1482
1661
  return { ok: false };
@@ -1521,6 +1700,17 @@ async function addFolder(options, deps, voice, descriptions) {
1521
1700
  );
1522
1701
  return { ok: false };
1523
1702
  }
1703
+ const worthAsking = walked.files.some((file) => {
1704
+ const entry = descriptions?.entries.get(asKey(file.relative));
1705
+ if (entry === void 0) return false;
1706
+ const checked = checkEntry(entry);
1707
+ return "wrong" in checked || "description" in checked.described;
1708
+ });
1709
+ if (walked.files.length > 0 && !worthAsking) {
1710
+ voice.write(NOTHING_DESCRIBED);
1711
+ voice.write(DESCRIBE_THEM);
1712
+ return { ok: false };
1713
+ }
1524
1714
  const auth2 = deps.auth ?? await resolveAuth(options);
1525
1715
  if (auth2 === null) {
1526
1716
  voice.write(CANNOT_REACH_DAZZER);
@@ -1535,7 +1725,6 @@ async function addFolder(options, deps, voice, descriptions) {
1535
1725
  const held = [];
1536
1726
  let added = 0;
1537
1727
  let ran = true;
1538
- const undescribed = [];
1539
1728
  const notKept = [];
1540
1729
  const alreadyThere = { filed: 0, alreadyDescribed: 0, notKept: 0 };
1541
1730
  for (const file of walked.files) {
@@ -1553,6 +1742,16 @@ async function addFolder(options, deps, voice, descriptions) {
1553
1742
  }
1554
1743
  described = checked.described;
1555
1744
  }
1745
+ if (described === void 0 || !("description" in described)) {
1746
+ skipped.push(
1747
+ descriptions !== void 0 && await isFile(file.path, descriptions.identity) ? { relative: file.relative, reason: "the-descriptions-file" } : {
1748
+ relative: file.relative,
1749
+ reason: "no-description",
1750
+ line: notSentNoDescription(asPrinted(file.relative))
1751
+ }
1752
+ );
1753
+ continue;
1754
+ }
1556
1755
  const handle = await openOne(file.path);
1557
1756
  if (typeof handle === "string") {
1558
1757
  skipped.push({ relative: file.relative, reason: "cannot-open" });
@@ -1589,9 +1788,6 @@ async function addFolder(options, deps, voice, descriptions) {
1589
1788
  }
1590
1789
  if (outcome.kind === "added") {
1591
1790
  added += 1;
1592
- if (descriptions !== void 0 && !("description" in (described ?? {}))) {
1593
- undescribed.push(file.relative);
1594
- }
1595
1791
  continue;
1596
1792
  }
1597
1793
  if (outcome.kind === "already-here") {
@@ -1607,16 +1803,19 @@ async function addFolder(options, deps, voice, descriptions) {
1607
1803
  continue;
1608
1804
  }
1609
1805
  if (outcome.kind === "refused") {
1806
+ const because2 = notAddedBecause(outcome);
1807
+ const reason = (outcome.code === void 0 ? void 0 : FROM_THE_SERVICE[outcome.code]) ?? "refused";
1610
1808
  skipped.push({
1611
1809
  relative: file.relative,
1612
- // KEYED ON THE SERVICE'S CODE, never on the words it sent: the words are the person's and
1613
- // may be reworded, the code is the label the service publishes.
1614
- reason: (outcome.code === void 0 ? void 0 : FROM_THE_SERVICE[outcome.code]) ?? "refused",
1810
+ reason,
1615
1811
  ...outcome.said === void 0 ? {} : { said: outcome.said },
1616
- // REFUSED ALTHOUGH IT CARRIED AN ENTRY: most likely about what the entry says, which only
1617
- // the person's assistant can put right — so it is named, in the service's own sentence,
1618
- // without anybody having to run the whole folder again with `--why` to find out.
1619
- ...described === void 0 ? {} : { named: true }
1812
+ ...because2 === void 0 ? {} : { line: notAdded(asPrinted(file.relative), because2) },
1813
+ // REFUSED ALTHOUGH IT CARRIED AN ENTRY, which every file sent now does: most likely about
1814
+ // what the entry says, which only the person's assistant can put right, so it is named
1815
+ // without anybody having to run the whole folder again with `--why`. A refusal about the
1816
+ // file's kind or size is about the file, not its entry, so it stays a count that `--why`
1817
+ // names: a folder with thirty spreadsheets in it is one line, not thirty.
1818
+ ...reason === "refused" ? { named: true } : {}
1620
1819
  });
1621
1820
  continue;
1622
1821
  }
@@ -1636,7 +1835,11 @@ async function addFolder(options, deps, voice, descriptions) {
1636
1835
  skipped.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
1637
1836
  held.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
1638
1837
  notKept.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
1639
- voice.write(countLine(added, settled.space, skipped, options.why === true, undescribed));
1838
+ const undescribed = skipped.filter((skip) => skip.reason === "no-description");
1839
+ if (undescribed.length <= NAMED_WHEN_AT_MOST) {
1840
+ for (const skip of undescribed) skip.named = true;
1841
+ }
1842
+ voice.write(countLine(added, settled.space, skipped, options.why === true));
1640
1843
  if (skipped.some((skip) => skip.reason === "the-descriptions-file")) voice.write(keepItBeside);
1641
1844
  if (alreadyThere.filed + alreadyThere.alreadyDescribed + alreadyThere.notKept > 0) {
1642
1845
  voice.write(alreadyThereDescriptions(alreadyThere));
@@ -1647,7 +1850,7 @@ async function addFolder(options, deps, voice, descriptions) {
1647
1850
  if (options.why === true || namedWithoutWhy(skip)) voice.write(whyLine(skip));
1648
1851
  }
1649
1852
  for (const one of notKept) {
1650
- for (const line of one.lines) voice.write(`${asPrinted(one.relative)} \u2014 ${line}`);
1853
+ for (const line of one.lines) voice.write(`${asPrinted(one.relative)}: ${line}`);
1651
1854
  }
1652
1855
  const onlyOne = held.length === 1 ? held[0] : void 0;
1653
1856
  if (onlyOne !== void 0) {
@@ -1660,7 +1863,15 @@ async function addFolder(options, deps, voice, descriptions) {
1660
1863
  )
1661
1864
  );
1662
1865
  }
1663
- return { ok: ran };
1866
+ return { ok: ran && !skipped.some(LEAVES_A_FILE_OUT) };
1867
+ }
1868
+ async function isFile(path, identity) {
1869
+ try {
1870
+ const info = await statPath(path, { bigint: true });
1871
+ return sameFile({ dev: info.dev, ino: info.ino }, identity);
1872
+ } catch {
1873
+ return false;
1874
+ }
1664
1875
  }
1665
1876
  async function aKeyItWouldMatch(files, descriptionsFile) {
1666
1877
  for (const file of files) {
@@ -2399,6 +2610,15 @@ var { name, version } = require2("../package.json");
2399
2610
  var client = `${name}/${version}`;
2400
2611
  var program = new Command();
2401
2612
  program.name("dazzer").description("Sign in to Dazzer from your terminal").version(version);
2613
+ var newerTool;
2614
+ program.hook("preAction", () => {
2615
+ newerTool = lookForNewerTool({ name, version, folder: getConfigDir() });
2616
+ });
2617
+ program.hook("postAction", async () => {
2618
+ const said = await newerTool?.finish();
2619
+ if (said) process.stderr.write(`${said}
2620
+ `);
2621
+ });
2402
2622
  program.command("add").description("Put a file, or a whole folder, into Dazzer").argument("<path>", "The file or folder to add").option("--space <name>", "The space to put it in \u2014 the name you were shown, or its number").option("--keep-to-myself", "Keep it to yourself, rather than putting it in a shared space").option("--why", "Show each document that was skipped, and why").option(
2403
2623
  "--described-in <file>",
2404
2624
  "A JSON file saying what each file is, keyed by its path inside the folder"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dazzer-connect",
3
3
  "private": false,
4
- "version": "0.3.0",
4
+ "version": "0.4.0",
5
5
  "description": "Sign in to Dazzer \u2014 the memory your AI tools share \u2014 from a terminal, for clients that cannot sign in themselves",
6
6
  "license": "MIT",
7
7
  "type": "module",