@wirecat/cli-core 0.463.0 → 0.465.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 +92 -0
- package/dist/background/after-upgrade.d.ts +51 -0
- package/dist/background/after-upgrade.js +245 -0
- package/dist/background/budget.d.ts +37 -0
- package/dist/background/budget.js +48 -0
- package/dist/background/index.d.ts +1 -0
- package/dist/background/index.js +1 -0
- package/dist/background/relay.js +5 -1
- package/dist/calendar/busy.d.ts +68 -0
- package/dist/calendar/busy.js +148 -0
- package/dist/calendar/cli/index.d.ts +7 -0
- package/dist/calendar/cli/index.js +98 -15
- package/dist/calendar/index.d.ts +3 -2
- package/dist/calendar/index.js +2 -1
- package/dist/calendar/mcp/index.d.ts +55 -13
- package/dist/calendar/mcp/index.js +19 -4
- package/dist/calendar/recurrence.d.ts +10 -0
- package/dist/calendar/recurrence.js +65 -0
- package/dist/calendar/source.d.ts +19 -0
- package/dist/calendar/sync.d.ts +86 -7
- package/dist/calendar/sync.js +271 -105
- package/dist/calendar/testing/source-contract.js +14 -0
- package/dist/calendar/testing/source.d.ts +6 -0
- package/dist/calendar/testing/source.js +30 -2
- package/dist/calendar/testing/sync-contract.d.ts +2 -1
- package/dist/calendar/testing/sync-contract.js +80 -5
- package/dist/calendar/testing/writer-contract.d.ts +2 -1
- package/dist/calendar/testing/writer-contract.js +46 -2
- package/dist/calendar/testing/writer.js +1 -1
- package/dist/calendar/writer.d.ts +7 -2
- package/dist/calendar/writes.d.ts +12 -2
- package/dist/calendar/writes.js +32 -6
- package/dist/cli/context.d.ts +8 -0
- package/dist/cli/context.js +15 -0
- package/dist/cli/execution.d.ts +7 -0
- package/dist/cli/execution.js +7 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/mcp-server-command.js +16 -10
- package/dist/cli/meeting-preparation-answer.d.ts +1 -0
- package/dist/cli/meeting-preparation-projects.d.ts +1 -0
- package/dist/cli/meeting-preparation-read.d.ts +1 -0
- package/dist/cli/messenger/chat-answers.js +1 -0
- package/dist/cli/messenger/search-answers.js +29 -4
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/program.js +15 -5
- package/dist/cli/upgrade-command.d.ts +7 -2
- package/dist/cli/upgrade-command.js +65 -9
- package/dist/entities/fields.d.ts +2 -0
- package/dist/entities/fields.js +2 -0
- package/dist/entities/hit.d.ts +1 -1
- package/dist/entities/hit.js +3 -3
- package/dist/entities/index.d.ts +10 -800
- package/dist/entities/index.js +37 -31
- package/dist/entities/meeting-preparation.d.ts +1 -0
- package/dist/entities/topic.d.ts +1 -0
- package/dist/entities/topic.js +2 -1
- package/dist/errors/reasons.js +4 -0
- package/dist/mcp/http/state.d.ts +14 -0
- package/dist/mcp/http/state.js +16 -0
- package/dist/mcp/meeting-preparation.d.ts +1 -0
- package/dist/meetings/answers.d.ts +2 -0
- package/dist/meetings/answers.js +3 -0
- package/dist/meetings/mcp/search.d.ts +1 -0
- package/dist/meetings/project-evidence-answer.d.ts +1 -0
- package/dist/ports/app.d.ts +5 -0
- package/dist/services/package-upgrade.d.ts +9 -0
- package/dist/services/package-upgrade.js +8 -1
- package/package.json +3 -3
|
@@ -17,7 +17,8 @@ export interface CalendarWriterFixture {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* What every calendar writer keeps (gcal-cli plan §9, W1): a stale `etag` is a conflict, never an overwrite; a create
|
|
20
|
-
* retried with its id finds the event;
|
|
20
|
+
* retried with its id finds the event; a repeat rule is kept as given and changed on the series; a timed event
|
|
21
|
+
* switches to all-day and back; `notify: false` mails nobody, and a profile that keeps `events.notify` at its
|
|
21
22
|
* default never reaches the provider with a write that would.
|
|
22
23
|
*/
|
|
23
24
|
export declare const calendarWriterContract: (fixture: () => CalendarWriterFixture | Promise<CalendarWriterFixture>) => ContractCase[];
|
|
@@ -17,9 +17,12 @@ const rejectsWith = async (promise, check, message) => {
|
|
|
17
17
|
assert.fail(`${message}, but it succeeded`);
|
|
18
18
|
};
|
|
19
19
|
const ID = "contract0sample0id0001";
|
|
20
|
+
const WEEKLY = "RRULE:FREQ=WEEKLY;COUNT=3";
|
|
21
|
+
const DAILY = "RRULE:FREQ=DAILY;INTERVAL=2;COUNT=5";
|
|
20
22
|
/**
|
|
21
23
|
* What every calendar writer keeps (gcal-cli plan §9, W1): a stale `etag` is a conflict, never an overwrite; a create
|
|
22
|
-
* retried with its id finds the event;
|
|
24
|
+
* retried with its id finds the event; a repeat rule is kept as given and changed on the series; a timed event
|
|
25
|
+
* switches to all-day and back; `notify: false` mails nobody, and a profile that keeps `events.notify` at its
|
|
23
26
|
* default never reaches the provider with a write that would.
|
|
24
27
|
*/
|
|
25
28
|
export const calendarWriterContract = (fixture) => {
|
|
@@ -75,6 +78,47 @@ export const calendarWriterContract = (fixture) => {
|
|
|
75
78
|
: f.writer.respond(f.calendar, id, "declined", options), (error) => reasonOf(error) === "event.conflict", "a stale etag is a calendarConflict()");
|
|
76
79
|
},
|
|
77
80
|
})),
|
|
81
|
+
{
|
|
82
|
+
name: "a create with a repeat rule returns the series holding that rule",
|
|
83
|
+
run: async () => {
|
|
84
|
+
const f = await fixture();
|
|
85
|
+
const made = await f.writer.create(f.calendar, draft(f.at, { recurrence: [WEEKLY] }), {
|
|
86
|
+
id: ID,
|
|
87
|
+
notify: false,
|
|
88
|
+
});
|
|
89
|
+
assert.equal(made.kind, "series");
|
|
90
|
+
assert.deepEqual(made.recurrence, [WEEKLY]);
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "a change of the series' rule replaces it",
|
|
95
|
+
run: async () => {
|
|
96
|
+
const f = await fixture();
|
|
97
|
+
const made = await f.writer.create(f.calendar, draft(f.at, { recurrence: [WEEKLY] }), {
|
|
98
|
+
id: ID,
|
|
99
|
+
notify: false,
|
|
100
|
+
});
|
|
101
|
+
const changed = await f.writer.update(f.calendar, made.externalId, { recurrence: [DAILY] }, { scope: "series", etag: made.etag, notify: false });
|
|
102
|
+
assert.deepEqual(changed.recurrence, [DAILY]);
|
|
103
|
+
assert.notEqual(changed.etag, made.etag);
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "a timed event becomes all-day, and back",
|
|
108
|
+
run: async () => {
|
|
109
|
+
const f = await fixture();
|
|
110
|
+
const made = await f.writer.create(f.calendar, draft(f.at), { id: ID, notify: false });
|
|
111
|
+
const day = new Date(f.at).toISOString().slice(0, 10);
|
|
112
|
+
const next = new Date(f.at + 86_400_000).toISOString().slice(0, 10);
|
|
113
|
+
const allDay = await f.writer.update(f.calendar, made.externalId, { startDate: day, endDate: next, timezone: "UTC" }, { scope: "this", etag: made.etag, notify: false });
|
|
114
|
+
assert.equal(allDay.allDay, true);
|
|
115
|
+
assert.equal(allDay.startDate, day);
|
|
116
|
+
const timed = await f.writer.update(f.calendar, made.externalId, { startsAt: f.at, endsAt: f.at + 3_600_000, timezone: "UTC" }, { scope: "this", etag: allDay.etag, notify: false });
|
|
117
|
+
assert.equal(timed.allDay ?? false, false);
|
|
118
|
+
assert.equal(timed.startsAt, f.at);
|
|
119
|
+
assert.equal(timed.startDate ?? null, null);
|
|
120
|
+
},
|
|
121
|
+
},
|
|
78
122
|
{
|
|
79
123
|
name: "notify reaches the provider as given: false mails nobody",
|
|
80
124
|
run: async () => {
|
|
@@ -122,7 +166,7 @@ export const calendarWriterContract = (fixture) => {
|
|
|
122
166
|
ask: async () => { },
|
|
123
167
|
now: f.at,
|
|
124
168
|
zone: "UTC",
|
|
125
|
-
names: { notify: "notify", id: "id", calendar: "calendar", series: "series" },
|
|
169
|
+
names: { notify: "notify", id: "id", calendar: "calendar", series: "series", repeat: "repeat" },
|
|
126
170
|
}, String(calendar?.id), {
|
|
127
171
|
title: "Example planning",
|
|
128
172
|
startTime: new Date(f.at).toISOString(),
|
|
@@ -3,7 +3,8 @@ import type { EntryInput, ParticipantResponse, StoredCalendar } from "../meeting
|
|
|
3
3
|
import type { Level, PermissionKey } from "../sends/permissions.js";
|
|
4
4
|
/**
|
|
5
5
|
* A new event in neutral words: a timed one has `startsAt` and `endsAt` (epoch ms, end exclusive) in `timezone`; an
|
|
6
|
-
* all-day one has `startDate` and `endDate` (`YYYY-MM-DD`, end exclusive).
|
|
6
|
+
* all-day one has `startDate` and `endDate` (`YYYY-MM-DD`, end exclusive). A repeating one has `recurrence`, RFC 5545
|
|
7
|
+
* lines as `EntryInput` holds them; its `timezone` is the zone the rule is expanded in, which Google requires.
|
|
7
8
|
*/
|
|
8
9
|
export interface EventDraft {
|
|
9
10
|
title: string;
|
|
@@ -18,8 +19,12 @@ export interface EventDraft {
|
|
|
18
19
|
email: string;
|
|
19
20
|
optional?: boolean;
|
|
20
21
|
}[];
|
|
22
|
+
recurrence?: string[];
|
|
21
23
|
}
|
|
22
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Only the fields to change; `null` clears one. `attendees` and `recurrence` replace the whole list. A patch with
|
|
26
|
+
* `startDate` for a timed event, or `startsAt` for an all-day one, switches it: the adapter clears the other kind.
|
|
27
|
+
*/
|
|
23
28
|
export type EventPatch = Partial<EventDraft>;
|
|
24
29
|
/** `this`: the one entry named; `series`: the repeating event it belongs to, named by the series' own id. */
|
|
25
30
|
export type WriteScope = "this" | "series";
|
|
@@ -29,6 +29,8 @@ export interface EventFields {
|
|
|
29
29
|
location?: string;
|
|
30
30
|
description?: string;
|
|
31
31
|
attendees?: string[];
|
|
32
|
+
/** An RFC 5545 rule, with or without `RRULE:`; on a change, only with the series' scope. */
|
|
33
|
+
repeat?: string;
|
|
32
34
|
}
|
|
33
35
|
export interface WriteContext {
|
|
34
36
|
store: EventStore;
|
|
@@ -44,11 +46,16 @@ export interface WriteContext {
|
|
|
44
46
|
id: string;
|
|
45
47
|
calendar: string;
|
|
46
48
|
series: string;
|
|
49
|
+
repeat: string;
|
|
47
50
|
};
|
|
48
51
|
}
|
|
49
|
-
/**
|
|
50
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Typed fields into a draft, or into a patch with only what was given. `allDay` is the kind of the event a patch
|
|
54
|
+
* changes, for a repeat rule's UNTIL when the patch keeps the times.
|
|
55
|
+
*/
|
|
56
|
+
export declare const draftOf: (fields: EventFields, zone: string, { whole, allDay }: {
|
|
51
57
|
whole: boolean;
|
|
58
|
+
allDay?: boolean;
|
|
52
59
|
}) => EventPatch;
|
|
53
60
|
/** An id every provider takes as a client-chosen one: Google's base32hex `[a-v0-9]`, a CalDAV UID, a Graph id. */
|
|
54
61
|
export declare const newEventId: () => string;
|
|
@@ -73,6 +80,7 @@ export declare const createEvent: (context: WriteContext, calendarReference: str
|
|
|
73
80
|
url: string | null;
|
|
74
81
|
deleted_at: string | null;
|
|
75
82
|
} | null;
|
|
83
|
+
hint?: string | undefined;
|
|
76
84
|
}>;
|
|
77
85
|
export declare const updateEvent: (context: WriteContext, reference: string, fields: EventFields, options?: {
|
|
78
86
|
calendar?: string;
|
|
@@ -96,6 +104,7 @@ export declare const updateEvent: (context: WriteContext, reference: string, fie
|
|
|
96
104
|
url: string | null;
|
|
97
105
|
deleted_at: string | null;
|
|
98
106
|
} | null;
|
|
107
|
+
hint?: string | undefined;
|
|
99
108
|
}>;
|
|
100
109
|
export declare const deleteEvent: (context: WriteContext, reference: string, options?: {
|
|
101
110
|
calendar?: string;
|
|
@@ -128,4 +137,5 @@ export declare const respondToEvent: (context: WriteContext, reference: string,
|
|
|
128
137
|
url: string | null;
|
|
129
138
|
deleted_at: string | null;
|
|
130
139
|
} | null;
|
|
140
|
+
hint?: string | undefined;
|
|
131
141
|
}>;
|
package/dist/calendar/writes.js
CHANGED
|
@@ -6,6 +6,7 @@ import { guardedWrite } from "../sends/guarded.js";
|
|
|
6
6
|
import { DEFAULT_PERMISSIONS, levelFor } from "../sends/permissions.js";
|
|
7
7
|
import { newOperationId } from "../sends/send-id.js";
|
|
8
8
|
import { calendarIn, entryItem, eventIdOf, eventOf, plusDays } from "./reads.js";
|
|
9
|
+
import { kindBoundLines, repeatRule } from "./recurrence.js";
|
|
9
10
|
import { CALENDAR_WRITE_DEFAULTS, EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY, } from "./writer.js";
|
|
10
11
|
export const EVENT_RESPONSES = ["accepted", "tentative", "declined"];
|
|
11
12
|
/** Each write is checked by its command's path, the key a declared CLI's `commandPermissions` already knows. */
|
|
@@ -24,8 +25,11 @@ const dateOf = (text, what) => {
|
|
|
24
25
|
return text;
|
|
25
26
|
};
|
|
26
27
|
const momentOf = (text, zone) => zonedMoment(text, zone, { start: 0, end: text.length });
|
|
27
|
-
/**
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Typed fields into a draft, or into a patch with only what was given. `allDay` is the kind of the event a patch
|
|
30
|
+
* changes, for a repeat rule's UNTIL when the patch keeps the times.
|
|
31
|
+
*/
|
|
32
|
+
export const draftOf = (fields, zone, { whole, allDay = false }) => {
|
|
29
33
|
const patch = {};
|
|
30
34
|
if (fields.title !== undefined) {
|
|
31
35
|
if (!fields.title.trim())
|
|
@@ -66,6 +70,12 @@ export const draftOf = (fields, zone, { whole }) => {
|
|
|
66
70
|
throw new CliError("validation_error", `${email} is not an email address`);
|
|
67
71
|
return { email };
|
|
68
72
|
});
|
|
73
|
+
if (fields.repeat !== undefined)
|
|
74
|
+
patch.recurrence = [
|
|
75
|
+
repeatRule(fields.repeat, {
|
|
76
|
+
allDay: patch.startDate !== undefined || (patch.startsAt === undefined && allDay),
|
|
77
|
+
}),
|
|
78
|
+
];
|
|
69
79
|
if (!whole && Object.keys(patch).length === 0)
|
|
70
80
|
throw new CliError("validation_error", "nothing to change");
|
|
71
81
|
return patch;
|
|
@@ -130,10 +140,12 @@ const keysFor = (context, write, notify) => [
|
|
|
130
140
|
...(context.writes.extraKeys?.(write) ?? []),
|
|
131
141
|
...(notify ? [EVENTS_NOTIFY_KEY] : []),
|
|
132
142
|
];
|
|
133
|
-
const answerOf = (operationId, saved) => ({
|
|
143
|
+
const answerOf = (operationId, saved, repeats = false) => ({
|
|
134
144
|
operation_id: operationId,
|
|
135
145
|
event: saved?.eventId == null ? null : `event:${saved.eventId}`,
|
|
136
146
|
entry: saved ? entryItem(saved) : null,
|
|
147
|
+
// Only the series is saved here; its occurrences come with the provider's expansion on the next sync.
|
|
148
|
+
...(repeats ? { hint: "sync to store its occurrences" } : {}),
|
|
137
149
|
});
|
|
138
150
|
/** An id every provider takes as a client-chosen one: Google's base32hex `[a-v0-9]`, a CalDAV UID, a Graph id. */
|
|
139
151
|
export const newEventId = () => randomUUID().replaceAll("-", "");
|
|
@@ -159,7 +171,7 @@ export const createEvent = async (context, calendarReference, fields, options =
|
|
|
159
171
|
throw error;
|
|
160
172
|
}
|
|
161
173
|
const [saved] = await context.store.saveEntries(calendar.id, [entry], context.now);
|
|
162
|
-
return answerOf(id, saved);
|
|
174
|
+
return answerOf(id, saved, draft.recurrence !== undefined);
|
|
163
175
|
};
|
|
164
176
|
const targetOf = async (context, reference, { calendar, scope }) => {
|
|
165
177
|
const details = await eventOf(context.store, context.accountId, eventIdOf(reference));
|
|
@@ -196,14 +208,28 @@ const targetOf = async (context, reference, { calendar, scope }) => {
|
|
|
196
208
|
};
|
|
197
209
|
};
|
|
198
210
|
export const updateEvent = async (context, reference, fields, options = {}) => {
|
|
199
|
-
const patch = draftOf(fields, context.zone, { whole: false });
|
|
200
211
|
const scope = options.series ? "series" : "this";
|
|
212
|
+
if (fields.repeat !== undefined && scope !== "series")
|
|
213
|
+
throw new CliError("validation_error", `a repeat rule belongs to the whole series: add ${context.names.series}`, {
|
|
214
|
+
param: context.names.series,
|
|
215
|
+
});
|
|
201
216
|
const { calendar, target, participants } = await targetOf(context, reference, { ...options, scope });
|
|
217
|
+
const patch = draftOf(fields, context.zone, { whole: false, allDay: target.allDay });
|
|
218
|
+
const kept = (target.recurrence ?? []).filter((line) => !/^RRULE:/iu.test(line));
|
|
219
|
+
if (patch.recurrence)
|
|
220
|
+
patch.recurrence = [...patch.recurrence, ...kept];
|
|
221
|
+
const switching = patch.startDate !== undefined ? !target.allDay : patch.startsAt !== undefined && target.allDay;
|
|
222
|
+
const bound = kindBoundLines(patch.recurrence ? kept : (target.recurrence ?? []));
|
|
223
|
+
if (switching && bound.length)
|
|
224
|
+
throw new CliError("validation_error", `${reference} repeats with ${bound.join(", ")}, which hold a ${target.allDay ? "date" : "time"}; ` +
|
|
225
|
+
(patch.recurrence || bound.some((line) => !/^RRULE:/iu.test(line))
|
|
226
|
+
? "change its kind in the calendar itself"
|
|
227
|
+
: `give its rule again with ${context.names.repeat} and UNTIL as a ${target.allDay ? "UTC time" : "date"}`), { reason: "event.repeat-kind" });
|
|
202
228
|
const notify = notifyOf(context, options.notify, hasOthers(participants) || (patch.attendees?.length ?? 0) > 0);
|
|
203
229
|
const operationId = newOperationId();
|
|
204
230
|
const entry = await guardedWrite(writeGuard(context, keysFor(context, "update", notify)), { chatId: null, kind: "edit", key: COMMAND_KEY.update, operationId, notify }, () => context.writes.writer.update(calendar, target.externalId, patch, { scope, etag: target.etag, notify }));
|
|
205
231
|
const [saved] = await context.store.saveEntries(calendar.id, [entry], context.now);
|
|
206
|
-
return answerOf(operationId, saved);
|
|
232
|
+
return answerOf(operationId, saved, patch.recurrence !== undefined || (switching && scope === "series"));
|
|
207
233
|
};
|
|
208
234
|
export const deleteEvent = async (context, reference, options = {}) => {
|
|
209
235
|
const scope = options.series ? "series" : "this";
|
package/dist/cli/context.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type KeyringStore, type Renderer, type RenderFormat, type Streams } from "@wirecat/cli-base";
|
|
2
2
|
import type { ErrorObject } from "@wirecat/cli-base/standards";
|
|
3
3
|
import type { Command } from "commander";
|
|
4
|
+
import { type Deadline, type RunBudget } from "../background/budget.js";
|
|
4
5
|
import type { MessageEvent } from "../domain/models.js";
|
|
5
6
|
import type { AppIdentity } from "../ports/app.js";
|
|
6
7
|
import type { MessengerAdapter } from "../ports/messenger.js";
|
|
@@ -71,6 +72,8 @@ export interface BaseEnvironment {
|
|
|
71
72
|
* what each step brings and may stop at any point: the history a phone pushes after linking.
|
|
72
73
|
*/
|
|
73
74
|
untilStopped?: () => AbortSignal;
|
|
75
|
+
/** Set by `run()`: the deadline running now, or none (a persistent command, a phase, a background job). */
|
|
76
|
+
deadline?: () => Deadline | undefined;
|
|
74
77
|
/**
|
|
75
78
|
* Set by `run()`: ends the phase `untilStopped()` began without aborting its signal — the command saw it finish.
|
|
76
79
|
* However a phase ends, the steps after it get the command's one-shot deadline again, started afresh, and a signal
|
|
@@ -101,6 +104,11 @@ export interface RelayedRun {
|
|
|
101
104
|
/** A promise when the lender opens it on first use. */
|
|
102
105
|
connection: () => MessengerAdapter | Promise<MessengerAdapter>;
|
|
103
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* A budget for a resumable sync, from the deadline running now and the CLI's `requestMs`: so a lower `--timeout`
|
|
109
|
+
* means fewer requests. `requests` (`--max-pages`) can only lower it.
|
|
110
|
+
*/
|
|
111
|
+
export declare const runBudgetOf: (command: Command, requests?: number) => RunBudget;
|
|
104
112
|
/** A partial answer's exit code, for this run alone. */
|
|
105
113
|
export declare const setExitCode: (command: Command, code: number) => void;
|
|
106
114
|
export declare const provide: (program: Command, environment: BaseEnvironment) => void;
|
package/dist/cli/context.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { runBudget } from "../background/budget.js";
|
|
2
3
|
import { resolveOutput } from "../output.js";
|
|
3
4
|
import { recorded } from "../runs/recording.js";
|
|
4
5
|
import { runContextOf } from "../runs/run-context.js";
|
|
@@ -6,6 +7,20 @@ import { withDeadline } from "./deadline.js";
|
|
|
6
7
|
import { withAbort } from "./execution.js";
|
|
7
8
|
import { rootOf } from "./profile.js";
|
|
8
9
|
import { namedProfile } from "./settings.js";
|
|
10
|
+
/**
|
|
11
|
+
* A budget for a resumable sync, from the deadline running now and the CLI's `requestMs`: so a lower `--timeout`
|
|
12
|
+
* means fewer requests. `requests` (`--max-pages`) can only lower it.
|
|
13
|
+
*/
|
|
14
|
+
export const runBudgetOf = (command, requests) => {
|
|
15
|
+
const { deadline, signal, app } = environmentOf(command);
|
|
16
|
+
const now = deadline?.();
|
|
17
|
+
return runBudget({
|
|
18
|
+
...(requests === undefined ? {} : { requests }),
|
|
19
|
+
...(app?.requestMs === undefined ? {} : { requestMs: app.requestMs }),
|
|
20
|
+
...(now ? { deadline: now } : {}),
|
|
21
|
+
...(signal ? { signal } : {}),
|
|
22
|
+
});
|
|
23
|
+
};
|
|
9
24
|
/** A partial answer's exit code, for this run alone. */
|
|
10
25
|
export const setExitCode = (command, code) => {
|
|
11
26
|
const slot = environmentOf(command).exit;
|
package/dist/cli/execution.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type Streams } from "@wirecat/cli-base";
|
|
2
2
|
import type { Closeable } from "./deadline.js";
|
|
3
3
|
export declare const DEFAULT_COMMAND_MS = 30000;
|
|
4
|
+
/** A `cooperative` command stops on its own before its deadline and keeps its work, so it may run longer. */
|
|
5
|
+
export declare const DEFAULT_COOPERATIVE_MS: number;
|
|
6
|
+
/**
|
|
7
|
+
* The same over MCP: clients cut a tool call at about 60 s (the MCP TypeScript SDK's `DEFAULT_REQUEST_TIMEOUT_MSEC`
|
|
8
|
+
* is 60000), so the command answers well before that.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEFAULT_AGENT_COOPERATIVE_MS = 45000;
|
|
4
11
|
export declare const DEFAULT_OUTPUT_BYTES: number;
|
|
5
12
|
export declare const byteCount: (value: string, option: string, zero?: boolean) => number;
|
|
6
13
|
export interface Execution {
|
package/dist/cli/execution.js
CHANGED
|
@@ -2,6 +2,13 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { stopWrites, withWriteScope } from "../sends/guarded.js";
|
|
3
3
|
import { fieldsOf, projectFields } from "./result-fields.js";
|
|
4
4
|
export const DEFAULT_COMMAND_MS = 30_000;
|
|
5
|
+
/** A `cooperative` command stops on its own before its deadline and keeps its work, so it may run longer. */
|
|
6
|
+
export const DEFAULT_COOPERATIVE_MS = 5 * 60_000;
|
|
7
|
+
/**
|
|
8
|
+
* The same over MCP: clients cut a tool call at about 60 s (the MCP TypeScript SDK's `DEFAULT_REQUEST_TIMEOUT_MSEC`
|
|
9
|
+
* is 60000), so the command answers well before that.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_AGENT_COOPERATIVE_MS = 45_000;
|
|
5
12
|
export const DEFAULT_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
6
13
|
export const byteCount = (value, option, zero = false) => {
|
|
7
14
|
const result = Number(value);
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -57,7 +57,7 @@ export { CONTRACT } from "./commands-command.js";
|
|
|
57
57
|
export { refuseUnknownKey } from "./config-command.js";
|
|
58
58
|
export { type ConfigIssue, type ConfigIssueKind, checkConfigText, issueMessage, readConfigFile, readConfigText, settingPathOf, withoutSetting, } from "./config-issues.js";
|
|
59
59
|
export { loadSnakeCaseConfig } from "./config-keys.js";
|
|
60
|
-
export { type AgentRun, type BaseContext, type BaseEnvironment, baseContext, environmentOf, outputFor, provide, secretInputOf, } from "./context.js";
|
|
60
|
+
export { type AgentRun, type BaseContext, type BaseEnvironment, baseContext, environmentOf, outputFor, provide, runBudgetOf, secretInputOf, } from "./context.js";
|
|
61
61
|
export { addContractInfo, type ContractEvidenceOptions, contractEvidence } from "./contract-evidence.js";
|
|
62
62
|
export { sharedCommandTemplate } from "./contract-templates.js";
|
|
63
63
|
export { type Closeable, withDeadline } from "./deadline.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -50,7 +50,7 @@ export { CONTRACT } from "./commands-command.js";
|
|
|
50
50
|
export { refuseUnknownKey } from "./config-command.js";
|
|
51
51
|
export { checkConfigText, issueMessage, readConfigFile, readConfigText, settingPathOf, withoutSetting, } from "./config-issues.js";
|
|
52
52
|
export { loadSnakeCaseConfig } from "./config-keys.js";
|
|
53
|
-
export { baseContext, environmentOf, outputFor, provide, secretInputOf, } from "./context.js";
|
|
53
|
+
export { baseContext, environmentOf, outputFor, provide, runBudgetOf, secretInputOf, } from "./context.js";
|
|
54
54
|
export { addContractInfo, contractEvidence } from "./contract-evidence.js";
|
|
55
55
|
export { sharedCommandTemplate } from "./contract-templates.js";
|
|
56
56
|
export { withDeadline } from "./deadline.js";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { CliError } from "@wirecat/cli-base";
|
|
1
|
+
import { CliError, writeSecurely } from "@wirecat/cli-base";
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { alive, carries } from "../background/processes.js";
|
|
5
5
|
import { thisMachine } from "../background/system.js";
|
|
6
6
|
import { readableLog, tail } from "../background/units.js";
|
|
7
7
|
import { mcpUrlOf, portOf, publicUrlOf } from "../http/public-url.js";
|
|
8
|
-
import { httpLockFile, httpLogFile, mcpHttpMarker, readHttpLock, signedInClients, } from "../mcp/http/state.js";
|
|
8
|
+
import { httpLaunchFile, httpLockFile, httpLogFile, mcpHttpMarker, readHttpLock, signedInClients, } from "../mcp/http/state.js";
|
|
9
9
|
import { destroysNothing, LOCAL_READ, LOCAL_WRITE } from "./effects.js";
|
|
10
10
|
export const MCP_HTTP_DEFAULT_PORT = 8765;
|
|
11
11
|
const LOOKS = 120;
|
|
@@ -82,14 +82,20 @@ export const mcpServerCommand = ({ example, defaultPort = MCP_HTTP_DEFAULT_PORT,
|
|
|
82
82
|
const sys = machine(this);
|
|
83
83
|
const log = httpLogFile(one.tokenFile);
|
|
84
84
|
const { [`${one.envPrefix}_TIMEOUT`]: _timeout, ...inherited } = one.env;
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
const launch = {
|
|
86
|
+
invocation: one.invocation,
|
|
87
|
+
argv: [
|
|
88
|
+
...one.argv,
|
|
89
|
+
"--port",
|
|
90
|
+
String(port),
|
|
91
|
+
"--public-url",
|
|
92
|
+
url.pathname === "/" ? url.origin : url.href,
|
|
93
|
+
...(flags.permission ?? []).flatMap((entry) => ["--permission", entry]),
|
|
94
|
+
],
|
|
95
|
+
env: { ...one.childEnv, [mcpHttpMarker(one.envPrefix)]: one.profile ?? "default" },
|
|
96
|
+
};
|
|
97
|
+
writeSecurely(httpLaunchFile(lockFile), `${JSON.stringify(launch)}\n`, 0o600);
|
|
98
|
+
const pid = sys.spawn(launch.argv, { ...inherited, ...launch.env }, log);
|
|
93
99
|
for (let look = 0; look < LOOKS; look++) {
|
|
94
100
|
const now = readHttpLock(lockFile);
|
|
95
101
|
if (now?.pid === pid && now.listeningAt) {
|
|
@@ -320,6 +320,7 @@ export declare const meetingPreparationAnswer: (packet: MeetingPreparation) => i
|
|
|
320
320
|
started_at: string | null;
|
|
321
321
|
url: string | null;
|
|
322
322
|
} | undefined;
|
|
323
|
+
untrusted?: string[] | undefined;
|
|
323
324
|
}[];
|
|
324
325
|
association: "unconfirmed-word-evidence";
|
|
325
326
|
vocabulary_fingerprint: string;
|
|
@@ -40,6 +40,7 @@ export declare const mapPreparationProjectEvidence: (section: NonNullable<Meetin
|
|
|
40
40
|
started_at: string | null;
|
|
41
41
|
url: string | null;
|
|
42
42
|
} | undefined;
|
|
43
|
+
untrusted?: string[] | undefined;
|
|
43
44
|
}[];
|
|
44
45
|
association: "unconfirmed-word-evidence";
|
|
45
46
|
vocabulary_fingerprint: string;
|
|
@@ -349,6 +349,7 @@ export declare const readMeetingPreparation: (read: MeetingPreparationReader, in
|
|
|
349
349
|
started_at: string | null;
|
|
350
350
|
url: string | null;
|
|
351
351
|
} | undefined;
|
|
352
|
+
untrusted?: string[] | undefined;
|
|
352
353
|
}[];
|
|
353
354
|
association: "unconfirmed-word-evidence";
|
|
354
355
|
vocabulary_fingerprint: string;
|
|
@@ -139,6 +139,7 @@ export const topicAnswer = (topic) => defined({
|
|
|
139
139
|
unread_count: topic.unreadCount,
|
|
140
140
|
last_message_at: topic.lastMessageAt,
|
|
141
141
|
created_at: topic.createdAt,
|
|
142
|
+
untrusted: ["title"],
|
|
142
143
|
});
|
|
143
144
|
export const folderAnswer = (folder) => {
|
|
144
145
|
const shown = "chats" in folder ? folder : undefined;
|
|
@@ -10,6 +10,16 @@ export const SOURCE_SEARCHED = ["messages", "notes", "events", "meetings"];
|
|
|
10
10
|
const defined = (value) => Object.fromEntries(Object.entries(value).filter(([, one]) => one !== undefined));
|
|
11
11
|
/** The page a `--page` asks for, as the offset the search services take. */
|
|
12
12
|
export const offsetOf = (page, limit) => page === undefined || page <= 1 ? undefined : (page - 1) * limit;
|
|
13
|
+
/** Who wrote it and what, the chat's title, the mail's headers: others write them; the owner's own messages too. */
|
|
14
|
+
const MESSAGE_HIT_UNTRUSTED = ["chat_title", "sender_name", "excerpt", "context.sender_name", "context.text"];
|
|
15
|
+
const MAIL_HIT_UNTRUSTED = [
|
|
16
|
+
"email.subject",
|
|
17
|
+
"email.from.name",
|
|
18
|
+
"email.to.name",
|
|
19
|
+
"email.cc.name",
|
|
20
|
+
"email.bcc.name",
|
|
21
|
+
"email.reply_addresses.name",
|
|
22
|
+
];
|
|
13
23
|
export const messageHitItem = (hit, query = "") => {
|
|
14
24
|
const at = parseLocator(hit.locator);
|
|
15
25
|
const around = hit.thread?.items ?? hit.context;
|
|
@@ -55,6 +65,7 @@ export const messageHitItem = (hit, query = "") => {
|
|
|
55
65
|
sender_name: one.senderName,
|
|
56
66
|
text: one.text,
|
|
57
67
|
})),
|
|
68
|
+
untrusted: hit.email ? [...MESSAGE_HIT_UNTRUSTED, ...MAIL_HIT_UNTRUSTED] : MESSAGE_HIT_UNTRUSTED,
|
|
58
69
|
});
|
|
59
70
|
};
|
|
60
71
|
const accountName = ({ provider, account }) => `${provider}:${account}`;
|
|
@@ -160,6 +171,7 @@ const pictureItems = (found) => {
|
|
|
160
171
|
sender_name: senderName,
|
|
161
172
|
matched_in: "picture",
|
|
162
173
|
score,
|
|
174
|
+
untrusted: ["sender_name"],
|
|
163
175
|
}));
|
|
164
176
|
};
|
|
165
177
|
/** `search messages` and `search mail`, CLI and MCP alike. */
|
|
@@ -175,9 +187,21 @@ export const messagesAnswer = (command, found, { limit, page, again }, query = "
|
|
|
175
187
|
meta: metaOf(accounts, found.skipped),
|
|
176
188
|
});
|
|
177
189
|
};
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
/**
|
|
191
|
+
* A document comes from a shared drive, where others write and edit; a note is the owner's own, in a folder of
|
|
192
|
+
* theirs or written in memo, so it is left unmarked.
|
|
193
|
+
*/
|
|
194
|
+
const DOCUMENT_HIT_UNTRUSTED = ["title", "excerpt"];
|
|
195
|
+
export const noteHitItem = (hit) => defined({ ...hit, untrusted: hit.kind === "document" ? DOCUMENT_HIT_UNTRUSTED : undefined });
|
|
196
|
+
/** The text of each kind of `search all` hit that someone other than the owner may have written. */
|
|
197
|
+
const ALL_HIT_UNTRUSTED = {
|
|
198
|
+
message: ["title", "excerpt", "sender_name"],
|
|
199
|
+
mail: ["title", "excerpt", "sender_name"],
|
|
200
|
+
document: DOCUMENT_HIT_UNTRUSTED,
|
|
201
|
+
note: undefined,
|
|
202
|
+
event: ["title", "excerpt"],
|
|
203
|
+
meeting: ["title", "excerpt"],
|
|
204
|
+
};
|
|
181
205
|
export const allHitItem = (item) => item.kind === "meeting"
|
|
182
206
|
? {
|
|
183
207
|
kind: "meeting",
|
|
@@ -190,6 +214,7 @@ export const allHitItem = (item) => item.kind === "meeting"
|
|
|
190
214
|
offset_ms: item.startMs,
|
|
191
215
|
scope: item.scope,
|
|
192
216
|
id: String(item.id),
|
|
217
|
+
untrusted: ALL_HIT_UNTRUSTED.meeting,
|
|
193
218
|
}
|
|
194
219
|
: defined({
|
|
195
220
|
kind: item.kind,
|
|
@@ -203,7 +228,7 @@ export const allHitItem = (item) => item.kind === "meeting"
|
|
|
203
228
|
sender_name: item.from?.name,
|
|
204
229
|
sender_ref: item.from?.ref,
|
|
205
230
|
found_by: item.foundBy,
|
|
206
|
-
untrusted: item.kind
|
|
231
|
+
untrusted: ALL_HIT_UNTRUSTED[item.kind],
|
|
207
232
|
});
|
|
208
233
|
/** Meaning was not searched because the model is not downloaded; `--exact` leaving it out is no gap. */
|
|
209
234
|
const meaningGap = (skipped, exact) => skipped === undefined || exact
|
|
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
|
|
|
3
3
|
import type { Messenger } from "./context.js";
|
|
4
4
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
5
5
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
6
|
-
error?: undefined;
|
|
7
6
|
path: string;
|
|
8
7
|
exists: boolean;
|
|
8
|
+
error?: undefined;
|
|
9
9
|
} | {
|
|
10
10
|
path: string;
|
|
11
11
|
exists: boolean;
|
package/dist/cli/program.js
CHANGED
|
@@ -20,7 +20,7 @@ import { outputFor, provide } from "./context.js";
|
|
|
20
20
|
import { refuseByCommandPermissions, refuseWriteBeforeConnecting } from "./early-refusal.js";
|
|
21
21
|
import { changesAuthorityAt, changesAuthorityNow, isLogin, isStoreChange, judgedEffects, judgedTargets, storeChangeRefusal, } from "./effects.js";
|
|
22
22
|
import { itemOf, renderAnswer, snakeKeys } from "./envelope.js";
|
|
23
|
-
import { byteCount, DEFAULT_COMMAND_MS, DEFAULT_OUTPUT_BYTES, execution } from "./execution.js";
|
|
23
|
+
import { byteCount, DEFAULT_AGENT_COOPERATIVE_MS, DEFAULT_COMMAND_MS, DEFAULT_COOPERATIVE_MS, DEFAULT_OUTPUT_BYTES, execution, } from "./execution.js";
|
|
24
24
|
import { setupAtStart } from "./first-run.js";
|
|
25
25
|
import { installHelp } from "./help/first-screen.js";
|
|
26
26
|
import { showInputFields } from "./input-fields.js";
|
|
@@ -150,6 +150,11 @@ const runOnce = async (argv, definition, options) => {
|
|
|
150
150
|
let oneShot;
|
|
151
151
|
// Headless, an interactive command's 5 minutes are for its phase; a person at a terminal may still be answering.
|
|
152
152
|
let afterPhase;
|
|
153
|
+
let running;
|
|
154
|
+
const startDeadline = (ms) => {
|
|
155
|
+
running = ms === undefined ? undefined : { endsAt: Date.now() + ms, ms };
|
|
156
|
+
control.start(ms);
|
|
157
|
+
};
|
|
153
158
|
let finished = false;
|
|
154
159
|
// The steps after a phase are one-shot again, however it ended: bounded, and ended by a signal as any command is.
|
|
155
160
|
const endPhase = () => {
|
|
@@ -161,7 +166,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
161
166
|
// A later phase keeps the --timeout the steps after this one got, not the one this phase already used up.
|
|
162
167
|
if (explicitEnd !== undefined && oneShot !== undefined)
|
|
163
168
|
explicitEnd = Date.now() + oneShot;
|
|
164
|
-
|
|
169
|
+
startDeadline(afterPhase);
|
|
165
170
|
};
|
|
166
171
|
const interrupted = (signal) => {
|
|
167
172
|
// A second signal ends the command as any signal does: one that ignores its phase can still be stopped.
|
|
@@ -234,7 +239,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
234
239
|
phased = true;
|
|
235
240
|
phase = new AbortController();
|
|
236
241
|
phase.signal.addEventListener("abort", endPhase, { once: true });
|
|
237
|
-
|
|
242
|
+
startDeadline(undefined);
|
|
238
243
|
if (explicitEnd !== undefined)
|
|
239
244
|
phaseTimer = setTimeout(() => phase.abort(new CliError("timeout", "the command reached its --timeout", {
|
|
240
245
|
retryable: false,
|
|
@@ -243,6 +248,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
243
248
|
return phase.signal;
|
|
244
249
|
},
|
|
245
250
|
endUntilStopped: endPhase,
|
|
251
|
+
deadline: () => running,
|
|
246
252
|
};
|
|
247
253
|
provide(program, environment);
|
|
248
254
|
program.hook("preAction", async (_root, action) => {
|
|
@@ -330,7 +336,11 @@ const runOnce = async (argv, definition, options) => {
|
|
|
330
336
|
? undefined
|
|
331
337
|
: lifetime === "interactive"
|
|
332
338
|
? HEADLESS_INTERACTIVE_MS
|
|
333
|
-
:
|
|
339
|
+
: stopsAtItsPace === true
|
|
340
|
+
? options.agent
|
|
341
|
+
? DEFAULT_AGENT_COOPERATIVE_MS
|
|
342
|
+
: DEFAULT_COOPERATIVE_MS
|
|
343
|
+
: DEFAULT_COMMAND_MS
|
|
334
344
|
: persistent
|
|
335
345
|
? undefined
|
|
336
346
|
: parseDuration(timeout, flags.timeout === undefined ? envName(definition.app, "TIMEOUT") : "--timeout");
|
|
@@ -372,7 +382,7 @@ const runOnce = async (argv, definition, options) => {
|
|
|
372
382
|
});
|
|
373
383
|
throw new Relayed(code);
|
|
374
384
|
}
|
|
375
|
-
|
|
385
|
+
startDeadline(deadline);
|
|
376
386
|
oneShot = deadline;
|
|
377
387
|
afterPhase = timeout !== undefined || persistent || job || interactiveLogin ? deadline : DEFAULT_COMMAND_MS;
|
|
378
388
|
if (timeout !== undefined && deadline !== undefined)
|
|
@@ -9,11 +9,16 @@ export interface UpgradeContext extends Omit<PackageUpgradePorts, "onInstall"> {
|
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
11
|
* A running serve keeps the code it started with; the update replaced the files under it. Each is restarted by
|
|
12
|
-
* the new `script` — `server restart` refuses a serve started by hand, which is then named in `left
|
|
13
|
-
* `spawn` replaces the child process, as in `UpdateEnvironment`.
|
|
12
|
+
* the new `script` — `server restart` refuses a serve started by hand, which is then named in `left`, and one that
|
|
13
|
+
* ran out of time is named in `timedOut` too. `spawn` replaces the child process, as in `UpdateEnvironment`.
|
|
14
14
|
*/
|
|
15
15
|
export declare const restartServers: (app: AppIdentity, script: string, env: NodeJS.ProcessEnv, spawn?: (argv: string[]) => number) => {
|
|
16
16
|
restarted: string[];
|
|
17
17
|
left: string[];
|
|
18
|
+
timedOut: string[];
|
|
19
|
+
} | {
|
|
20
|
+
timedOut?: undefined;
|
|
21
|
+
restarted: string[];
|
|
22
|
+
left: string[];
|
|
18
23
|
};
|
|
19
24
|
export declare const upgradeCommand: (app: AppIdentity, packageName: string, contextFor: (command: Command) => UpgradeContext) => Command;
|