@wirecat/cli-core 0.506.0 → 0.507.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/CHANGELOG.md +23 -0
- package/dist/cli/messenger/backfill-command.js +26 -3
- package/dist/oauth/keeper.js +2 -2
- package/dist/oauth/storage.d.ts +3 -2
- package/dist/oauth/storage.js +3 -3
- package/dist/services/archive.d.ts +6 -0
- package/dist/services/archive.js +4 -0
- package/dist/terminal/sign-in-steps.js +2 -4
- package/dist/terminal/step-lines.d.ts +7 -0
- package/dist/terminal/step-lines.js +8 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.507.0 — 31.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`messages sync --json` prints its progress on stderr**, for a window to draw a bar instead of an endless one
|
|
15
|
+
(WireCat Desktop). One JSON line each, counts only — never a chat's id or title:
|
|
16
|
+
`{"event":"progress","step":"sync","chats":3,"chats_done":1,"fetched":250}`. With `--all`, one once the chat list
|
|
17
|
+
is read (`chats_done` 0), then one after each chat, those passed as already current included; for one chat, one per
|
|
18
|
+
page with `chats` and `chats_done` `null`. At most about two a second; the first and last of `--all` always come.
|
|
19
|
+
Only in machine output (`--json`, `--jsonl`, or stdout not a terminal) and not in a `--background` job's log;
|
|
20
|
+
stdout still carries the one answer. Archive services take an optional `onChat` for the same counts.
|
|
21
|
+
|
|
22
|
+
### Changed — may break callers
|
|
23
|
+
|
|
24
|
+
- **Needs `@wirecat/cli-base` 0.68.0** (peer range `>=0.68.0 <0.69.0`). cli-base's `Credentials` in `auto` now reads
|
|
25
|
+
a sign-in the private file holds before the keyring and keeps its refreshes there (WC-SECR-4).
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **A new OAuth sign-in moves a file sign-in back to the keyring.** `TokenStorage.write` takes `{ signIn: true }`, and
|
|
30
|
+
the keeper's `signIn` and `save` pass it: the keyring is tried again and, once it takes the sign-in, the file copy
|
|
31
|
+
is removed. A refresh stays where the sign-in is, so a locked keyring is not tried for it on every refresh.
|
|
32
|
+
|
|
10
33
|
## 0.506.0 — 31.10.2026
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -12,6 +12,7 @@ import { JOBS_QUERY, jobItems } from "../../services/job-query.js";
|
|
|
12
12
|
import { dateWindow } from "../../services/moment.js";
|
|
13
13
|
import { validateCatchUpBounds } from "../../services/search-catchup.js";
|
|
14
14
|
import { applyTaskRulesSinceLastPass } from "../../services/task-rules.js";
|
|
15
|
+
import { stepLines } from "../../terminal/step-lines.js";
|
|
15
16
|
import { environmentOf } from "../context.js";
|
|
16
17
|
import { destroysNothing, FETCH, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
|
|
17
18
|
import { itemOf, listOf, renderAnswer, snakeKeys } from "../envelope.js";
|
|
@@ -41,7 +42,8 @@ export const syncCommand = (messenger) => {
|
|
|
41
42
|
cooperative: true,
|
|
42
43
|
})
|
|
43
44
|
.summary("fetch and save a chat's message history")
|
|
44
|
-
.description("bring a chat's history in the local store up to date, newest first; run it again to continue; --all syncs every chat"
|
|
45
|
+
.description("bring a chat's history in the local store up to date, newest first; run it again to continue; --all syncs every chat; " +
|
|
46
|
+
'with --json, stderr carries progress lines: {"event":"progress","step":"sync","chats","chats_done","fetched"}, counts only')
|
|
45
47
|
.argument("[chat]", messenger.chatArgument)
|
|
46
48
|
.option("--all", `every chat, most recently active first — what search needs; the last ${ALL_SINCE} unless --since-time or --last`)
|
|
47
49
|
.option("--limit <n>", `at most this many messages in this run, per chat with --all; ${fetching.maxPages * fetching.page} if not given`, wholeNumber)
|
|
@@ -139,6 +141,8 @@ export const syncCommand = (messenger) => {
|
|
|
139
141
|
const jobs = jobsDir(messenger.app, context.env);
|
|
140
142
|
const jobId = context.env[envName(messenger.app, "BACKFILL_JOB")];
|
|
141
143
|
const stop = stopOnSignal(this);
|
|
144
|
+
// A background job keeps its log as it was; its progress is the job file.
|
|
145
|
+
const progress = syncProgress(stepLines(context.streams.diagnostic, !jobId && context.format !== "pretty"));
|
|
142
146
|
try {
|
|
143
147
|
const options = {
|
|
144
148
|
limit,
|
|
@@ -149,10 +153,13 @@ export const syncCommand = (messenger) => {
|
|
|
149
153
|
...(last === undefined ? {} : { last }),
|
|
150
154
|
note: context.renderer.note,
|
|
151
155
|
stop: stop.signal,
|
|
152
|
-
onPage: (
|
|
156
|
+
onPage: (page) => {
|
|
153
157
|
if (jobId)
|
|
154
|
-
updateJob(jobs, jobId, { progress: { ...
|
|
158
|
+
updateJob(jobs, jobId, { progress: { ...page, at: new Date().toISOString() } });
|
|
159
|
+
if (chat !== undefined)
|
|
160
|
+
progress({ chats: null, chats_done: null, fetched: page.fetched });
|
|
155
161
|
},
|
|
162
|
+
onChat: ({ chats, done, fetched }) => progress({ chats, chats_done: done, fetched }, done === 0 || done === chats),
|
|
156
163
|
};
|
|
157
164
|
const result = await context.withServices((services) => chat === undefined ? services.archive.fetchAll(options) : services.archive.fetch(chat, options));
|
|
158
165
|
if (jobId)
|
|
@@ -197,6 +204,22 @@ export const syncCommand = (messenger) => {
|
|
|
197
204
|
}
|
|
198
205
|
});
|
|
199
206
|
};
|
|
207
|
+
/** At most this often a progress line, so a run of fast pages or of chats already current does not flood the window. */
|
|
208
|
+
const PROGRESS_EVERY_MS = 500;
|
|
209
|
+
/**
|
|
210
|
+
* `messages sync`'s progress for a window to draw a bar by: counts only, because the lines reach a desktop app and
|
|
211
|
+
* its logs — never a chat's id or title. `always` keeps the first and the last line of `--all` through the throttle.
|
|
212
|
+
*/
|
|
213
|
+
const syncProgress = (emit) => {
|
|
214
|
+
let last = Number.NEGATIVE_INFINITY;
|
|
215
|
+
return (counts, always = false) => {
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
if (!always && now - last < PROGRESS_EVERY_MS)
|
|
218
|
+
return;
|
|
219
|
+
last = now;
|
|
220
|
+
emit({ event: "progress", step: "sync", ...counts });
|
|
221
|
+
};
|
|
222
|
+
};
|
|
200
223
|
/**
|
|
201
224
|
* What the sync stored, through the task rules (owner, 2026-10-14). A failure is noted, never the sync's failure:
|
|
202
225
|
* the messages are already kept, and the next sync's pass starts from the same stop point.
|
package/dist/oauth/keeper.js
CHANGED
|
@@ -137,13 +137,13 @@ export const tokenKeeper = (options) => {
|
|
|
137
137
|
const extra = (await accept?.(received)) ?? undefined;
|
|
138
138
|
await locked(async () => {
|
|
139
139
|
forget();
|
|
140
|
-
storage.write({ tokens: received, client, ...(extra ? { extra } : {}) });
|
|
140
|
+
storage.write({ tokens: received, client, ...(extra ? { extra } : {}) }, { signIn: true });
|
|
141
141
|
});
|
|
142
142
|
return { scopes: received.scopes };
|
|
143
143
|
},
|
|
144
144
|
save: (saved, extra) => locked(async () => {
|
|
145
145
|
forget();
|
|
146
|
-
storage.write({ tokens: saved, client: clientOf(storage.read()), ...(extra ? { extra } : {}) });
|
|
146
|
+
storage.write({ tokens: saved, client: clientOf(storage.read()), ...(extra ? { extra } : {}) }, { signIn: true });
|
|
147
147
|
}),
|
|
148
148
|
signOut: ({ revoke = false } = {}) => locked(async () => {
|
|
149
149
|
forget();
|
package/dist/oauth/storage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CredentialSource, type CredentialsOptions } from "@wirecat/cli-base";
|
|
1
|
+
import { type CredentialSource, type CredentialsOptions, type WriteOptions } from "@wirecat/cli-base";
|
|
2
2
|
import type { OAuthClient, TokenSet } from "./provider.js";
|
|
3
3
|
export interface StoredTokens {
|
|
4
4
|
tokens: TokenSet;
|
|
@@ -29,7 +29,8 @@ export interface TokenStorage {
|
|
|
29
29
|
/** Throws when the entry cannot be saved, so a sign-in fails before the browser, not after consent. */
|
|
30
30
|
probe(): void;
|
|
31
31
|
read(): StoredTokens | undefined;
|
|
32
|
-
|
|
32
|
+
/** `signIn` moves an entry the file holds back to the keyring when it takes it (WC-SECR-4); a refresh leaves it. */
|
|
33
|
+
write(input: Omit<EncodeInput, "previous">, options?: WriteOptions): void;
|
|
33
34
|
remove(): CredentialSource[];
|
|
34
35
|
}
|
|
35
36
|
export declare const oauthAccount: (providerId: string, profile: string) => string;
|
package/dist/oauth/storage.js
CHANGED
|
@@ -162,9 +162,9 @@ export const credentialStorage = (options) => {
|
|
|
162
162
|
throw keyringFailure(error);
|
|
163
163
|
}
|
|
164
164
|
};
|
|
165
|
-
const save = (value) => {
|
|
165
|
+
const save = (value, writeOptions) => {
|
|
166
166
|
try {
|
|
167
|
-
credentials.write(account, value);
|
|
167
|
+
credentials.write(account, value, writeOptions);
|
|
168
168
|
}
|
|
169
169
|
catch (error) {
|
|
170
170
|
throw keyringFailure(error, value);
|
|
@@ -195,7 +195,7 @@ export const credentialStorage = (options) => {
|
|
|
195
195
|
const value = current();
|
|
196
196
|
return value === undefined ? undefined : codec.decode(value);
|
|
197
197
|
},
|
|
198
|
-
write: (input) => save(codec.encode({ ...input, previous: raw(account) })),
|
|
198
|
+
write: (input, writeOptions) => save(codec.encode({ ...input, previous: raw(account) }), writeOptions),
|
|
199
199
|
remove: () => {
|
|
200
200
|
try {
|
|
201
201
|
return credentials.remove(account);
|
|
@@ -39,6 +39,12 @@ export interface FetchOptions {
|
|
|
39
39
|
chatId: string;
|
|
40
40
|
oldest: number;
|
|
41
41
|
}) => void;
|
|
42
|
+
/** `fetchAll` only: once the chat list is read, then after each chat, current or not; counts across every chat. */
|
|
43
|
+
onChat?: (progress: {
|
|
44
|
+
chats: number;
|
|
45
|
+
done: number;
|
|
46
|
+
fetched: number;
|
|
47
|
+
}) => void;
|
|
42
48
|
}
|
|
43
49
|
/** A type, not an interface: a job keeps it as a plain record. */
|
|
44
50
|
export type Fetched = {
|
package/dist/services/archive.js
CHANGED
|
@@ -148,6 +148,8 @@ export const archiveService = (deps) => {
|
|
|
148
148
|
...options,
|
|
149
149
|
catchUp: catchingUp ? (options.catchUp ?? {}) : false,
|
|
150
150
|
});
|
|
151
|
+
const finished = () => options.onChat?.({ chats: chats.length, done: answer.items.length, fetched: answer.fetched });
|
|
152
|
+
finished();
|
|
151
153
|
for (const chat of chats) {
|
|
152
154
|
if (options.stop.aborted || batch.stopped) {
|
|
153
155
|
answer.stopped = true;
|
|
@@ -155,6 +157,7 @@ export const archiveService = (deps) => {
|
|
|
155
157
|
}
|
|
156
158
|
if (await upToDate(chat)) {
|
|
157
159
|
answer.items.push({ chat: chat.id, title: chat.title, fetched: 0, complete: true, current: true });
|
|
160
|
+
finished();
|
|
158
161
|
continue;
|
|
159
162
|
}
|
|
160
163
|
try {
|
|
@@ -183,6 +186,7 @@ export const archiveService = (deps) => {
|
|
|
183
186
|
const issue = batch.fail(chat.id, "history", error);
|
|
184
187
|
answer.items.push({ chat: chat.id, title: chat.title, fetched: 0, complete: false, error: issue.code, issue });
|
|
185
188
|
}
|
|
189
|
+
finished();
|
|
186
190
|
}
|
|
187
191
|
if (batch.failed > 0)
|
|
188
192
|
answer.batch = batch.result();
|
|
@@ -2,6 +2,7 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
3
|
import { Option } from "commander";
|
|
4
4
|
import { MAX_SECRET_INPUT } from "../cli/input-policy.js";
|
|
5
|
+
import { stepLines } from "./step-lines.js";
|
|
5
6
|
/**
|
|
6
7
|
* Opt-in, because a read nobody answers hangs: an agent running `session start` in a shell must get the refusal it
|
|
7
8
|
* gets today, and only a caller that holds stdin open and answers each `needs` line (WireCat Desktop) passes it.
|
|
@@ -10,10 +11,7 @@ export const answerStdinOption = () => new Option("--answer-stdin", "with --json
|
|
|
10
11
|
export const signInSteps = ({ diagnostic, json, answers, input = process.stdin }) => {
|
|
11
12
|
if (answers && !json)
|
|
12
13
|
throw new CliError("validation_error", "--answer-stdin needs --json");
|
|
13
|
-
const emit = (
|
|
14
|
-
if (json)
|
|
15
|
-
diagnostic(JSON.stringify(event));
|
|
16
|
-
};
|
|
14
|
+
const emit = stepLines(diagnostic, json);
|
|
17
15
|
const next = answers ? lineReader(input) : undefined;
|
|
18
16
|
return {
|
|
19
17
|
json,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-readable step lines: one JSON object per line on the diagnostic stream, only in `--json` / `--jsonl`, so
|
|
3
|
+
* stdout keeps its single answer and a person at a terminal sees nothing new. Sign-in and `messages sync` share it.
|
|
4
|
+
*/
|
|
5
|
+
export declare const stepLines: (diagnostic: (line: string) => void, json: boolean) => (event: Record<string, unknown> & {
|
|
6
|
+
event: string;
|
|
7
|
+
}) => void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-readable step lines: one JSON object per line on the diagnostic stream, only in `--json` / `--jsonl`, so
|
|
3
|
+
* stdout keeps its single answer and a person at a terminal sees nothing new. Sign-in and `messages sync` share it.
|
|
4
|
+
*/
|
|
5
|
+
export const stepLines = (diagnostic, json) => (event) => {
|
|
6
|
+
if (json)
|
|
7
|
+
diagnostic(JSON.stringify(event));
|
|
8
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.507.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -247,7 +247,7 @@
|
|
|
247
247
|
"@types/node": "^22.20.5",
|
|
248
248
|
"@types/pngjs": "6.0.5",
|
|
249
249
|
"@vitest/coverage-v8": "^5.0.5",
|
|
250
|
-
"@wirecat/cli-base": "0.
|
|
250
|
+
"@wirecat/cli-base": "0.68.0",
|
|
251
251
|
"@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
|
|
252
252
|
"commander": "15.0.0",
|
|
253
253
|
"cspell": "10.4.0",
|
|
@@ -267,7 +267,7 @@
|
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
269
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
270
|
-
"@wirecat/cli-base": ">=0.
|
|
270
|
+
"@wirecat/cli-base": ">=0.68.0 <0.69.0",
|
|
271
271
|
"commander": ">=15"
|
|
272
272
|
},
|
|
273
273
|
"bin": {
|