@wirecat/cli-core 0.463.0 → 0.464.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/background/after-upgrade.d.ts +51 -0
  3. package/dist/background/after-upgrade.js +245 -0
  4. package/dist/background/budget.d.ts +37 -0
  5. package/dist/background/budget.js +48 -0
  6. package/dist/background/index.d.ts +1 -0
  7. package/dist/background/index.js +1 -0
  8. package/dist/calendar/busy.d.ts +68 -0
  9. package/dist/calendar/busy.js +148 -0
  10. package/dist/calendar/cli/index.d.ts +7 -0
  11. package/dist/calendar/cli/index.js +94 -12
  12. package/dist/calendar/index.d.ts +3 -2
  13. package/dist/calendar/index.js +2 -1
  14. package/dist/calendar/mcp/index.d.ts +55 -13
  15. package/dist/calendar/mcp/index.js +19 -4
  16. package/dist/calendar/recurrence.d.ts +10 -0
  17. package/dist/calendar/recurrence.js +65 -0
  18. package/dist/calendar/source.d.ts +19 -0
  19. package/dist/calendar/sync.d.ts +86 -7
  20. package/dist/calendar/sync.js +271 -105
  21. package/dist/calendar/testing/source-contract.js +14 -0
  22. package/dist/calendar/testing/source.d.ts +6 -0
  23. package/dist/calendar/testing/source.js +30 -2
  24. package/dist/calendar/testing/sync-contract.d.ts +2 -1
  25. package/dist/calendar/testing/sync-contract.js +80 -5
  26. package/dist/calendar/testing/writer-contract.d.ts +2 -1
  27. package/dist/calendar/testing/writer-contract.js +46 -2
  28. package/dist/calendar/testing/writer.js +1 -1
  29. package/dist/calendar/writer.d.ts +7 -2
  30. package/dist/calendar/writes.d.ts +12 -2
  31. package/dist/calendar/writes.js +32 -6
  32. package/dist/cli/context.d.ts +8 -0
  33. package/dist/cli/context.js +15 -0
  34. package/dist/cli/execution.d.ts +7 -0
  35. package/dist/cli/execution.js +7 -0
  36. package/dist/cli/index.d.ts +1 -1
  37. package/dist/cli/index.js +1 -1
  38. package/dist/cli/mcp-server-command.js +16 -10
  39. package/dist/cli/meeting-preparation-answer.d.ts +1 -0
  40. package/dist/cli/meeting-preparation-projects.d.ts +1 -0
  41. package/dist/cli/meeting-preparation-read.d.ts +1 -0
  42. package/dist/cli/messenger/chat-answers.js +1 -0
  43. package/dist/cli/messenger/search-answers.js +29 -4
  44. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  45. package/dist/cli/program.js +15 -5
  46. package/dist/cli/upgrade-command.d.ts +7 -2
  47. package/dist/cli/upgrade-command.js +65 -9
  48. package/dist/entities/fields.d.ts +2 -0
  49. package/dist/entities/fields.js +2 -0
  50. package/dist/entities/hit.d.ts +1 -1
  51. package/dist/entities/hit.js +3 -3
  52. package/dist/entities/index.d.ts +10 -800
  53. package/dist/entities/index.js +37 -31
  54. package/dist/entities/meeting-preparation.d.ts +1 -0
  55. package/dist/entities/topic.d.ts +1 -0
  56. package/dist/entities/topic.js +2 -1
  57. package/dist/errors/reasons.js +4 -0
  58. package/dist/mcp/http/state.d.ts +14 -0
  59. package/dist/mcp/http/state.js +16 -0
  60. package/dist/mcp/meeting-preparation.d.ts +1 -0
  61. package/dist/meetings/answers.d.ts +2 -0
  62. package/dist/meetings/answers.js +3 -0
  63. package/dist/meetings/mcp/search.d.ts +1 -0
  64. package/dist/meetings/project-evidence-answer.d.ts +1 -0
  65. package/dist/ports/app.d.ts +5 -0
  66. package/dist/services/package-upgrade.d.ts +9 -0
  67. package/dist/services/package-upgrade.js +8 -1
  68. 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; `notify: false` mails nobody, and a profile that keeps `events.notify` at its
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; `notify: false` mails nobody, and a profile that keeps `events.notify` at its
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(),
@@ -65,7 +65,7 @@ export const fakeCalendarWriter = (options = {}) => {
65
65
  return answer(existing);
66
66
  const entry = stamp({
67
67
  externalId: id,
68
- kind: "single",
68
+ kind: draft.recurrence ? "series" : "single",
69
69
  status: "confirmed",
70
70
  participants: [],
71
71
  ...fields(draft),
@@ -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). Never repeating.
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
- /** Only the fields to change; `null` clears one. `attendees` replaces the whole list. */
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
- /** Typed fields into a draft, or into a patch with only what was given. */
50
- export declare const draftOf: (fields: EventFields, zone: string, { whole }: {
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
  }>;
@@ -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
- /** Typed fields into a draft, or into a patch with only what was given. */
28
- export const draftOf = (fields, zone, { whole }) => {
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";
@@ -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;
@@ -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;
@@ -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 {
@@ -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);
@@ -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 pid = sys.spawn([
86
- ...one.argv,
87
- "--port",
88
- String(port),
89
- "--public-url",
90
- url.pathname === "/" ? url.origin : url.href,
91
- ...(flags.permission ?? []).flatMap((entry) => ["--permission", entry]),
92
- ], { ...inherited, ...one.childEnv, [mcpHttpMarker(one.envPrefix)]: one.profile ?? "default" }, log);
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
- export const noteHitItem = (hit) => defined({ ...hit });
179
- /** An event's title and the text its excerpt is cut from come from whoever sent the invitation. */
180
- const EVENT_HIT_UNTRUSTED = ["title", "excerpt"];
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 === "event" ? EVENT_HIT_UNTRUSTED : undefined,
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;
@@ -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
- control.start(afterPhase);
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
- control.start(undefined);
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
- : DEFAULT_COMMAND_MS
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
- control.start(deadline);
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;