dazzer-connect 0.2.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.
- package/README.md +68 -0
- package/dist/index.js +751 -106
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -67,6 +67,74 @@ The second carries a pass, for clients that cannot do that:
|
|
|
67
67
|
|
|
68
68
|
Merge either one with any `mcpServers` entries you already have, save, and restart your client.
|
|
69
69
|
|
|
70
|
+
## Put files into Dazzer
|
|
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
|
+
|
|
74
|
+
```bash
|
|
75
|
+
dazzer add report.pdf
|
|
76
|
+
dazzer add ./backlog
|
|
77
|
+
```
|
|
78
|
+
|
|
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.
|
|
82
|
+
|
|
83
|
+
### With what your assistant wrote about each file
|
|
84
|
+
|
|
85
|
+
Your own AI assistant can read the files and say what each one is. It writes that into one JSON file, called the descriptions file. Each key is a file's path inside the folder, or the file's name when you add just one, and what sits under a key is that file's entry:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"notes/plan.md": {
|
|
90
|
+
"description": {
|
|
91
|
+
"what_it_is": { "text": "the plan for the spring launch", "quote": "Spring launch plan" },
|
|
92
|
+
"what_it_covers": { "text": "who ships what, week by week", "quote": "Week 1: design review" },
|
|
93
|
+
"what_it_decides": null,
|
|
94
|
+
"how_much_was_read": { "all": true }
|
|
95
|
+
},
|
|
96
|
+
"facts": [{ "text": "the launch is on 14 April", "quote": "Launch: 14 April" }],
|
|
97
|
+
"document_date": "2026-03-01"
|
|
98
|
+
},
|
|
99
|
+
"scan.png": {
|
|
100
|
+
"description": {
|
|
101
|
+
"what_it_is": { "text": "a photo of the gate log", "quote": "GATE LOG" },
|
|
102
|
+
"what_it_covers": { "text": "who signed for the gate overnight", "quote": "night porter" },
|
|
103
|
+
"what_it_decides": null,
|
|
104
|
+
"how_much_was_read": { "all": true }
|
|
105
|
+
},
|
|
106
|
+
"transcription": "GATE LOG\nSigned by the night porter at four."
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
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:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
dazzer add ./backlog --described-in ./backlog-descriptions.json
|
|
115
|
+
```
|
|
116
|
+
|
|
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.
|
|
118
|
+
|
|
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.
|
|
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.
|
|
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.
|
|
125
|
+
|
|
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.
|
|
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
|
+
|
|
70
138
|
## Other commands
|
|
71
139
|
|
|
72
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) {
|
|
@@ -265,6 +271,7 @@ function assertHttpsServiceAddress(address) {
|
|
|
265
271
|
}
|
|
266
272
|
return address.replace(/\/$/, "");
|
|
267
273
|
}
|
|
274
|
+
var CLIENT_HEADER = "x-dazzer-client";
|
|
268
275
|
function readOfferedPlaces(offered) {
|
|
269
276
|
if (!Array.isArray(offered)) return [];
|
|
270
277
|
return offered.flatMap((place) => {
|
|
@@ -314,6 +321,31 @@ function refusalPlaces(body) {
|
|
|
314
321
|
...typeof keep === "boolean" ? { keepToYourself: keep } : {}
|
|
315
322
|
};
|
|
316
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
|
+
}
|
|
317
349
|
async function resolveCredential(auth2) {
|
|
318
350
|
let held;
|
|
319
351
|
try {
|
|
@@ -357,7 +389,8 @@ async function callDazzer(request) {
|
|
|
357
389
|
const fetchFn = request.fetchFn ?? fetch;
|
|
358
390
|
const headers = {
|
|
359
391
|
accept: "application/json",
|
|
360
|
-
authorization: `Bearer ${credential.held}
|
|
392
|
+
authorization: `Bearer ${credential.held}`,
|
|
393
|
+
[CLIENT_HEADER]: request.client
|
|
361
394
|
};
|
|
362
395
|
if (request.body !== void 0) headers["content-type"] = "application/json";
|
|
363
396
|
let res;
|
|
@@ -382,15 +415,99 @@ async function callDazzer(request) {
|
|
|
382
415
|
httpStatus: res.status,
|
|
383
416
|
...detail === void 0 ? {} : { detail },
|
|
384
417
|
...code === void 0 ? {} : { code },
|
|
385
|
-
...refusalPlaces(body)
|
|
418
|
+
...refusalPlaces(body),
|
|
419
|
+
...refusalAbout(body)
|
|
386
420
|
};
|
|
387
421
|
}
|
|
388
422
|
return { status: "ok", httpStatus: res.status, body };
|
|
389
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
|
+
}
|
|
390
507
|
|
|
391
508
|
// src/lib/config-store.ts
|
|
392
|
-
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
393
|
-
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";
|
|
394
511
|
import envPaths from "env-paths";
|
|
395
512
|
|
|
396
513
|
// src/lib/discovery.ts
|
|
@@ -454,9 +571,9 @@ async function discoverEndpoints(issuer, fetchFn = fetch) {
|
|
|
454
571
|
|
|
455
572
|
// src/lib/config-store.ts
|
|
456
573
|
var paths = envPaths("dazzer", { suffix: "" });
|
|
457
|
-
var CONFIG_FILE =
|
|
574
|
+
var CONFIG_FILE = join2(paths.config, "auth.json");
|
|
458
575
|
async function ensureConfigDir() {
|
|
459
|
-
await
|
|
576
|
+
await mkdir2(paths.config, { recursive: true });
|
|
460
577
|
}
|
|
461
578
|
var MAX_CLIENT_ID_LENGTH = 512;
|
|
462
579
|
async function saveAuthMetadata(meta) {
|
|
@@ -469,12 +586,12 @@ async function saveAuthMetadata(meta) {
|
|
|
469
586
|
const savedToStore = typeof meta.savedToStore === "boolean" ? meta.savedToStore : void 0;
|
|
470
587
|
await ensureConfigDir();
|
|
471
588
|
const checked = { issuer, clientId, expiresAt, savedToStore };
|
|
472
|
-
await
|
|
589
|
+
await writeFile2(CONFIG_FILE, `${JSON.stringify(checked, null, 2)}
|
|
473
590
|
`, "utf8");
|
|
474
591
|
}
|
|
475
592
|
async function loadAuthMetadata() {
|
|
476
593
|
try {
|
|
477
|
-
const raw = await
|
|
594
|
+
const raw = await readFile2(CONFIG_FILE, "utf8");
|
|
478
595
|
const parsed = JSON.parse(raw);
|
|
479
596
|
if (typeof parsed.issuer !== "string" || typeof parsed.clientId !== "string") {
|
|
480
597
|
return null;
|
|
@@ -640,7 +757,7 @@ function createTokenStore() {
|
|
|
640
757
|
|
|
641
758
|
// src/lib/walk.ts
|
|
642
759
|
import { readdir, realpath, stat } from "node:fs/promises";
|
|
643
|
-
import { join as
|
|
760
|
+
import { join as join3, sep } from "node:path";
|
|
644
761
|
function inside(base, candidate) {
|
|
645
762
|
return candidate === base || candidate.startsWith(base.endsWith(sep) ? base : `${base}${sep}`);
|
|
646
763
|
}
|
|
@@ -665,7 +782,7 @@ async function walkFolder(root) {
|
|
|
665
782
|
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
666
783
|
for (const entry of entries) {
|
|
667
784
|
if (entry.name.startsWith(".")) continue;
|
|
668
|
-
const full =
|
|
785
|
+
const full = join3(dir, entry.name);
|
|
669
786
|
const relative = `${prefix}${entry.name}`;
|
|
670
787
|
if (entry.isSymbolicLink()) {
|
|
671
788
|
await followShortcut(full, relative);
|
|
@@ -733,7 +850,7 @@ async function walkFolder(root) {
|
|
|
733
850
|
var SPACE_PATH = "/documents/v1/space";
|
|
734
851
|
var LANDING_PATH = "/documents/v1/landing";
|
|
735
852
|
var LANDED_PATH = "/documents/v1/landed";
|
|
736
|
-
var cannotOpen = (
|
|
853
|
+
var cannotOpen = (name2) => `Couldn't open ${name2} \u2014 check its permissions.`;
|
|
737
854
|
var SLOWING_DOWN = "Slowing down to stay within limits \u2014 still going.";
|
|
738
855
|
var ALREADY_IN = "That one's already in \u2014 nothing to do.";
|
|
739
856
|
var waitingOnYou = (many) => `${many} waiting on your answer.`;
|
|
@@ -756,9 +873,38 @@ var NUMBER_WORDS = [
|
|
|
756
873
|
"ten"
|
|
757
874
|
];
|
|
758
875
|
var inWords = (many) => NUMBER_WORDS[many] ?? String(many);
|
|
759
|
-
var cannotFind = (
|
|
760
|
-
var didNotFinish = (
|
|
761
|
-
var wentWrong = (
|
|
876
|
+
var cannotFind = (name2) => `Couldn't find ${name2}.`;
|
|
877
|
+
var didNotFinish = (name2) => `The send didn't finish \u2014 ${name2} is not in Dazzer. Try again shortly.`;
|
|
878
|
+
var wentWrong = (name2) => `Something went wrong and ${name2} is not in Dazzer. Try again shortly.`;
|
|
879
|
+
var descriptionsNotFound = (name2) => `Couldn't find the descriptions file ${name2} \u2014 nothing was sent.`;
|
|
880
|
+
var descriptionsUnreadable = (name2) => `Couldn't read the descriptions file ${name2} \u2014 nothing was sent.`;
|
|
881
|
+
var descriptionsNotJson = (name2) => `The descriptions file ${name2} is not JSON \u2014 nothing was sent.`;
|
|
882
|
+
var descriptionsNotAnObject = (name2) => `The descriptions file ${name2} is not one JSON object keyed by file path \u2014 nothing was sent.`;
|
|
883
|
+
var isTheDescriptionsFile = (name2) => `${name2} is the descriptions file \u2014 nothing was sent.`;
|
|
884
|
+
function namingNoFile(keys) {
|
|
885
|
+
const many = keys.length;
|
|
886
|
+
const which = many <= NAMED_WHEN_AT_MOST ? ` \u2014 ${listed(keys.map(asPrinted))}` : "";
|
|
887
|
+
return `${many} ${many === 1 ? "entry names" : "entries name"} no file being added${which}. Nothing was sent for ${many === 1 ? "it" : "them"}.`;
|
|
888
|
+
}
|
|
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))}.`);
|
|
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.";
|
|
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(" ");
|
|
896
|
+
function alreadyThereDescriptions(counted) {
|
|
897
|
+
const parts = [
|
|
898
|
+
...counted.filed > 0 ? [`${counted.filed} took the description sent now`] : [],
|
|
899
|
+
...counted.alreadyDescribed > 0 ? [`${counted.alreadyDescribed} already had a description`] : [],
|
|
900
|
+
...counted.notKept > 0 ? [`${counted.notKept} did not take the description sent`] : []
|
|
901
|
+
];
|
|
902
|
+
return `Of those already there: ${parts.join(", ")}.`;
|
|
903
|
+
}
|
|
904
|
+
var itsDescriptionFiledNow = "Its description was filed now.";
|
|
905
|
+
var itAlreadyHasOne = "It already has a description, and keeps it.";
|
|
906
|
+
var itsDescriptionNotKept = "The description sent was not kept.";
|
|
907
|
+
var NAMED_WHEN_AT_MOST = 5;
|
|
762
908
|
var choiceLine = (space, scopeId) => ` ${asPrinted(space)} \u2014 ${scopeId}`;
|
|
763
909
|
var answerWith = (keepToYourself, numbered) => {
|
|
764
910
|
const ways = [
|
|
@@ -788,8 +934,8 @@ var DECLARED_BY_EXTENSION = {
|
|
|
788
934
|
".webp": "image/webp"
|
|
789
935
|
};
|
|
790
936
|
var UNDECLARED = "application/octet-stream";
|
|
791
|
-
function asPrinted(
|
|
792
|
-
return
|
|
937
|
+
function asPrinted(name2) {
|
|
938
|
+
return name2.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, " ").trim();
|
|
793
939
|
}
|
|
794
940
|
function readEnvelope(body) {
|
|
795
941
|
if (typeof body !== "object" || body === null) return null;
|
|
@@ -865,10 +1011,11 @@ function askedWith(options) {
|
|
|
865
1011
|
if (answer === void 0 || answer === "") return {};
|
|
866
1012
|
return { space: /^\d+$/.test(answer) ? Number(answer) : answer };
|
|
867
1013
|
}
|
|
868
|
-
async function settleSpace(options, auth2, deps, voice,
|
|
1014
|
+
async function settleSpace(options, auth2, deps, voice, name2) {
|
|
869
1015
|
const answer = await callPaced(
|
|
870
1016
|
{
|
|
871
1017
|
baseUrl: options.baseUrl,
|
|
1018
|
+
client: options.client,
|
|
872
1019
|
path: SPACE_PATH,
|
|
873
1020
|
method: "POST",
|
|
874
1021
|
body: askedWith(options),
|
|
@@ -879,12 +1026,12 @@ async function settleSpace(options, auth2, deps, voice, name) {
|
|
|
879
1026
|
voice
|
|
880
1027
|
);
|
|
881
1028
|
if (answer.status !== "ok") {
|
|
882
|
-
say(voice.write, answer,
|
|
1029
|
+
say(voice.write, answer, name2);
|
|
883
1030
|
return null;
|
|
884
1031
|
}
|
|
885
1032
|
const data = readEnvelope(answer.body);
|
|
886
1033
|
if (data === null) {
|
|
887
|
-
voice.write(wentWrong(
|
|
1034
|
+
voice.write(wentWrong(name2));
|
|
888
1035
|
return null;
|
|
889
1036
|
}
|
|
890
1037
|
const space = typeof data.space === "string" && data.space.trim() !== "" ? data.space : null;
|
|
@@ -893,7 +1040,7 @@ async function settleSpace(options, auth2, deps, voice, name) {
|
|
|
893
1040
|
}
|
|
894
1041
|
const question = typeof data.question === "string" && data.question !== "" ? data.question : null;
|
|
895
1042
|
if (question === null) {
|
|
896
|
-
voice.write(wentWrong(
|
|
1043
|
+
voice.write(wentWrong(name2));
|
|
897
1044
|
return null;
|
|
898
1045
|
}
|
|
899
1046
|
voice.write(question);
|
|
@@ -905,6 +1052,7 @@ async function settleSpace(options, auth2, deps, voice, name) {
|
|
|
905
1052
|
var HELD_FOR_AN_ANSWER = "same_name_different_contents";
|
|
906
1053
|
function fromAnswer(answer) {
|
|
907
1054
|
if (answer.status === "refused") {
|
|
1055
|
+
if (aboutThisTool(answer)) return { kind: "stop", answer };
|
|
908
1056
|
if (answer.code === HELD_FOR_AN_ANSWER && answer.detail !== void 0) {
|
|
909
1057
|
return { kind: "held", question: answer.detail };
|
|
910
1058
|
}
|
|
@@ -915,16 +1063,23 @@ function fromAnswer(answer) {
|
|
|
915
1063
|
// Carried, not filled in: absent stays absent, so nothing downstream can print an offer the
|
|
916
1064
|
// service did not make.
|
|
917
1065
|
...answer.choices === void 0 ? {} : { choices: answer.choices },
|
|
918
|
-
...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 }
|
|
919
1070
|
};
|
|
920
1071
|
}
|
|
921
1072
|
return { kind: "stop", answer };
|
|
922
1073
|
}
|
|
1074
|
+
function aboutThisTool(answer) {
|
|
1075
|
+
return answer.status === "refused" && answer.fields?.includes(CLIENT_HEADER) === true;
|
|
1076
|
+
}
|
|
923
1077
|
async function sendOne(input) {
|
|
924
1078
|
const { deps, handle, options, voice } = input;
|
|
925
1079
|
const described = await callPaced(
|
|
926
1080
|
{
|
|
927
1081
|
baseUrl: options.baseUrl,
|
|
1082
|
+
client: options.client,
|
|
928
1083
|
path: LANDING_PATH,
|
|
929
1084
|
method: "POST",
|
|
930
1085
|
body: {
|
|
@@ -984,9 +1139,10 @@ async function sendOne(input) {
|
|
|
984
1139
|
const landed = await callPaced(
|
|
985
1140
|
{
|
|
986
1141
|
baseUrl: options.baseUrl,
|
|
1142
|
+
client: options.client,
|
|
987
1143
|
path: LANDED_PATH,
|
|
988
1144
|
method: "POST",
|
|
989
|
-
body: { ticket: landing.ticket },
|
|
1145
|
+
body: { ticket: landing.ticket, ...input.described },
|
|
990
1146
|
auth: input.auth,
|
|
991
1147
|
fetchFn: deps.fetchFn
|
|
992
1148
|
},
|
|
@@ -996,11 +1152,310 @@ async function sendOne(input) {
|
|
|
996
1152
|
if (landed.status !== "ok") return fromAnswer(landed);
|
|
997
1153
|
const stored = readEnvelope(landed.body);
|
|
998
1154
|
if (stored === null) return { kind: "went-wrong" };
|
|
999
|
-
|
|
1155
|
+
const alreadyHere = stored.already_here === true;
|
|
1156
|
+
const becameOfIt = whatBecameOfIt(stored, alreadyHere, "description" in (input.described ?? {}));
|
|
1157
|
+
if (alreadyHere) return { kind: "already-here", ...becameOfIt };
|
|
1000
1158
|
return {
|
|
1001
1159
|
kind: "added",
|
|
1002
1160
|
fileName: typeof stored.file_name === "string" ? asPrinted(stored.file_name) : asPrinted(basename(input.path)),
|
|
1003
|
-
space: typeof stored.space_name === "string" ? stored.space_name : null
|
|
1161
|
+
space: typeof stored.space_name === "string" ? stored.space_name : null,
|
|
1162
|
+
...becameOfIt
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
var ENTRY_FIELDS = ["description", "facts", "document_date", "transcription"];
|
|
1166
|
+
var DESCRIPTION_FIELDS = [
|
|
1167
|
+
"what_it_is",
|
|
1168
|
+
"what_it_covers",
|
|
1169
|
+
"what_it_decides",
|
|
1170
|
+
"how_much_was_read"
|
|
1171
|
+
];
|
|
1172
|
+
var QUOTED_FIELDS = ["text", "quote"];
|
|
1173
|
+
var MOST_FACTS = 100;
|
|
1174
|
+
var MOST_TYPED_CHARACTERS = 1e6;
|
|
1175
|
+
var MOST_MESSAGE_BYTES = 1024 * 1024;
|
|
1176
|
+
var LONGEST_TICKET = 4096;
|
|
1177
|
+
var A_DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
1178
|
+
var A_MOMENT_ON_THE_DAY = /^T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
1179
|
+
var EARLIEST_YEAR = 1e3;
|
|
1180
|
+
function aDateTheDoorReads(value) {
|
|
1181
|
+
const written = value.trim();
|
|
1182
|
+
const day = A_DAY.exec(written.slice(0, 10));
|
|
1183
|
+
if (day === null) return false;
|
|
1184
|
+
const [year, month, date] = [Number(day[1]), Number(day[2]), Number(day[3])];
|
|
1185
|
+
if (year < EARLIEST_YEAR) return false;
|
|
1186
|
+
const midnight = new Date(Date.UTC(year, month - 1, date));
|
|
1187
|
+
if (midnight.getUTCFullYear() !== year || midnight.getUTCMonth() !== month - 1 || midnight.getUTCDate() !== date) {
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
if (written.length === 10) return true;
|
|
1191
|
+
return A_MOMENT_ON_THE_DAY.test(written.slice(10)) && !Number.isNaN(new Date(written).getTime());
|
|
1192
|
+
}
|
|
1193
|
+
var sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
|
|
1194
|
+
var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1195
|
+
function asKey(path) {
|
|
1196
|
+
let key = path.normalize("NFC").replaceAll("\\", "/");
|
|
1197
|
+
while (key.startsWith("./")) key = key.slice(2);
|
|
1198
|
+
return key;
|
|
1199
|
+
}
|
|
1200
|
+
async function readDescriptions(path) {
|
|
1201
|
+
const name2 = asPrinted(basename(path));
|
|
1202
|
+
let handle;
|
|
1203
|
+
try {
|
|
1204
|
+
handle = await open(path, "r");
|
|
1205
|
+
} catch (err) {
|
|
1206
|
+
const missing = err?.code === "ENOENT";
|
|
1207
|
+
return { said: missing ? descriptionsNotFound(name2) : descriptionsUnreadable(name2) };
|
|
1208
|
+
}
|
|
1209
|
+
let text;
|
|
1210
|
+
let identity;
|
|
1211
|
+
try {
|
|
1212
|
+
const info = await handle.stat({ bigint: true });
|
|
1213
|
+
identity = { dev: info.dev, ino: info.ino };
|
|
1214
|
+
text = await handle.readFile("utf8");
|
|
1215
|
+
} catch {
|
|
1216
|
+
return { said: descriptionsUnreadable(name2) };
|
|
1217
|
+
} finally {
|
|
1218
|
+
await handle.close();
|
|
1219
|
+
}
|
|
1220
|
+
const json = text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1221
|
+
let parsed;
|
|
1222
|
+
try {
|
|
1223
|
+
parsed = JSON.parse(json);
|
|
1224
|
+
} catch {
|
|
1225
|
+
return { said: descriptionsNotJson(name2) };
|
|
1226
|
+
}
|
|
1227
|
+
if (!isPlainObject(parsed)) return { said: descriptionsNotAnObject(name2) };
|
|
1228
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1229
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1230
|
+
const matched = asKey(key);
|
|
1231
|
+
const already = entries.get(matched);
|
|
1232
|
+
entries.set(matched, { keys: [...already?.keys ?? [], key], value: already?.value ?? value });
|
|
1233
|
+
}
|
|
1234
|
+
return { name: name2, entries, identity };
|
|
1235
|
+
}
|
|
1236
|
+
function quoted(text) {
|
|
1237
|
+
const flat = asPrinted(text);
|
|
1238
|
+
return `"${flat.length > 40 ? `${flat.slice(0, 40)}\u2026` : flat}"`;
|
|
1239
|
+
}
|
|
1240
|
+
var A_QUOTED_SENTENCE = '{"text": "...", "quote": "..."}';
|
|
1241
|
+
function strayKey(value, fields, where, mayHold) {
|
|
1242
|
+
const stray = Object.keys(value).find((key) => !fields.includes(key));
|
|
1243
|
+
return stray === void 0 ? void 0 : `${where} holds ${quoted(stray)}. It holds only ${mayHold}.`;
|
|
1244
|
+
}
|
|
1245
|
+
function quotedSentenceWrong(value, where) {
|
|
1246
|
+
const stray = strayKey(value, QUOTED_FIELDS, where, "text and quote");
|
|
1247
|
+
if (stray !== void 0) return stray;
|
|
1248
|
+
for (const field of QUOTED_FIELDS) {
|
|
1249
|
+
const said = value[field];
|
|
1250
|
+
if (said !== void 0 && said !== null && typeof said !== "string") {
|
|
1251
|
+
return `the ${field} of ${where} is not text. Write it in quotes, like "Spring launch plan".`;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return void 0;
|
|
1255
|
+
}
|
|
1256
|
+
function lineWrong(description, line, mayBeNull) {
|
|
1257
|
+
if (!(line in description)) return void 0;
|
|
1258
|
+
const value = description[line];
|
|
1259
|
+
if (value === null && mayBeNull) return void 0;
|
|
1260
|
+
const where = `description.${line}`;
|
|
1261
|
+
if (!isPlainObject(value)) {
|
|
1262
|
+
return `${where} is not an object. Write it like ${A_QUOTED_SENTENCE}${mayBeNull ? ", or null when the document settles nothing" : ""}.`;
|
|
1263
|
+
}
|
|
1264
|
+
return quotedSentenceWrong(value, where);
|
|
1265
|
+
}
|
|
1266
|
+
function descriptionWrong(value) {
|
|
1267
|
+
if (!isPlainObject(value)) {
|
|
1268
|
+
return `description is not an object. Write it like {"what_it_is": ${A_QUOTED_SENTENCE}, ...}.`;
|
|
1269
|
+
}
|
|
1270
|
+
return strayKey(
|
|
1271
|
+
value,
|
|
1272
|
+
DESCRIPTION_FIELDS,
|
|
1273
|
+
"description",
|
|
1274
|
+
"what_it_is, what_it_covers, what_it_decides and how_much_was_read"
|
|
1275
|
+
) ?? lineWrong(value, "what_it_is", false) ?? lineWrong(value, "what_it_covers", false) ?? lineWrong(value, "what_it_decides", true);
|
|
1276
|
+
}
|
|
1277
|
+
function factsWrong(value) {
|
|
1278
|
+
if (!Array.isArray(value)) return `facts is not a list. Write it like [${A_QUOTED_SENTENCE}].`;
|
|
1279
|
+
if (value.length > MOST_FACTS) {
|
|
1280
|
+
return `facts holds ${value.length} facts. It takes at most ${MOST_FACTS}.`;
|
|
1281
|
+
}
|
|
1282
|
+
for (const [at, fact] of value.entries()) {
|
|
1283
|
+
const where = `fact ${at + 1}`;
|
|
1284
|
+
if (!isPlainObject(fact)) {
|
|
1285
|
+
return `${where} is not an object. Write each fact like ${A_QUOTED_SENTENCE}.`;
|
|
1286
|
+
}
|
|
1287
|
+
const wrong = quotedSentenceWrong(fact, where);
|
|
1288
|
+
if (wrong !== void 0) return wrong;
|
|
1289
|
+
}
|
|
1290
|
+
return void 0;
|
|
1291
|
+
}
|
|
1292
|
+
function dateWrong(value) {
|
|
1293
|
+
if (typeof value !== "string") return 'document_date is not text. Write it like "2026-03-01".';
|
|
1294
|
+
return aDateTheDoorReads(value) ? void 0 : `document_date ${quoted(value)} is not a date. Write it like "2026-03-01".`;
|
|
1295
|
+
}
|
|
1296
|
+
function typedWrong(value) {
|
|
1297
|
+
if (typeof value !== "string") {
|
|
1298
|
+
return "transcription is not text. Write the typed-out words as one string.";
|
|
1299
|
+
}
|
|
1300
|
+
return /\S/.test(value) && value.length > MOST_TYPED_CHARACTERS ? `transcription runs to ${value.length.toLocaleString("en-US")} characters. It takes at most ${MOST_TYPED_CHARACTERS.toLocaleString("en-US")}.` : void 0;
|
|
1301
|
+
}
|
|
1302
|
+
function sizeWrong(entry) {
|
|
1303
|
+
const bytes = Buffer.byteLength(
|
|
1304
|
+
JSON.stringify({ ticket: "t".repeat(LONGEST_TICKET), ...entry }),
|
|
1305
|
+
"utf8"
|
|
1306
|
+
);
|
|
1307
|
+
return bytes > MOST_MESSAGE_BYTES ? `its entry comes to ${bytes.toLocaleString("en-US")} bytes. One message takes at most ${MOST_MESSAGE_BYTES.toLocaleString("en-US")}.` : void 0;
|
|
1308
|
+
}
|
|
1309
|
+
function checkEntry(found) {
|
|
1310
|
+
if (found.keys.length > 1) {
|
|
1311
|
+
return {
|
|
1312
|
+
wrong: `${found.keys.length} entries name it \u2014 ${listed(found.keys.map(quoted))}. Keep one.`
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
const entry = found.value;
|
|
1316
|
+
if (!isPlainObject(entry)) {
|
|
1317
|
+
return {
|
|
1318
|
+
wrong: 'its entry is not an object. Write it like {"description": {...}, "facts": [...], "document_date": "2026-03-01"}.'
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
const wrong = strayKey(
|
|
1322
|
+
entry,
|
|
1323
|
+
ENTRY_FIELDS,
|
|
1324
|
+
"its entry",
|
|
1325
|
+
"description, facts, document_date and transcription"
|
|
1326
|
+
) ?? ("description" in entry ? descriptionWrong(entry.description) : void 0) ?? ("facts" in entry ? factsWrong(entry.facts) : void 0) ?? ("document_date" in entry ? dateWrong(entry.document_date) : void 0) ?? ("transcription" in entry ? typedWrong(entry.transcription) : void 0) ?? sizeWrong(entry);
|
|
1327
|
+
return wrong === void 0 ? { described: entry } : { wrong };
|
|
1328
|
+
}
|
|
1329
|
+
var wordsOf = (value) => typeof value === "string" && value.trim() !== "" ? value : void 0;
|
|
1330
|
+
var listOf = (value) => Array.isArray(value) ? value : [];
|
|
1331
|
+
var LINE_DROPPED = {
|
|
1332
|
+
"line-empty": "it has no words",
|
|
1333
|
+
"line-too-long": "it is too long",
|
|
1334
|
+
"line-verbatim-from-document": "its words are copied out of the document",
|
|
1335
|
+
"quote-empty": "it quotes nothing",
|
|
1336
|
+
"quote-too-long": "its quote is too long",
|
|
1337
|
+
"unstorable-characters": "it holds a character that cannot be stored",
|
|
1338
|
+
"quote-not-in-document": "the words it quotes are not in the document"
|
|
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.";
|
|
1353
|
+
var FACT_DROPPED = {
|
|
1354
|
+
"fact-empty": "it has no words",
|
|
1355
|
+
"fact-too-long": "it is too long",
|
|
1356
|
+
"quote-empty": "it quotes nothing",
|
|
1357
|
+
"quote-too-long": "its quote is too long",
|
|
1358
|
+
"unstorable-characters": "it holds a character that cannot be stored",
|
|
1359
|
+
"quote-not-in-document": "the words it quotes are not in the document",
|
|
1360
|
+
"nothing-to-check-it-against": "there are no words in the file to check it against"
|
|
1361
|
+
};
|
|
1362
|
+
var DATE_NOT_KEPT = {
|
|
1363
|
+
"later-than-today": "it is later than today",
|
|
1364
|
+
"document-already-held": "the document was already here, with the date it came with",
|
|
1365
|
+
"stored-as-the-moment-it-arrived": "that day had not begun in universal time, so the moment it arrived is kept"
|
|
1366
|
+
};
|
|
1367
|
+
var DESCRIPTION_NOT_FILED = {
|
|
1368
|
+
nothing_to_check_it_against: "there are no words in the file to check it against"
|
|
1369
|
+
};
|
|
1370
|
+
var TYPING_NOT_USED = {
|
|
1371
|
+
not_used_file_has_its_own_text: "the file has words of its own",
|
|
1372
|
+
not_used_document_already_held: "the document was already here"
|
|
1373
|
+
};
|
|
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
|
+
}
|
|
1412
|
+
function whatBecameOfIt(stored, alreadyHere, descriptionSent) {
|
|
1413
|
+
const lines = [];
|
|
1414
|
+
let filing;
|
|
1415
|
+
const description = isPlainObject(stored.description) ? stored.description : null;
|
|
1416
|
+
if (description !== null) {
|
|
1417
|
+
const state = wordsOf(description.state);
|
|
1418
|
+
const what = wordsOf(description.what);
|
|
1419
|
+
if (descriptionSent && state !== void 0) {
|
|
1420
|
+
filing = state === "filed" ? "filed" : alreadyHere && state === "not_filed" && what === void 0 ? "already-described" : "not-kept";
|
|
1421
|
+
}
|
|
1422
|
+
if (state === "not_filed" && what !== void 0) {
|
|
1423
|
+
lines.push("Its description was not kept.");
|
|
1424
|
+
} else if (state !== void 0 && state !== "filed" && state !== "none_supplied" && !(alreadyHere && state === "not_filed")) {
|
|
1425
|
+
lines.push(
|
|
1426
|
+
state === "not_filed" ? "Its description was not filed." : `Its description was not filed${because(DESCRIPTION_NOT_FILED, state)}`
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
for (const dropped of listOf(description.dropped)) {
|
|
1430
|
+
if (!isPlainObject(dropped)) continue;
|
|
1431
|
+
const line = wordsOf(dropped.line);
|
|
1432
|
+
const reason = wordsOf(dropped.reason);
|
|
1433
|
+
if (line !== void 0 && reason !== void 0) lines.push(lineNotKept({ line, reason }));
|
|
1434
|
+
}
|
|
1435
|
+
const howMuch = isPlainObject(description.how_much_was_read) ? description.how_much_was_read : null;
|
|
1436
|
+
if (howMuch !== null && wordsOf(howMuch.what) !== void 0) {
|
|
1437
|
+
lines.push(howMuchNotKept(wordsOf(howMuch.reason) ?? ""));
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const facts = isPlainObject(stored.facts) ? stored.facts : null;
|
|
1441
|
+
for (const dropped of listOf(facts?.dropped)) {
|
|
1442
|
+
if (!isPlainObject(dropped)) continue;
|
|
1443
|
+
const reason = wordsOf(dropped.reason);
|
|
1444
|
+
if (typeof dropped.fact === "number" && reason !== void 0) {
|
|
1445
|
+
lines.push(`Fact ${dropped.fact} was not kept${because(FACT_DROPPED, reason)}`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const date = isPlainObject(stored.date_not_kept) ? wordsOf(stored.date_not_kept.reason) : void 0;
|
|
1449
|
+
if (date !== void 0 && !(alreadyHere && date === "document-already-held")) {
|
|
1450
|
+
lines.push(`Its date was not kept${because(DATE_NOT_KEPT, date)}`);
|
|
1451
|
+
}
|
|
1452
|
+
const typed = wordsOf(stored.transcription);
|
|
1453
|
+
if (typed !== void 0 && typed !== "used" && !(alreadyHere && typed === "not_used_document_already_held")) {
|
|
1454
|
+
lines.push(`The text typed out of it was not used${because(TYPING_NOT_USED, typed)}`);
|
|
1455
|
+
}
|
|
1456
|
+
return {
|
|
1457
|
+
notKept: lines.map(asPrinted),
|
|
1458
|
+
...filing === void 0 ? {} : { description: filing }
|
|
1004
1459
|
};
|
|
1005
1460
|
}
|
|
1006
1461
|
async function sendOneSafely(input) {
|
|
@@ -1017,6 +1472,7 @@ async function openOne(path) {
|
|
|
1017
1472
|
return err?.code === "ENOENT" ? "missing" : "cannot-open";
|
|
1018
1473
|
}
|
|
1019
1474
|
}
|
|
1475
|
+
var LEAVES_A_FILE_OUT = (skip) => skip.reason !== "already-here" && skip.reason !== "the-descriptions-file";
|
|
1020
1476
|
var REASON_ORDER = [
|
|
1021
1477
|
"type-not-readable",
|
|
1022
1478
|
"too-large",
|
|
@@ -1024,8 +1480,13 @@ var REASON_ORDER = [
|
|
|
1024
1480
|
"refused",
|
|
1025
1481
|
"cannot-open",
|
|
1026
1482
|
"did-not-finish",
|
|
1027
|
-
"leads-out"
|
|
1483
|
+
"leads-out",
|
|
1484
|
+
"entry-wrong-shape",
|
|
1485
|
+
"no-description",
|
|
1486
|
+
"the-descriptions-file"
|
|
1028
1487
|
];
|
|
1488
|
+
var NAMED_ALWAYS = /* @__PURE__ */ new Set(["entry-wrong-shape"]);
|
|
1489
|
+
var namedWithoutWhy = (skip) => NAMED_ALWAYS.has(skip.reason) || skip.named === true;
|
|
1029
1490
|
var FROM_THE_SERVICE = {
|
|
1030
1491
|
type_not_readable: "type-not-readable",
|
|
1031
1492
|
too_large: "too-large"
|
|
@@ -1037,7 +1498,10 @@ var COUNTED = {
|
|
|
1037
1498
|
refused: (many) => `${many} refused`,
|
|
1038
1499
|
"cannot-open": (many) => `${many} I couldn't open`,
|
|
1039
1500
|
"did-not-finish": (many) => `${many} that didn't finish`,
|
|
1040
|
-
"leads-out": (many) => `${many} shortcut${many === 1 ? "" : "s"} out of the folder
|
|
1501
|
+
"leads-out": (many) => `${many} shortcut${many === 1 ? "" : "s"} out of the folder`,
|
|
1502
|
+
"entry-wrong-shape": (many) => `${many} with an entry in the wrong shape`,
|
|
1503
|
+
"no-description": (many) => `${many} with no description`,
|
|
1504
|
+
"the-descriptions-file": (many) => `${many} that ${many === 1 ? "is" : "are"} the descriptions file`
|
|
1041
1505
|
};
|
|
1042
1506
|
var IN_DETAIL = {
|
|
1043
1507
|
"type-not-readable": "a file type I can't read",
|
|
@@ -1046,7 +1510,10 @@ var IN_DETAIL = {
|
|
|
1046
1510
|
refused: "refused",
|
|
1047
1511
|
"cannot-open": "I couldn't open it",
|
|
1048
1512
|
"did-not-finish": "the send didn't finish",
|
|
1049
|
-
"leads-out": "a shortcut out of the folder"
|
|
1513
|
+
"leads-out": "a shortcut out of the folder",
|
|
1514
|
+
"entry-wrong-shape": "not sent: its entry is in the wrong shape.",
|
|
1515
|
+
"no-description": "not sent: it has no description.",
|
|
1516
|
+
"the-descriptions-file": "the descriptions file itself, never sent"
|
|
1050
1517
|
};
|
|
1051
1518
|
function countLine(added, space, skipped, why) {
|
|
1052
1519
|
const documents = `${added} document${added === 1 ? "" : "s"} in ${named(space)}.`;
|
|
@@ -1055,11 +1522,13 @@ function countLine(added, space, skipped, why) {
|
|
|
1055
1522
|
const many = skipped.filter((skip) => skip.reason === reason).length;
|
|
1056
1523
|
return many === 0 ? [] : [COUNTED[reason](many)];
|
|
1057
1524
|
}).join(", ");
|
|
1058
|
-
|
|
1525
|
+
const pointAtWhy = !why && skipped.some((skip) => !namedWithoutWhy(skip) && skip.reason !== "the-descriptions-file");
|
|
1526
|
+
return `${documents} ${skipped.length} skipped \u2014 ${reasons}.${pointAtWhy ? " Run with --why to see each." : ""}`;
|
|
1059
1527
|
}
|
|
1060
1528
|
function whyLine(skip) {
|
|
1061
|
-
|
|
1062
|
-
|
|
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}`;
|
|
1063
1532
|
}
|
|
1064
1533
|
var FROM_THE_WALK = {
|
|
1065
1534
|
"leads-out": "leads-out",
|
|
@@ -1068,90 +1537,146 @@ var FROM_THE_WALK = {
|
|
|
1068
1537
|
async function runAdd(options, deps = {}) {
|
|
1069
1538
|
const voice = voiceFor(deps.write ?? ((line) => process.stdout.write(`${line}
|
|
1070
1539
|
`)));
|
|
1071
|
-
const
|
|
1540
|
+
const name2 = asPrinted(basename(options.file));
|
|
1072
1541
|
let isFolder;
|
|
1073
1542
|
try {
|
|
1074
1543
|
isFolder = (await statPath(options.file)).isDirectory();
|
|
1075
1544
|
} catch (err) {
|
|
1076
1545
|
voice.write(
|
|
1077
|
-
err?.code === "ENOENT" ? cannotFind(
|
|
1546
|
+
err?.code === "ENOENT" ? cannotFind(name2) : cannotOpen(name2)
|
|
1078
1547
|
);
|
|
1079
1548
|
return { ok: false };
|
|
1080
1549
|
}
|
|
1081
|
-
|
|
1550
|
+
let descriptions;
|
|
1551
|
+
if (options.describedIn !== void 0) {
|
|
1552
|
+
const read = await readDescriptions(options.describedIn);
|
|
1553
|
+
if ("said" in read) {
|
|
1554
|
+
voice.write(read.said);
|
|
1555
|
+
return { ok: false };
|
|
1556
|
+
}
|
|
1557
|
+
descriptions = read;
|
|
1558
|
+
}
|
|
1559
|
+
return isFolder ? await addFolder(options, deps, voice, descriptions) : await addOneFile(options, deps, voice, descriptions);
|
|
1082
1560
|
}
|
|
1083
|
-
async function addOneFile(options, deps, voice) {
|
|
1084
|
-
const
|
|
1561
|
+
async function addOneFile(options, deps, voice, descriptions) {
|
|
1562
|
+
const name2 = asPrinted(basename(options.file));
|
|
1085
1563
|
const handle = await openOne(options.file);
|
|
1086
1564
|
if (typeof handle === "string") {
|
|
1087
|
-
voice.write(handle === "missing" ? cannotFind(
|
|
1565
|
+
voice.write(handle === "missing" ? cannotFind(name2) : cannotOpen(name2));
|
|
1088
1566
|
return { ok: false };
|
|
1089
1567
|
}
|
|
1090
1568
|
try {
|
|
1091
|
-
const
|
|
1569
|
+
const info = await handle.stat({ bigint: true });
|
|
1570
|
+
const byteSize = Number(info.size);
|
|
1571
|
+
let described;
|
|
1572
|
+
if (descriptions !== void 0) {
|
|
1573
|
+
if (sameFile({ dev: info.dev, ino: info.ino }, descriptions.identity)) {
|
|
1574
|
+
voice.write(isTheDescriptionsFile(name2));
|
|
1575
|
+
return { ok: false };
|
|
1576
|
+
}
|
|
1577
|
+
const key = asKey(basename(options.file));
|
|
1578
|
+
const found = descriptions.entries.get(key);
|
|
1579
|
+
if (found === void 0 && descriptions.entries.size > 0) {
|
|
1580
|
+
voice.write(noEntryMatches(descriptions.name, basename(options.file)));
|
|
1581
|
+
return { ok: false };
|
|
1582
|
+
}
|
|
1583
|
+
const namingNothing = [...descriptions.entries].filter(([other]) => other !== key).flatMap(([, other]) => other.keys);
|
|
1584
|
+
if (namingNothing.length > 0) voice.write(namingNoFile(namingNothing.sort()));
|
|
1585
|
+
if (found !== void 0) {
|
|
1586
|
+
const checked = checkEntry(found);
|
|
1587
|
+
if ("wrong" in checked) {
|
|
1588
|
+
voice.write(`${name2} \u2014 not sent: ${checked.wrong}`);
|
|
1589
|
+
return { ok: false };
|
|
1590
|
+
}
|
|
1591
|
+
described = checked.described;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1092
1594
|
const auth2 = deps.auth ?? await resolveAuth(options);
|
|
1093
1595
|
if (auth2 === null) {
|
|
1094
1596
|
voice.write(CANNOT_REACH_DAZZER);
|
|
1095
1597
|
return { ok: false };
|
|
1096
1598
|
}
|
|
1097
|
-
const settled = await settleSpace(options, auth2, deps, voice,
|
|
1599
|
+
const settled = await settleSpace(options, auth2, deps, voice, name2);
|
|
1098
1600
|
if (settled === null) return { ok: false };
|
|
1099
1601
|
const outcome = await sendOneSafely({
|
|
1100
1602
|
path: options.file,
|
|
1101
1603
|
handle,
|
|
1102
1604
|
byteSize,
|
|
1103
1605
|
scopeId: settled.scopeId,
|
|
1606
|
+
...described === void 0 ? {} : { described },
|
|
1104
1607
|
options,
|
|
1105
1608
|
deps,
|
|
1106
1609
|
auth: auth2,
|
|
1107
1610
|
voice
|
|
1108
1611
|
});
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
return { ok: true };
|
|
1113
|
-
case "already-here":
|
|
1114
|
-
voice.write(ALREADY_IN);
|
|
1115
|
-
return { ok: true };
|
|
1116
|
-
// ONE FILE, ONE QUESTION, IN THE SERVICE'S OWN WORDS. It is the only thing that knows what it
|
|
1117
|
-
// already holds and what the space is called, and rewriting its question here would be a
|
|
1118
|
-
// second wording of a question somebody approved. Nothing was added and nobody has answered,
|
|
1119
|
-
// so this run did not do what it was asked to — the same answer a run that asked where things
|
|
1120
|
-
// go gives.
|
|
1121
|
-
case "held":
|
|
1122
|
-
voice.write(outcome.question);
|
|
1123
|
-
return { ok: false };
|
|
1124
|
-
// ONE FILE, ONE REFUSAL, AND THE PLACES IT OFFERED. The sentence is the service's and is
|
|
1125
|
-
// printed as sent; the places under it are the service's too, and without them a refusal
|
|
1126
|
-
// that says to name one of them is a dead end for the person holding this one file.
|
|
1127
|
-
case "refused":
|
|
1128
|
-
voice.write(outcome.said ?? wentWrong(name));
|
|
1129
|
-
offerThePlaces(voice.write, outcome);
|
|
1130
|
-
return { ok: false };
|
|
1131
|
-
case "did-not-finish":
|
|
1132
|
-
voice.write(didNotFinish(name));
|
|
1133
|
-
return { ok: false };
|
|
1134
|
-
case "not-reached":
|
|
1135
|
-
voice.write(CANNOT_REACH_DAZZER);
|
|
1136
|
-
return { ok: false };
|
|
1137
|
-
case "went-wrong":
|
|
1138
|
-
voice.write(wentWrong(name));
|
|
1139
|
-
return { ok: false };
|
|
1140
|
-
// IT OPENED AND WOULD NOT READ — answered in the approved sentence for a file that cannot be
|
|
1141
|
-
// opened, because that is the same thing a person can act on, and never as a crash.
|
|
1142
|
-
case "cannot-read":
|
|
1143
|
-
voice.write(cannotOpen(name));
|
|
1144
|
-
return { ok: false };
|
|
1145
|
-
case "stop":
|
|
1146
|
-
say(voice.write, outcome.answer, name);
|
|
1147
|
-
return { ok: false };
|
|
1612
|
+
const result = answerForOneFile(outcome, name2, voice);
|
|
1613
|
+
if (outcome.kind === "added" && descriptions !== void 0 && !("description" in (described ?? {}))) {
|
|
1614
|
+
voice.write(itHadNoDescription);
|
|
1148
1615
|
}
|
|
1616
|
+
if (outcome.kind === "already-here" && outcome.description !== void 0) {
|
|
1617
|
+
voice.write(
|
|
1618
|
+
outcome.description === "filed" ? itsDescriptionFiledNow : outcome.description === "already-described" ? itAlreadyHasOne : itsDescriptionNotKept
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
if (outcome.kind === "added" || outcome.kind === "already-here") {
|
|
1622
|
+
for (const line of outcome.notKept) voice.write(`${name2}: ${line}`);
|
|
1623
|
+
}
|
|
1624
|
+
return result;
|
|
1149
1625
|
} finally {
|
|
1150
1626
|
await handle.close();
|
|
1151
1627
|
}
|
|
1152
1628
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1629
|
+
function answerForOneFile(outcome, name2, voice) {
|
|
1630
|
+
switch (outcome.kind) {
|
|
1631
|
+
case "added":
|
|
1632
|
+
voice.write(`${outcome.fileName} is in Dazzer, in ${named(outcome.space)}.`);
|
|
1633
|
+
return { ok: true };
|
|
1634
|
+
case "already-here":
|
|
1635
|
+
voice.write(ALREADY_IN);
|
|
1636
|
+
return { ok: true };
|
|
1637
|
+
// ONE FILE, ONE QUESTION, IN THE SERVICE'S OWN WORDS. It is the only thing that knows what it
|
|
1638
|
+
// already holds and what the space is called, and rewriting its question here would be a
|
|
1639
|
+
// second wording of a question somebody approved. Nothing was added and nobody has answered,
|
|
1640
|
+
// so this run did not do what it was asked to — the same answer a run that asked where things
|
|
1641
|
+
// go gives.
|
|
1642
|
+
case "held":
|
|
1643
|
+
voice.write(outcome.question);
|
|
1644
|
+
return { ok: false };
|
|
1645
|
+
// ONE FILE, ONE REFUSAL, AND THE PLACES IT OFFERED. The sentence is the service's and is
|
|
1646
|
+
// printed as sent; the places under it are the service's too, and without them a refusal
|
|
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);
|
|
1656
|
+
offerThePlaces(voice.write, outcome);
|
|
1657
|
+
return { ok: false };
|
|
1658
|
+
}
|
|
1659
|
+
case "did-not-finish":
|
|
1660
|
+
voice.write(didNotFinish(name2));
|
|
1661
|
+
return { ok: false };
|
|
1662
|
+
case "not-reached":
|
|
1663
|
+
voice.write(CANNOT_REACH_DAZZER);
|
|
1664
|
+
return { ok: false };
|
|
1665
|
+
case "went-wrong":
|
|
1666
|
+
voice.write(wentWrong(name2));
|
|
1667
|
+
return { ok: false };
|
|
1668
|
+
// IT OPENED AND WOULD NOT READ — answered in the approved sentence for a file that cannot be
|
|
1669
|
+
// opened, because that is the same thing a person can act on, and never as a crash.
|
|
1670
|
+
case "cannot-read":
|
|
1671
|
+
voice.write(cannotOpen(name2));
|
|
1672
|
+
return { ok: false };
|
|
1673
|
+
case "stop":
|
|
1674
|
+
say(voice.write, outcome.answer, name2);
|
|
1675
|
+
return { ok: false };
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
async function addFolder(options, deps, voice, descriptions) {
|
|
1679
|
+
const name2 = asPrinted(basename(options.file));
|
|
1155
1680
|
const walked = await (async () => {
|
|
1156
1681
|
try {
|
|
1157
1682
|
return await walkFolder(options.file);
|
|
@@ -1160,7 +1685,30 @@ async function addFolder(options, deps, voice) {
|
|
|
1160
1685
|
}
|
|
1161
1686
|
})();
|
|
1162
1687
|
if (walked === null) {
|
|
1163
|
-
voice.write(cannotOpen(
|
|
1688
|
+
voice.write(cannotOpen(name2));
|
|
1689
|
+
return { ok: false };
|
|
1690
|
+
}
|
|
1691
|
+
const found = new Set([...walked.files, ...walked.skipped].map((one) => asKey(one.relative)));
|
|
1692
|
+
const entries = [...descriptions?.entries ?? /* @__PURE__ */ new Map()];
|
|
1693
|
+
const namingNothing = entries.filter(([key]) => !found.has(key)).flatMap(([, entry]) => entry.keys).sort();
|
|
1694
|
+
if (descriptions !== void 0 && entries.length > 0 && entries.every(([key]) => !found.has(key))) {
|
|
1695
|
+
voice.write(
|
|
1696
|
+
noEntryMatches(
|
|
1697
|
+
descriptions.name,
|
|
1698
|
+
await aKeyItWouldMatch(walked.files, descriptions.identity)
|
|
1699
|
+
)
|
|
1700
|
+
);
|
|
1701
|
+
return { ok: false };
|
|
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);
|
|
1164
1712
|
return { ok: false };
|
|
1165
1713
|
}
|
|
1166
1714
|
const auth2 = deps.auth ?? await resolveAuth(options);
|
|
@@ -1168,7 +1716,7 @@ async function addFolder(options, deps, voice) {
|
|
|
1168
1716
|
voice.write(CANNOT_REACH_DAZZER);
|
|
1169
1717
|
return { ok: false };
|
|
1170
1718
|
}
|
|
1171
|
-
const settled = await settleSpace(options, auth2, deps, voice,
|
|
1719
|
+
const settled = await settleSpace(options, auth2, deps, voice, name2);
|
|
1172
1720
|
if (settled === null) return { ok: false };
|
|
1173
1721
|
const skipped = walked.skipped.map((skip) => ({
|
|
1174
1722
|
relative: skip.relative,
|
|
@@ -1177,7 +1725,33 @@ async function addFolder(options, deps, voice) {
|
|
|
1177
1725
|
const held = [];
|
|
1178
1726
|
let added = 0;
|
|
1179
1727
|
let ran = true;
|
|
1728
|
+
const notKept = [];
|
|
1729
|
+
const alreadyThere = { filed: 0, alreadyDescribed: 0, notKept: 0 };
|
|
1180
1730
|
for (const file of walked.files) {
|
|
1731
|
+
let described;
|
|
1732
|
+
const entry = descriptions?.entries.get(asKey(file.relative));
|
|
1733
|
+
if (entry !== void 0) {
|
|
1734
|
+
const checked = checkEntry(entry);
|
|
1735
|
+
if ("wrong" in checked) {
|
|
1736
|
+
skipped.push({
|
|
1737
|
+
relative: file.relative,
|
|
1738
|
+
reason: "entry-wrong-shape",
|
|
1739
|
+
said: `not sent: ${checked.wrong}`
|
|
1740
|
+
});
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
described = checked.described;
|
|
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
|
+
}
|
|
1181
1755
|
const handle = await openOne(file.path);
|
|
1182
1756
|
if (typeof handle === "string") {
|
|
1183
1757
|
skipped.push({ relative: file.relative, reason: "cannot-open" });
|
|
@@ -1185,11 +1759,13 @@ async function addFolder(options, deps, voice) {
|
|
|
1185
1759
|
}
|
|
1186
1760
|
let outcome;
|
|
1187
1761
|
try {
|
|
1188
|
-
|
|
1762
|
+
const info = await handle.stat({ bigint: true });
|
|
1763
|
+
outcome = descriptions !== void 0 && sameFile({ dev: info.dev, ino: info.ino }, descriptions.identity) ? "the-descriptions-file" : await sendOneSafely({
|
|
1189
1764
|
path: file.path,
|
|
1190
1765
|
handle,
|
|
1191
|
-
byteSize: (
|
|
1766
|
+
byteSize: Number(info.size),
|
|
1192
1767
|
scopeId: settled.scopeId,
|
|
1768
|
+
...described === void 0 ? {} : { described },
|
|
1193
1769
|
options,
|
|
1194
1770
|
deps,
|
|
1195
1771
|
auth: auth2,
|
|
@@ -1198,6 +1774,18 @@ async function addFolder(options, deps, voice) {
|
|
|
1198
1774
|
} finally {
|
|
1199
1775
|
await handle.close();
|
|
1200
1776
|
}
|
|
1777
|
+
if (outcome === "the-descriptions-file") {
|
|
1778
|
+
skipped.push({ relative: file.relative, reason: "the-descriptions-file" });
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if (outcome.kind === "added" || outcome.kind === "already-here") {
|
|
1782
|
+
if (outcome.notKept.length > 0)
|
|
1783
|
+
notKept.push({ relative: file.relative, lines: outcome.notKept });
|
|
1784
|
+
}
|
|
1785
|
+
if (outcome.kind === "already-here" && outcome.description !== void 0) {
|
|
1786
|
+
const answer = outcome.description;
|
|
1787
|
+
alreadyThere[answer === "filed" ? "filed" : answer === "already-described" ? "alreadyDescribed" : "notKept"] += 1;
|
|
1788
|
+
}
|
|
1201
1789
|
if (outcome.kind === "added") {
|
|
1202
1790
|
added += 1;
|
|
1203
1791
|
continue;
|
|
@@ -1215,12 +1803,19 @@ async function addFolder(options, deps, voice) {
|
|
|
1215
1803
|
continue;
|
|
1216
1804
|
}
|
|
1217
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";
|
|
1218
1808
|
skipped.push({
|
|
1219
1809
|
relative: file.relative,
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1810
|
+
reason,
|
|
1811
|
+
...outcome.said === void 0 ? {} : { said: outcome.said },
|
|
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 } : {}
|
|
1224
1819
|
});
|
|
1225
1820
|
continue;
|
|
1226
1821
|
}
|
|
@@ -1233,15 +1828,30 @@ async function addFolder(options, deps, voice) {
|
|
|
1233
1828
|
continue;
|
|
1234
1829
|
}
|
|
1235
1830
|
if (outcome.kind === "not-reached") voice.write(CANNOT_REACH_DAZZER);
|
|
1236
|
-
else say(voice.write, outcome.answer,
|
|
1831
|
+
else say(voice.write, outcome.answer, name2);
|
|
1237
1832
|
ran = false;
|
|
1238
1833
|
break;
|
|
1239
1834
|
}
|
|
1240
1835
|
skipped.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
|
|
1241
1836
|
held.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
|
|
1837
|
+
notKept.sort((a, b) => a.relative < b.relative ? -1 : a.relative > b.relative ? 1 : 0);
|
|
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
|
+
}
|
|
1242
1842
|
voice.write(countLine(added, settled.space, skipped, options.why === true));
|
|
1843
|
+
if (skipped.some((skip) => skip.reason === "the-descriptions-file")) voice.write(keepItBeside);
|
|
1844
|
+
if (alreadyThere.filed + alreadyThere.alreadyDescribed + alreadyThere.notKept > 0) {
|
|
1845
|
+
voice.write(alreadyThereDescriptions(alreadyThere));
|
|
1846
|
+
}
|
|
1847
|
+
if (namingNothing.length > 0) voice.write(namingNoFile(namingNothing));
|
|
1243
1848
|
if (held.length > 0) voice.write(waitingOnYou(held.length));
|
|
1244
|
-
|
|
1849
|
+
for (const skip of skipped) {
|
|
1850
|
+
if (options.why === true || namedWithoutWhy(skip)) voice.write(whyLine(skip));
|
|
1851
|
+
}
|
|
1852
|
+
for (const one of notKept) {
|
|
1853
|
+
for (const line of one.lines) voice.write(`${asPrinted(one.relative)}: ${line}`);
|
|
1854
|
+
}
|
|
1245
1855
|
const onlyOne = held.length === 1 ? held[0] : void 0;
|
|
1246
1856
|
if (onlyOne !== void 0) {
|
|
1247
1857
|
voice.write(onlyOne.question);
|
|
@@ -1253,7 +1863,25 @@ async function addFolder(options, deps, voice) {
|
|
|
1253
1863
|
)
|
|
1254
1864
|
);
|
|
1255
1865
|
}
|
|
1256
|
-
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
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async function aKeyItWouldMatch(files, descriptionsFile) {
|
|
1877
|
+
for (const file of files) {
|
|
1878
|
+
try {
|
|
1879
|
+
const info = await statPath(file.path, { bigint: true });
|
|
1880
|
+
if (!sameFile({ dev: info.dev, ino: info.ino }, descriptionsFile)) return file.relative;
|
|
1881
|
+
} catch {
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
return void 0;
|
|
1257
1885
|
}
|
|
1258
1886
|
function offerThePlaces(write, offer) {
|
|
1259
1887
|
const places = offer.choices ?? [];
|
|
@@ -1262,10 +1890,10 @@ function offerThePlaces(write, offer) {
|
|
|
1262
1890
|
for (const place of places) write(choiceLine(place.space, place.scopeId));
|
|
1263
1891
|
write(answerWith(keepToYourself, places.length > 0));
|
|
1264
1892
|
}
|
|
1265
|
-
function say(write, answer,
|
|
1893
|
+
function say(write, answer, name2) {
|
|
1266
1894
|
switch (answer.status) {
|
|
1267
1895
|
case "refused":
|
|
1268
|
-
write(answer.detail ?? wentWrong(
|
|
1896
|
+
write(answer.detail ?? wentWrong(name2));
|
|
1269
1897
|
offerThePlaces(write, answer);
|
|
1270
1898
|
return;
|
|
1271
1899
|
case "sign-in-invalid":
|
|
@@ -1978,23 +2606,40 @@ function signInServerDefaults() {
|
|
|
1978
2606
|
|
|
1979
2607
|
// src/index.ts
|
|
1980
2608
|
var require2 = createRequire(import.meta.url);
|
|
1981
|
-
var { version } = require2("../package.json");
|
|
2609
|
+
var { name, version } = require2("../package.json");
|
|
2610
|
+
var client = `${name}/${version}`;
|
|
1982
2611
|
var program = new Command();
|
|
1983
2612
|
program.name("dazzer").description("Sign in to Dazzer from your terminal").version(version);
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
issuer: env.DAZZER_AUTH_ISSUER,
|
|
1993
|
-
clientId: env.DAZZER_AUTH_CLIENT_ID,
|
|
1994
|
-
resource: env.DAZZER_AUTH_RESOURCE
|
|
1995
|
-
});
|
|
1996
|
-
if (!result.ok) process.exitCode = 1;
|
|
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
|
+
`);
|
|
1997
2621
|
});
|
|
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(
|
|
2623
|
+
"--described-in <file>",
|
|
2624
|
+
"A JSON file saying what each file is, keyed by its path inside the folder"
|
|
2625
|
+
).action(
|
|
2626
|
+
async (file, opts) => {
|
|
2627
|
+
const env = validateEnv();
|
|
2628
|
+
const result = await runAdd({
|
|
2629
|
+
file,
|
|
2630
|
+
space: opts.space,
|
|
2631
|
+
keepToMyself: opts.keepToMyself === true,
|
|
2632
|
+
why: opts.why === true,
|
|
2633
|
+
...opts.describedIn === void 0 ? {} : { describedIn: opts.describedIn },
|
|
2634
|
+
client,
|
|
2635
|
+
baseUrl: env.DAZZER_API_BASE_URL,
|
|
2636
|
+
issuer: env.DAZZER_AUTH_ISSUER,
|
|
2637
|
+
clientId: env.DAZZER_AUTH_CLIENT_ID,
|
|
2638
|
+
resource: env.DAZZER_AUTH_RESOURCE
|
|
2639
|
+
});
|
|
2640
|
+
if (!result.ok) process.exitCode = 1;
|
|
2641
|
+
}
|
|
2642
|
+
);
|
|
1998
2643
|
var auth = program.command("auth").description("Manage sign-in");
|
|
1999
2644
|
auth.command("login").description("Sign in through your browser; falls back to a device code when headless").option("--issuer <url>", "Authorization server URL").option("--client-id <id>", "OAuth client id").option("--resource <url>", "Resource indicator for access tokens").option("--no-browser", "Use the device-code flow instead of opening a browser").action(
|
|
2000
2645
|
async (opts) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dazzer-connect",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "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",
|
|
@@ -34,16 +34,16 @@
|
|
|
34
34
|
"dev": "tsx src/index.ts"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@napi-rs/keyring": "^2.
|
|
37
|
+
"@napi-rs/keyring": "^2.1.0",
|
|
38
38
|
"commander": "^15.0.0",
|
|
39
39
|
"env-paths": "^4.0.0",
|
|
40
|
-
"zod": "^4.6.
|
|
40
|
+
"zod": "^4.6.5"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@dazzer/auth-ui": "workspace:*",
|
|
44
|
-
"@types/node": "^26.
|
|
44
|
+
"@types/node": "^26.6.2",
|
|
45
45
|
"esbuild": "^0.28.2",
|
|
46
|
-
"tsx": "^4.23.
|
|
46
|
+
"tsx": "^4.23.15",
|
|
47
47
|
"typescript": "^5.7.2",
|
|
48
48
|
"vitest": "^4.1.11"
|
|
49
49
|
},
|