@wirecat/cli-core 0.452.0 → 0.454.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 (54) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/cli/mcp-server-command.js +5 -1
  3. package/dist/cli/messenger/search-answers.d.ts +1 -1
  4. package/dist/cli/messenger/search-answers.js +1 -1
  5. package/dist/cli/messenger/search-command.js +6 -3
  6. package/dist/entities/contact-context.d.ts +42 -0
  7. package/dist/entities/contact-context.js +11 -0
  8. package/dist/entities/hit.d.ts +3 -3
  9. package/dist/entities/hit.js +3 -3
  10. package/dist/entities/index.d.ts +61 -21
  11. package/dist/entities/task-results.d.ts +20 -20
  12. package/dist/entities/task-results.js +3 -15
  13. package/dist/mcp/http/activity.d.ts +24 -0
  14. package/dist/mcp/http/activity.js +86 -0
  15. package/dist/mcp/http/serve.js +14 -0
  16. package/dist/mcp/http/state.d.ts +5 -0
  17. package/dist/mcp/http/state.js +14 -4
  18. package/dist/meetings/event-store.d.ts +23 -0
  19. package/dist/meetings/index.d.ts +1 -1
  20. package/dist/search/query.js +1 -3
  21. package/dist/services/events-search.d.ts +67 -0
  22. package/dist/services/events-search.js +131 -0
  23. package/dist/services/index.d.ts +3 -2
  24. package/dist/services/index.js +2 -1
  25. package/dist/services/notes-search.d.ts +25 -1
  26. package/dist/services/notes-search.js +24 -11
  27. package/dist/services/person-context.d.ts +20 -1
  28. package/dist/services/person-context.js +46 -1
  29. package/dist/services/search-all-meetings.d.ts +1 -1
  30. package/dist/services/search-all-meetings.js +7 -2
  31. package/dist/services/search-all.d.ts +12 -7
  32. package/dist/services/search-all.js +39 -4
  33. package/dist/store/index.d.ts +1 -0
  34. package/dist/store/index.js +1 -0
  35. package/dist/store/project-transfer/request.d.ts +1 -1
  36. package/dist/store/project-transfer/schema.d.ts +2 -2
  37. package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
  38. package/dist/store/sqlite/calendar.js +85 -11
  39. package/dist/store/sqlite/event-search.d.ts +66 -0
  40. package/dist/store/sqlite/event-search.js +163 -0
  41. package/dist/store/sqlite/files.d.ts +4 -0
  42. package/dist/store/sqlite/files.js +28 -0
  43. package/dist/store/sqlite/link-kinds.d.ts +5 -1
  44. package/dist/store/sqlite/link-kinds.js +5 -0
  45. package/dist/store/sqlite/note-index.d.ts +17 -1
  46. package/dist/store/sqlite/note-index.js +32 -8
  47. package/dist/store/sqlite/note-search.d.ts +43 -0
  48. package/dist/store/sqlite/note-search.js +24 -24
  49. package/dist/store/sqlite/notes.d.ts +4 -5
  50. package/dist/store/sqlite/notes.js +2 -2
  51. package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
  52. package/dist/store/store.d.ts +2 -1
  53. package/dist/store/store.js +2 -1
  54. package/package.json +1 -1
@@ -37,9 +37,9 @@ export declare const taskCard: v.StrictObjectSchema<{
37
37
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
38
38
  readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
39
39
  readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
40
- readonly kind: v.PicklistSchema<["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
40
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to", "attached"], undefined>;
41
41
  readonly anchor: v.NullableSchema<v.StringSchema<undefined>, undefined>;
42
- readonly origin: v.PicklistSchema<["file", "owner", "suggested", "message"], undefined>;
42
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message", "calendar"], undefined>;
43
43
  readonly target_text: v.NullableSchema<v.StringSchema<undefined>, undefined>;
44
44
  readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
45
45
  readonly evidence: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -51,9 +51,9 @@ export declare const taskCard: v.StrictObjectSchema<{
51
51
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
52
52
  readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
53
53
  readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
54
- readonly kind: v.PicklistSchema<["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
54
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to", "attached"], undefined>;
55
55
  readonly anchor: v.NullableSchema<v.StringSchema<undefined>, undefined>;
56
- readonly origin: v.PicklistSchema<["file", "owner", "suggested", "message"], undefined>;
56
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message", "calendar"], undefined>;
57
57
  readonly target_text: v.NullableSchema<v.StringSchema<undefined>, undefined>;
58
58
  readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
59
59
  readonly evidence: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -138,9 +138,9 @@ export declare const taskCreated: v.StrictObjectSchema<{
138
138
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
139
139
  readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
140
140
  readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
141
- readonly kind: v.PicklistSchema<["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
141
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to", "attached"], undefined>;
142
142
  readonly anchor: v.NullableSchema<v.StringSchema<undefined>, undefined>;
143
- readonly origin: v.PicklistSchema<["file", "owner", "suggested", "message"], undefined>;
143
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message", "calendar"], undefined>;
144
144
  readonly target_text: v.NullableSchema<v.StringSchema<undefined>, undefined>;
145
145
  readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
146
146
  readonly evidence: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -152,9 +152,9 @@ export declare const taskCreated: v.StrictObjectSchema<{
152
152
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
153
153
  readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
154
154
  readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
155
- readonly kind: v.PicklistSchema<["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
155
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to", "attached"], undefined>;
156
156
  readonly anchor: v.NullableSchema<v.StringSchema<undefined>, undefined>;
157
- readonly origin: v.PicklistSchema<["file", "owner", "suggested", "message"], undefined>;
157
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message", "calendar"], undefined>;
158
158
  readonly target_text: v.NullableSchema<v.StringSchema<undefined>, undefined>;
159
159
  readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
160
160
  readonly evidence: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -240,9 +240,9 @@ export declare const taskLinks: v.UnionSchema<[v.StrictObjectSchema<{
240
240
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
241
241
  readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
242
242
  readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
243
- readonly kind: v.PicklistSchema<["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
243
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to", "attached"], undefined>;
244
244
  readonly anchor: v.NullableSchema<v.StringSchema<undefined>, undefined>;
245
- readonly origin: v.PicklistSchema<["file", "owner", "suggested", "message"], undefined>;
245
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message", "calendar"], undefined>;
246
246
  readonly target_text: v.NullableSchema<v.StringSchema<undefined>, undefined>;
247
247
  readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
248
248
  readonly evidence: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -254,9 +254,9 @@ export declare const taskLinks: v.UnionSchema<[v.StrictObjectSchema<{
254
254
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
255
255
  readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
256
256
  readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
257
- readonly kind: v.PicklistSchema<["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
257
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to", "attached"], undefined>;
258
258
  readonly anchor: v.NullableSchema<v.StringSchema<undefined>, undefined>;
259
- readonly origin: v.PicklistSchema<["file", "owner", "suggested", "message"], undefined>;
259
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message", "calendar"], undefined>;
260
260
  readonly target_text: v.NullableSchema<v.StringSchema<undefined>, undefined>;
261
261
  readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
262
262
  readonly evidence: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -370,9 +370,9 @@ export declare const taskMerge: v.StrictObjectSchema<{
370
370
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
371
371
  readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
372
372
  readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
373
- readonly kind: v.PicklistSchema<["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
373
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to", "attached"], undefined>;
374
374
  readonly anchor: v.NullableSchema<v.StringSchema<undefined>, undefined>;
375
- readonly origin: v.PicklistSchema<["file", "owner", "suggested", "message"], undefined>;
375
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message", "calendar"], undefined>;
376
376
  readonly target_text: v.NullableSchema<v.StringSchema<undefined>, undefined>;
377
377
  readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
378
378
  readonly evidence: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -384,9 +384,9 @@ export declare const taskMerge: v.StrictObjectSchema<{
384
384
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
385
385
  readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
386
386
  readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
387
- readonly kind: v.PicklistSchema<["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
387
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to", "attached"], undefined>;
388
388
  readonly anchor: v.NullableSchema<v.StringSchema<undefined>, undefined>;
389
- readonly origin: v.PicklistSchema<["file", "owner", "suggested", "message"], undefined>;
389
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message", "calendar"], undefined>;
390
390
  readonly target_text: v.NullableSchema<v.StringSchema<undefined>, undefined>;
391
391
  readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
392
392
  readonly evidence: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -465,9 +465,9 @@ export declare const taskUnmerge: v.StrictObjectSchema<{
465
465
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
466
466
  readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
467
467
  readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
468
- readonly kind: v.PicklistSchema<["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
468
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to", "attached"], undefined>;
469
469
  readonly anchor: v.NullableSchema<v.StringSchema<undefined>, undefined>;
470
- readonly origin: v.PicklistSchema<["file", "owner", "suggested", "message"], undefined>;
470
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message", "calendar"], undefined>;
471
471
  readonly target_text: v.NullableSchema<v.StringSchema<undefined>, undefined>;
472
472
  readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
473
473
  readonly evidence: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -479,9 +479,9 @@ export declare const taskUnmerge: v.StrictObjectSchema<{
479
479
  readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
480
480
  readonly from: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
481
481
  readonly to: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>, undefined>;
482
- readonly kind: v.PicklistSchema<["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to"], undefined>;
482
+ readonly kind: v.PicklistSchema<readonly ["links-to", "blocked-by", "follow-up-of", "about", "member-of", "labelled", "answered-by", "evidence", "created-from", "duplicate-of", "related-to", "assigned-to", "attached"], undefined>;
483
483
  readonly anchor: v.NullableSchema<v.StringSchema<undefined>, undefined>;
484
- readonly origin: v.PicklistSchema<["file", "owner", "suggested", "message"], undefined>;
484
+ readonly origin: v.PicklistSchema<readonly ["file", "owner", "suggested", "message", "calendar"], undefined>;
485
485
  readonly target_text: v.NullableSchema<v.StringSchema<undefined>, undefined>;
486
486
  readonly role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
487
487
  readonly evidence: v.NullableSchema<v.StringSchema<undefined>, undefined>;
@@ -1,5 +1,6 @@
1
1
  import * as v from "valibot";
2
2
  import { TASK_TYPES } from "../domain/knowledge.js";
3
+ import { LINK_KINDS, LINK_SOURCES } from "../store/sqlite/link-kinds.js";
3
4
  import { TASK_KINDS, TASK_ORIGINS, TASK_STATES, TASK_VERDICTS } from "../tasks/index.js";
4
5
  import { count, id, reference, text, time } from "./fields.js";
5
6
  import { closeSuggestion } from "./task.js";
@@ -9,22 +10,9 @@ const link = v.strictObject({
9
10
  id,
10
11
  from: reference,
11
12
  to: v.nullable(reference),
12
- kind: v.picklist([
13
- "links-to",
14
- "blocked-by",
15
- "follow-up-of",
16
- "about",
17
- "member-of",
18
- "labelled",
19
- "answered-by",
20
- "evidence",
21
- "created-from",
22
- "duplicate-of",
23
- "related-to",
24
- "assigned-to",
25
- ]),
13
+ kind: v.picklist(LINK_KINDS),
26
14
  anchor: v.nullable(text),
27
- origin: v.picklist(["file", "owner", "suggested", "message"]),
15
+ origin: v.picklist(LINK_SOURCES),
28
16
  target_text: v.nullable(text),
29
17
  role: v.nullable(text),
30
18
  evidence: v.nullable(text),
@@ -0,0 +1,24 @@
1
+ /** What each logged-in app did last, keyed by its client id. Tool names and times only: never arguments. */
2
+ export interface ClientActivity {
3
+ lastRequestAt: string;
4
+ lastTool?: string;
5
+ lastToolAt?: string;
6
+ /** The local day `toolCalls` counts, `YYYY-MM-DD`. */
7
+ day: string;
8
+ toolCalls: number;
9
+ }
10
+ export type Activity = Record<string, ClientActivity>;
11
+ export declare const httpActivityFile: (tokenFile: string) => string;
12
+ export declare const localDay: (ms: number) => string;
13
+ export declare const readActivity: (path: string) => Activity;
14
+ /** The names of the tools a JSON-RPC body calls; a batch may call several. */
15
+ export declare const calledTools: (body: unknown) => string[];
16
+ /**
17
+ * Keeps the activity file of one running server. A call is written at once, so status shows it while the owner
18
+ * watches; other requests at most every two seconds. Its own file, not the token file: `mcp --revoke` rewrites that
19
+ * from another process.
20
+ */
21
+ export declare const activityRecorder: (path: string, now?: () => number) => {
22
+ record: (clientId: string, tools: string[]) => void;
23
+ flush: () => void;
24
+ };
@@ -0,0 +1,86 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ const WRITE_EVERY_MS = 2000;
5
+ /** Any app may send any name; one that does not look like a tool's is not kept. */
6
+ const TOOL_NAME = /^[A-Za-z0-9_.-]{1,64}$/;
7
+ export const httpActivityFile = (tokenFile) => `${tokenFile.replace(/\.json$/, "")}.activity.json`;
8
+ export const localDay = (ms) => {
9
+ const d = new Date(ms);
10
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
11
+ };
12
+ export const readActivity = (path) => {
13
+ if (!existsSync(path))
14
+ return {};
15
+ try {
16
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
17
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ };
23
+ /** The names of the tools a JSON-RPC body calls; a batch may call several. */
24
+ export const calledTools = (body) => (Array.isArray(body) ? body : [body]).flatMap((message) => {
25
+ if (!message || typeof message !== "object")
26
+ return [];
27
+ const { method, params } = message;
28
+ const name = params?.name;
29
+ return method === "tools/call" && typeof name === "string" && TOOL_NAME.test(name) ? [name] : [];
30
+ });
31
+ /**
32
+ * Keeps the activity file of one running server. A call is written at once, so status shows it while the owner
33
+ * watches; other requests at most every two seconds. Its own file, not the token file: `mcp --revoke` rewrites that
34
+ * from another process.
35
+ */
36
+ export const activityRecorder = (path, now = Date.now) => {
37
+ const activity = readActivity(path);
38
+ let timer;
39
+ const write = () => {
40
+ timer = undefined;
41
+ try {
42
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
43
+ const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
44
+ writeFileSync(temporary, JSON.stringify(activity), { mode: 0o600, flag: "wx" });
45
+ chmodSync(temporary, 0o600);
46
+ renameSync(temporary, path);
47
+ }
48
+ catch {
49
+ // Activity is a convenience for the owner; serving must not fail over it.
50
+ }
51
+ };
52
+ return {
53
+ record: (clientId, tools) => {
54
+ const at = now();
55
+ const iso = new Date(at).toISOString();
56
+ const day = localDay(at);
57
+ const before = activity[clientId];
58
+ const counted = before?.day === day ? before.toolCalls : 0;
59
+ const last = tools.at(-1);
60
+ activity[clientId] = {
61
+ lastRequestAt: iso,
62
+ day,
63
+ toolCalls: counted + tools.length,
64
+ ...(last
65
+ ? { lastTool: last, lastToolAt: iso }
66
+ : before?.lastTool
67
+ ? { lastTool: before.lastTool, ...(before.lastToolAt ? { lastToolAt: before.lastToolAt } : {}) }
68
+ : {}),
69
+ };
70
+ if (last) {
71
+ clearTimeout(timer);
72
+ write();
73
+ }
74
+ else if (!timer) {
75
+ timer = setTimeout(write, WRITE_EVERY_MS);
76
+ timer.unref();
77
+ }
78
+ },
79
+ flush: () => {
80
+ if (!timer)
81
+ return;
82
+ clearTimeout(timer);
83
+ write();
84
+ },
85
+ };
86
+ };
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { createMcpHandler, getOAuthProtectedResourceMetadataUrl, hostHeaderValidationResponse, isLegacyRequest, oauthMetadataResponse, requireBearerAuth, WebStandardStreamableHTTPServerTransport, } from "@modelcontextprotocol/server";
3
3
  import { CliError } from "@wirecat/cli-base";
4
4
  import { listen } from "../../http/listen.js";
5
+ import { activityRecorder, calledTools, httpActivityFile } from "./activity.js";
5
6
  import { ownerLogin } from "./oauth.js";
6
7
  import { httpLock } from "./state.js";
7
8
  export const MCP_PATH = "/mcp";
@@ -74,6 +75,16 @@ const metadataRequest = (request, path, base) => {
74
75
  url.pathname = inserted ? AS_METADATA : path;
75
76
  return new Request(url, { method: request.method, headers: request.headers });
76
77
  };
78
+ const toolsIn = async (request) => {
79
+ if (request.method !== "POST")
80
+ return [];
81
+ try {
82
+ return calledTools(await request.clone().json());
83
+ }
84
+ catch {
85
+ return [];
86
+ }
87
+ };
77
88
  /**
78
89
  * MCP over Streamable HTTP on 127.0.0.1, behind the owner's tunnel (CLI-58). Every MCP request needs
79
90
  * a token from the owner login; the Host header must be the tunnel's or this machine's, so a page in
@@ -116,6 +127,7 @@ export const serveOverHttp = async (build, options) => {
116
127
  verifier: login.verifier,
117
128
  resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(resource),
118
129
  });
130
+ const activity = activityRecorder(httpActivityFile(tokenFile), now);
119
131
  const allowedHosts = [...new Set([publicUrl.hostname, ...LOCAL])];
120
132
  const allowedOrigins = [publicUrl.origin];
121
133
  const listening = await listen(async (request) => {
@@ -145,6 +157,7 @@ export const serveOverHttp = async (build, options) => {
145
157
  const auth = await bearer(request);
146
158
  if (auth instanceof Response)
147
159
  return auth;
160
+ activity.record(auth.clientId, await toolsIn(request));
148
161
  return (await isLegacyRequest(request)) ? legacy.handle(request, auth) : mcp.fetch(request, { authInfo: auth });
149
162
  }, { host: "127.0.0.1", port }).catch((error) => {
150
163
  login.close();
@@ -156,6 +169,7 @@ export const serveOverHttp = async (build, options) => {
156
169
  url: listening.url,
157
170
  close: async () => {
158
171
  login.close();
172
+ activity.flush();
159
173
  try {
160
174
  await listening.close();
161
175
  await legacy.close();
@@ -44,6 +44,11 @@ export interface SignedInClient {
44
44
  /** What the app said its name was when it registered — anyone may register, so it proves nothing. */
45
45
  claimedName: string | null;
46
46
  lastLoginAt: string;
47
+ /** Any MCP request with its token, so the app reached the server after logging in. */
48
+ lastRequestAt: string | null;
49
+ lastTool: string | null;
50
+ lastToolAt: string | null;
51
+ toolCallsToday: number;
47
52
  }
48
53
  /**
49
54
  * The apps that hold a token now, and when each last got one. Ids and hashes stay in the file: what
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { holdLock, readLock, releaseLock, takeLock } from "../../background/lock.js";
3
+ import { httpActivityFile, localDay, readActivity } from "./activity.js";
3
4
  export const ACCESS_SECONDS = 60 * 60;
4
5
  export const REFRESH_SECONDS = 30 * 24 * 60 * 60;
5
6
  export const readStored = (path) => existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : { clients: [], tokens: [] };
@@ -32,6 +33,8 @@ export const httpLock = (path, first) => {
32
33
  export const signedInClients = (tokenFile, now = Date.now()) => {
33
34
  const seconds = Math.floor(now / 1000);
34
35
  const { clients, tokens } = readStored(tokenFile);
36
+ const activity = readActivity(httpActivityFile(tokenFile));
37
+ const today = localDay(now);
35
38
  const last = new Map();
36
39
  for (const token of tokens) {
37
40
  if (token.expiresAt <= seconds)
@@ -41,9 +44,16 @@ export const signedInClients = (tokenFile, now = Date.now()) => {
41
44
  }
42
45
  return clients
43
46
  .filter((client) => last.has(client.id))
44
- .map((client) => ({
45
- claimedName: client.name ?? null,
46
- lastLoginAt: new Date((last.get(client.id) ?? 0) * 1000).toISOString(),
47
- }))
47
+ .map((client) => {
48
+ const did = activity[client.id];
49
+ return {
50
+ claimedName: client.name ?? null,
51
+ lastLoginAt: new Date((last.get(client.id) ?? 0) * 1000).toISOString(),
52
+ lastRequestAt: did?.lastRequestAt ?? null,
53
+ lastTool: did?.lastTool ?? null,
54
+ lastToolAt: did?.lastToolAt ?? null,
55
+ toolCallsToday: did?.day === today ? did.toolCalls : 0,
56
+ };
57
+ })
48
58
  .sort((a, b) => b.lastLoginAt.localeCompare(a.lastLoginAt));
49
59
  };
@@ -30,6 +30,20 @@ export interface EntryParticipantInput {
30
30
  /** A room or a piece of equipment: listed, never a contact. */
31
31
  resource?: boolean;
32
32
  }
33
+ /** A Google Drive file attached to a calendar entry (Google's `attachments[]`). */
34
+ export interface EntryAttachmentInput {
35
+ /** The Drive file's id: `files.external_id` of a stored Drive file. */
36
+ fileId: string;
37
+ title?: string | null;
38
+ }
39
+ /** A Drive file attached to an entry, and the document it is once Drive has stored it. */
40
+ export interface EntryAttachment {
41
+ entryId: number;
42
+ fileId: string;
43
+ title: string | null;
44
+ /** `null` while no Drive account holds the file, while two do, or while its document is deleted. */
45
+ documentId: number | null;
46
+ }
33
47
  export type EntryKind = "single" | "series" | "occurrence";
34
48
  export type EntryStatus = "confirmed" | "tentative" | "cancelled";
35
49
  /**
@@ -75,6 +89,12 @@ export interface EntryInput {
75
89
  externalUpdatedAt?: number | null;
76
90
  /** Only the keys `ENTRY_METADATA_KEYS` names are kept. */
77
91
  metadata?: Record<string, unknown> | null;
92
+ /**
93
+ * Its Drive files: each links the entry to the file's document, now or once Drive stores the file. Absent keeps the
94
+ * links the entry has; a list replaces them. Google leaves `attachments` out of an event that has none, so a provider
95
+ * sending a whole event sends `[]` then, or a file removed from the event stays linked.
96
+ */
97
+ attachments?: EntryAttachmentInput[];
78
98
  }
79
99
  export interface StoredEntry {
80
100
  id: number;
@@ -138,6 +158,7 @@ export interface EventDetails {
138
158
  entries: StoredEntry[];
139
159
  participants: StoredParticipant[];
140
160
  revisions: EntryRevision[];
161
+ attachments: EntryAttachment[];
141
162
  }
142
163
  export interface EventFilter {
143
164
  /** Events that end after this instant (exclusive end). */
@@ -148,6 +169,8 @@ export interface EventFilter {
148
169
  calendarId?: number;
149
170
  /** Events whose entries list one of this person's contacts. */
150
171
  personId?: number;
172
+ /** Latest start first; earliest first when absent. */
173
+ newest?: boolean;
151
174
  limit?: number;
152
175
  offset?: number;
153
176
  }
@@ -4,7 +4,7 @@ export { calendarText, ENTRY_METADATA_KEYS, entryMetadata, withoutLinkQueries }
4
4
  export type { MeetingContextStore, MeetingEventLinkInput, MeetingEventLinkReceipt, ParticipantPerson, ParticipantPersonInput, ParticipantPersonLinkInput, ParticipantPersonLinkReceipt, } from "./context-store.js";
5
5
  export { type MeetingCueIdentity, meetingCueFingerprint } from "./cue-fingerprint.js";
6
6
  export { EVENT_QUERY } from "./event-query.js";
7
- export type { CalendarInput, EntryInput, EntryKind, EntryParticipantInput, EntryRevision, EntryStatus, EventDetails, EventFilter, EventStore, LinkReview, ReviewDecision, StoredCalendar, StoredEntry, StoredParticipant, } from "./event-store.js";
7
+ export type { CalendarInput, EntryAttachment, EntryAttachmentInput, EntryInput, EntryKind, EntryParticipantInput, EntryRevision, EntryStatus, EventDetails, EventFilter, EventStore, LinkReview, ReviewDecision, StoredCalendar, StoredEntry, StoredParticipant, } from "./event-store.js";
8
8
  export { linkEvent } from "./events.js";
9
9
  export { exportMeeting, type MeetingExport, type MeetingExportFormat, type MeetingExportOptions, serializeVtt, } from "./export.js";
10
10
  export { type FindingEvidence, type FindingOutcome, findingHash, MEETING_FINDINGS_KIND, type MeetingFactFinding, type MeetingFinding, type MeetingFindingsInput, type MeetingFindingsReceipt, type MeetingTaskFinding, meetingFindingFingerprint, validateMeetingFindings, } from "./findings.js";
@@ -27,9 +27,7 @@ export const sourceOf = (name, value, providers) => {
27
27
  return source;
28
28
  if (isKind(source))
29
29
  fail(`${name}${source} names ${source}, which a message search does not read — ` +
30
- (source === "events"
31
- ? "no search reads them yet"
32
- : `search ${source === "meetings" ? "all --source meetings" : "notes"} does`));
30
+ `search ${source === "meetings" || source === "events" ? `all --source ${source}` : "notes"} does`);
33
31
  const named = providerNamed(source);
34
32
  if (named && providers.includes(named.name))
35
33
  return named.name;
@@ -0,0 +1,67 @@
1
+ import type { WarmEmbedders } from "../embeddings/embed.js";
2
+ import type { EventHit } from "../store/index.js";
3
+ import type { AccountKey, MessageStore } from "../store/store.js";
4
+ export interface EventsQuery {
5
+ limit?: number;
6
+ offset?: number;
7
+ /** Calendar accounts to read; every one the store holds when absent. */
8
+ accounts?: readonly AccountKey[];
9
+ /** Every word as written: no stems. */
10
+ exact?: boolean;
11
+ /** Latest start first instead of best first. */
12
+ newest?: boolean;
13
+ /** For `date:`, which reads an event's start — the zone a bare day is read in. */
14
+ timezone?: string;
15
+ signal?: AbortSignal;
16
+ /** How many matching events are read before grouping; for tests. */
17
+ candidates?: number;
18
+ env?: NodeJS.ProcessEnv;
19
+ /** The tool's command, named in the hint when the model is missing. */
20
+ command?: string;
21
+ /** The search's own models, when another part of it loads the same one. */
22
+ embedders?: Pick<WarmEmbedders, "get">;
23
+ }
24
+ export interface FoundEvent {
25
+ /** `event:<id>`, of the occurrence shown: of a series, the match nearest now that is not cancelled. */
26
+ ref: string;
27
+ /** The calendar account it is in; `null` for an event no calendar holds, such as a Zoom meeting's own. */
28
+ provider: string | null;
29
+ account: string | null;
30
+ title: string | null;
31
+ starts_at: string | null;
32
+ ends_at: string | null;
33
+ all_day: boolean;
34
+ /** confirmed, tentative or cancelled. */
35
+ status: string | null;
36
+ location: string | null;
37
+ /** The event's page at the calendar. */
38
+ url?: string;
39
+ /** Around the first word of the query in its description, place and attendees. */
40
+ excerpt: string;
41
+ /** How many occurrences of its series matched; 1 for an event that does not repeat. */
42
+ occurrences: number;
43
+ /** When the store's copy last changed. */
44
+ updated_at: string;
45
+ found_by: ("words" | "meaning")[];
46
+ }
47
+ export interface EventsFound {
48
+ query: string;
49
+ /** Words and word stems always; meaning too when the model is there and the query is plain words. */
50
+ by: "words" | "words and meaning";
51
+ /** Why meaning was not searched, when it was not. */
52
+ meaning_skipped?: string;
53
+ hits: FoundEvent[];
54
+ has_more: boolean;
55
+ /** More events matched than one search reads (50,000); the later ones were not read — narrow the query. */
56
+ truncated?: true;
57
+ next_offset?: number;
58
+ }
59
+ /** The calendar accounts the store holds: every account whose provider is of the `events` kind. */
60
+ export declare const calendarAccounts: (store: MessageStore) => Promise<AccountKey[]>;
61
+ export declare const foundEvent: (hit: Omit<EventHit, "relevance">, query: string, foundBy: FoundEvent["found_by"]) => FoundEvent;
62
+ /**
63
+ * Calendar events by words and, when the local text model is there and the query is plain words, by meaning, merged
64
+ * by reciprocal rank: title, cleaned description, place and attendees' names. The occurrences of one series are one
65
+ * hit. Working location, focus time, out of office, birthdays and busy-only slots are never found.
66
+ */
67
+ export declare const searchEvents: (store: MessageStore, query: string, { limit, offset, accounts, exact, newest, timezone, signal, candidates, env, command, embedders, }?: EventsQuery) => Promise<EventsFound>;