@wirecat/cli-core 0.382.0 → 0.384.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 +35 -0
- package/dist/attachments/documents.d.ts +3 -0
- package/dist/attachments/documents.js +17 -0
- package/dist/cli/bot/moderation.js +4 -3
- package/dist/cli/bot/store.js +5 -4
- package/dist/cli/list-query.d.ts +11 -1
- package/dist/cli/list-query.js +11 -1
- package/dist/cli/meeting-batch.js +2 -1
- package/dist/cli/messenger/admin-moderation-command.js +4 -3
- package/dist/cli/messenger/archive-commands.js +5 -3
- package/dist/cli/messenger/backfill-command.js +9 -4
- package/dist/cli/messenger/catch-up-answers.d.ts +5 -2
- package/dist/cli/messenger/catch-up-answers.js +11 -6
- package/dist/cli/messenger/chat-lists.js +11 -6
- package/dist/cli/messenger/chats-stats-command.js +7 -7
- package/dist/cli/messenger/contacts-command.js +4 -3
- package/dist/cli/messenger/conversation-answers.d.ts +2 -2
- package/dist/cli/messenger/conversation-answers.js +5 -2
- package/dist/cli/messenger/conversations-command.js +7 -3
- package/dist/cli/messenger/inbox.js +2 -2
- package/dist/cli/messenger/replies-command.js +4 -3
- package/dist/cli/messenger/review.js +2 -1
- package/dist/cli/messenger/stats-answers.d.ts +1 -1
- package/dist/cli/messenger/stats-answers.js +4 -3
- package/dist/cli/runs/search.d.ts +7 -0
- package/dist/cli/runs/search.js +11 -3
- package/dist/documents/blocks.d.ts +45 -0
- package/dist/documents/blocks.js +48 -0
- package/dist/documents/index.d.ts +5 -0
- package/dist/documents/index.js +4 -0
- package/dist/documents/markdown-blocks.d.ts +3 -0
- package/dist/documents/markdown-blocks.js +123 -0
- package/dist/documents/push-model.d.ts +89 -0
- package/dist/documents/push-model.js +1 -0
- package/dist/documents.d.ts +1 -0
- package/dist/documents.js +1 -0
- package/dist/errors/reasons.js +3 -0
- package/dist/meetings/answers.d.ts +4 -2
- package/dist/meetings/answers.js +2 -1
- package/dist/meetings/cli/index.d.ts +1 -1
- package/dist/meetings/cli/index.js +1 -1
- package/dist/meetings/cli/search.d.ts +5 -0
- package/dist/meetings/cli/search.js +23 -5
- package/dist/meetings/mcp/search.d.ts +3 -1
- package/dist/meetings/mcp/search.js +5 -4
- package/dist/services/admin-statistics.d.ts +3 -0
- package/dist/services/admin-statistics.js +10 -7
- package/dist/services/documents/block-diff.d.ts +24 -0
- package/dist/services/documents/block-diff.js +91 -0
- package/dist/services/documents/push.d.ts +60 -0
- package/dist/services/documents/push.js +218 -0
- package/dist/services/retention.d.ts +7 -0
- package/dist/services/retention.js +10 -5
- package/package.json +5 -1
|
@@ -45,7 +45,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
|
|
|
45
45
|
const hearWith = modelWith(options.transcribe, options.model);
|
|
46
46
|
const context = messengerContext(this, messenger);
|
|
47
47
|
const { settings, renderer, format, env } = context;
|
|
48
|
-
const { since: from, chat } = catchUpQuery(query, REVIEW_QUERY, "review");
|
|
48
|
+
const { since: from, chat, meta } = catchUpQuery(query, REVIEW_QUERY, "review");
|
|
49
49
|
if (options.new && (from !== undefined || options.unanswered !== undefined)) {
|
|
50
50
|
// An open question stays open: moving past it would drop it from the next review.
|
|
51
51
|
throw new CliError("validation_error", "--new keeps its own point — not with a date or --unanswered");
|
|
@@ -96,6 +96,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
|
|
|
96
96
|
transcribe,
|
|
97
97
|
fresh: saved !== undefined,
|
|
98
98
|
...(chat === undefined ? {} : { chat }),
|
|
99
|
+
...(meta === undefined ? {} : { meta }),
|
|
99
100
|
}), () => {
|
|
100
101
|
const read = found.chats
|
|
101
102
|
.flatMap((one) => one.messages.map((message) => ({ ...message, chatTitle: one.title })))
|
|
@@ -67,7 +67,7 @@ export declare const chartAnswer: (chart: ChartData, file?: {
|
|
|
67
67
|
/** A report or a chart is one object: `--jsonl` would print one line pretending to be a stream. */
|
|
68
68
|
export declare const refuseJsonl: (format: string) => void;
|
|
69
69
|
/** `stats chats show`, CLI and MCP alike; a chat the store does not hold whole names the sync that fills it. */
|
|
70
|
-
export declare const chatStatsAnswer: (stats: ChatStats, account: AccountKey) => ItemBody<Of<"chat_stats">>;
|
|
70
|
+
export declare const chatStatsAnswer: (stats: ChatStats, account: AccountKey, meta?: Partial<Meta>) => ItemBody<Of<"chat_stats">>;
|
|
71
71
|
/** Said where the store has kept no join, leave or other chat event for the chat: a 0 would read as nobody joined. */
|
|
72
72
|
export declare const MEMBERS_UNRECORDED = "joins and leaves are not recorded for this chat yet, so they are not counted";
|
|
73
73
|
/** `stats tasks show`, CLI and MCP alike. */
|
|
@@ -167,7 +167,7 @@ export const adminAnswer = (found) => {
|
|
|
167
167
|
rows,
|
|
168
168
|
}, {
|
|
169
169
|
next: drilldownSteps(top?.drilldown, top?.drilldown.arguments.component, top?.message ?? (top?.id.startsWith("msg:") ? top.id : undefined)),
|
|
170
|
-
...
|
|
170
|
+
meta: { ...found.timeMeta, ...(found.hasMore ? { truncated: true } : {}) },
|
|
171
171
|
});
|
|
172
172
|
};
|
|
173
173
|
const cohortRow = (row) => ({
|
|
@@ -221,7 +221,7 @@ export const retentionAnswer = (found) => {
|
|
|
221
221
|
rows: found.items.map(cohortRow),
|
|
222
222
|
}, {
|
|
223
223
|
next: drilldownSteps(top?.drilldown, top?.drilldown.arguments.component),
|
|
224
|
-
...
|
|
224
|
+
meta: { ...found.timeMeta, ...(found.hasMore ? { truncated: true } : {}) },
|
|
225
225
|
});
|
|
226
226
|
};
|
|
227
227
|
const evidenceMessage = (hit) => ({
|
|
@@ -335,7 +335,7 @@ export const refuseJsonl = (format) => {
|
|
|
335
335
|
});
|
|
336
336
|
};
|
|
337
337
|
/** `stats chats show`, CLI and MCP alike; a chat the store does not hold whole names the sync that fills it. */
|
|
338
|
-
export const chatStatsAnswer = (stats, account) => {
|
|
338
|
+
export const chatStatsAnswer = (stats, account, meta) => {
|
|
339
339
|
const { chatId, completeness, topPosts, questions, memberCounts, members, series, fetch } = stats;
|
|
340
340
|
const item = defined({
|
|
341
341
|
chat: chatRef(account, chatId),
|
|
@@ -405,6 +405,7 @@ export const chatStatsAnswer = (stats, account) => {
|
|
|
405
405
|
...(completeness.state === "complete" || fetch === undefined
|
|
406
406
|
? {}
|
|
407
407
|
: { next: [nextStep("messages.sync", { args: { chat: chatId } })] }),
|
|
408
|
+
...(meta ? { meta } : {}),
|
|
408
409
|
});
|
|
409
410
|
};
|
|
410
411
|
/** Said where the store has kept no join, leave or other chat event for the chat: a 0 would read as nobody joined. */
|
|
@@ -42,6 +42,13 @@ export declare const searchRuns: (dir: string, { query, status, errorCode, opera
|
|
|
42
42
|
page: number;
|
|
43
43
|
limit: number;
|
|
44
44
|
hasMore: boolean;
|
|
45
|
+
meta: {
|
|
46
|
+
timezone?: string;
|
|
47
|
+
window?: {
|
|
48
|
+
since?: string;
|
|
49
|
+
until?: string;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
45
52
|
};
|
|
46
53
|
/** A page of `searchRuns` in the list envelope; `args` are the search's own, so `page.next` reruns it. */
|
|
47
54
|
export declare const runsFound: (found: ReturnType<typeof searchRuns>, args: Record<string, unknown>) => import("../envelope.js").ListBody<unknown>;
|
package/dist/cli/runs/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { findRun, listRuns, readEvents } from "../../runs/run.js";
|
|
3
|
-
import {
|
|
3
|
+
import { dateWindow } from "../../services/moment.js";
|
|
4
4
|
import { listOf, snakeKeys } from "../envelope.js";
|
|
5
5
|
const FIELDS = [
|
|
6
6
|
"event",
|
|
@@ -35,7 +35,8 @@ const safeEvent = (event) => Object.fromEntries(FIELDS.flatMap((name) => {
|
|
|
35
35
|
export const searchRuns = (dir, { query = "", status, errorCode, operation, profile, since, limit = 20, page = 1 } = {}) => {
|
|
36
36
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(page) || page < 1)
|
|
37
37
|
throw new CliError("validation_error", "run search uses a limit from 1 to 100 and a positive page");
|
|
38
|
-
const
|
|
38
|
+
const window = dateWindow(since === undefined ? {} : { sinceTime: since });
|
|
39
|
+
const from = window.since;
|
|
39
40
|
if (from !== undefined && !Number.isFinite(from))
|
|
40
41
|
throw new CliError("validation_error", "--since-time must be a valid date or ISO timestamp");
|
|
41
42
|
const needle = query.toLowerCase();
|
|
@@ -100,7 +101,13 @@ export const searchRuns = (dir, { query = "", status, errorCode, operation, prof
|
|
|
100
101
|
if (matches.length > offset + limit)
|
|
101
102
|
break;
|
|
102
103
|
}
|
|
103
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
items: matches.slice(offset, offset + limit),
|
|
106
|
+
page,
|
|
107
|
+
limit,
|
|
108
|
+
hasMore: matches.length > offset + limit,
|
|
109
|
+
meta: window.meta,
|
|
110
|
+
};
|
|
104
111
|
};
|
|
105
112
|
/** A page of `searchRuns` in the list envelope; `args` are the search's own, so `page.next` reruns it. */
|
|
106
113
|
export const runsFound = (found, args) => listOf("runs search", {
|
|
@@ -109,4 +116,5 @@ export const runsFound = (found, args) => listOf("runs search", {
|
|
|
109
116
|
has_more: found.hasMore,
|
|
110
117
|
page: found.page,
|
|
111
118
|
args: Object.fromEntries(Object.entries(args).filter(([name, value]) => value !== undefined && name !== "page")),
|
|
119
|
+
meta: found.meta,
|
|
112
120
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export interface Inline {
|
|
2
|
+
text: string;
|
|
3
|
+
bold?: true;
|
|
4
|
+
italic?: true;
|
|
5
|
+
strike?: true;
|
|
6
|
+
code?: true;
|
|
7
|
+
link?: string;
|
|
8
|
+
}
|
|
9
|
+
export type ListKind = "bullet" | "number" | "check";
|
|
10
|
+
export type Block = {
|
|
11
|
+
kind: "heading";
|
|
12
|
+
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
13
|
+
inlines: Inline[];
|
|
14
|
+
} | {
|
|
15
|
+
kind: "paragraph";
|
|
16
|
+
inlines: Inline[];
|
|
17
|
+
} | {
|
|
18
|
+
kind: "item";
|
|
19
|
+
list: ListKind;
|
|
20
|
+
depth: number;
|
|
21
|
+
checked?: boolean;
|
|
22
|
+
inlines: Inline[];
|
|
23
|
+
} | {
|
|
24
|
+
kind: "code";
|
|
25
|
+
language?: string;
|
|
26
|
+
text: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "quote";
|
|
29
|
+
inlines: Inline[];
|
|
30
|
+
} | {
|
|
31
|
+
kind: "table";
|
|
32
|
+
header: Inline[][];
|
|
33
|
+
rows: Inline[][][];
|
|
34
|
+
} | {
|
|
35
|
+
kind: "image";
|
|
36
|
+
url: string;
|
|
37
|
+
alt: string;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "rule";
|
|
40
|
+
};
|
|
41
|
+
/** Adjacent runs with the same marks merge and empty runs drop, so two equal texts give one key. */
|
|
42
|
+
export declare const normalizeInlines: (inlines: Inline[]) => Inline[];
|
|
43
|
+
export declare const blockKey: (block: Block) => string;
|
|
44
|
+
/** The same key with checklist ticks left out: two blocks that differ only in a tick share it. */
|
|
45
|
+
export declare const blockShape: (block: Block) => string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const MARKS = ["bold", "italic", "strike", "code", "link"];
|
|
2
|
+
const sameMarks = (a, b) => MARKS.every((mark) => a[mark] === b[mark]);
|
|
3
|
+
/** Adjacent runs with the same marks merge and empty runs drop, so two equal texts give one key. */
|
|
4
|
+
export const normalizeInlines = (inlines) => {
|
|
5
|
+
const out = [];
|
|
6
|
+
for (const inline of inlines) {
|
|
7
|
+
if (!inline.text)
|
|
8
|
+
continue;
|
|
9
|
+
const last = out.at(-1);
|
|
10
|
+
if (last && sameMarks(last, inline))
|
|
11
|
+
last.text += inline.text;
|
|
12
|
+
else
|
|
13
|
+
out.push({ ...inline });
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
};
|
|
17
|
+
const inlineKey = (inline) => [inline.text, ...MARKS.map((mark) => inline[mark] ?? "")];
|
|
18
|
+
const key = (block, withChecked) => {
|
|
19
|
+
switch (block.kind) {
|
|
20
|
+
case "heading":
|
|
21
|
+
return [block.kind, block.level, normalizeInlines(block.inlines).map(inlineKey)];
|
|
22
|
+
case "paragraph":
|
|
23
|
+
case "quote":
|
|
24
|
+
return [block.kind, normalizeInlines(block.inlines).map(inlineKey)];
|
|
25
|
+
case "item":
|
|
26
|
+
return [
|
|
27
|
+
block.kind,
|
|
28
|
+
block.list,
|
|
29
|
+
block.depth,
|
|
30
|
+
withChecked && block.list === "check" ? Boolean(block.checked) : "",
|
|
31
|
+
normalizeInlines(block.inlines).map(inlineKey),
|
|
32
|
+
];
|
|
33
|
+
case "code":
|
|
34
|
+
return [block.kind, block.language ?? "", block.text];
|
|
35
|
+
case "table":
|
|
36
|
+
return [
|
|
37
|
+
block.kind,
|
|
38
|
+
[block.header, ...block.rows].map((row) => row.map((cell) => normalizeInlines(cell).map(inlineKey))),
|
|
39
|
+
];
|
|
40
|
+
case "image":
|
|
41
|
+
return [block.kind, block.url, block.alt];
|
|
42
|
+
case "rule":
|
|
43
|
+
return [block.kind];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export const blockKey = (block) => JSON.stringify(key(block, true));
|
|
47
|
+
/** The same key with checklist ticks left out: two blocks that differ only in a tick share it. */
|
|
48
|
+
export const blockShape = (block) => JSON.stringify(key(block, false));
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
+
export { type BlockHunk, diffBlocks, type Reconciled, reconcileBlocks } from "../services/documents/block-diff.js";
|
|
1
2
|
export { type Complete, extractMemories } from "../services/documents/extraction.js";
|
|
2
3
|
export { documentIngestion, type IngestionOptions } from "../services/documents/ingestion.js";
|
|
3
4
|
export { type DocumentKnowledge, documentKnowledge } from "../services/documents/knowledge.js";
|
|
4
5
|
export type { SyncOptions } from "../services/documents/ports.js";
|
|
5
6
|
export { extractDocumentMemories, type ImportedSource, type ProposalOptions } from "../services/documents/proposals.js";
|
|
7
|
+
export { type PushOptions, type PushResult, planPush, pushDocuments, scanLocal, } from "../services/documents/push.js";
|
|
6
8
|
export { type SyncResult, syncDocuments } from "../services/documents/sync.js";
|
|
9
|
+
export { type Block, blockKey, blockShape, type Inline, type ListKind, normalizeInlines } from "./blocks.js";
|
|
7
10
|
export { catalogFiles } from "./catalog.js";
|
|
11
|
+
export { markdownBlocks } from "./markdown-blocks.js";
|
|
8
12
|
export type { DocumentSource, IndexedSource, PictureWriter, SourceChange, SourceContent, SourceFile, SourceIndex, SourceSkip, SourceState, SourceStatePort, SourceWalk, SourceWork, TabText, } from "./model.js";
|
|
13
|
+
export type { DocumentTarget, LocalDocument, Prepared, PushEntry, Pushed, PushPlan, PushState, PushStatePort, } from "./push-model.js";
|
|
9
14
|
export { within } from "./scope.js";
|
package/dist/documents/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
export { diffBlocks, reconcileBlocks } from "../services/documents/block-diff.js";
|
|
1
2
|
export { extractMemories } from "../services/documents/extraction.js";
|
|
2
3
|
export { documentIngestion } from "../services/documents/ingestion.js";
|
|
3
4
|
export { documentKnowledge } from "../services/documents/knowledge.js";
|
|
4
5
|
export { extractDocumentMemories } from "../services/documents/proposals.js";
|
|
6
|
+
export { planPush, pushDocuments, scanLocal, } from "../services/documents/push.js";
|
|
5
7
|
export { syncDocuments } from "../services/documents/sync.js";
|
|
8
|
+
export { blockKey, blockShape, normalizeInlines } from "./blocks.js";
|
|
6
9
|
export { catalogFiles } from "./catalog.js";
|
|
10
|
+
export { markdownBlocks } from "./markdown-blocks.js";
|
|
7
11
|
export { within } from "./scope.js";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { normalizeInlines } from "./blocks.js";
|
|
2
|
+
// Loaded on first use: importing the documents entry point would otherwise cost every command about 50 ms.
|
|
3
|
+
let parser;
|
|
4
|
+
const loadParser = () => (parser ??= Promise.all([
|
|
5
|
+
import("mdast-util-from-markdown"),
|
|
6
|
+
import("mdast-util-gfm"),
|
|
7
|
+
import("micromark-extension-gfm"),
|
|
8
|
+
]).then(([{ fromMarkdown }, { gfmFromMarkdown }, { gfm }]) => (markdown) => fromMarkdown(markdown, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })));
|
|
9
|
+
/** Markdown (CommonMark + GFM tables, task lists and strikethrough) as the blocks a provider can write natively. */
|
|
10
|
+
export const markdownBlocks = async (markdown) => {
|
|
11
|
+
const tree = (await loadParser())(markdown);
|
|
12
|
+
const definitions = new Map();
|
|
13
|
+
const collect = (node) => {
|
|
14
|
+
if (node.type === "definition")
|
|
15
|
+
definitions.set(node.identifier, node);
|
|
16
|
+
if ("children" in node)
|
|
17
|
+
for (const child of node.children)
|
|
18
|
+
collect(child);
|
|
19
|
+
};
|
|
20
|
+
collect(tree);
|
|
21
|
+
const images = [];
|
|
22
|
+
const inlinesOf = (nodes, marks = {}) => nodes.flatMap((node) => {
|
|
23
|
+
switch (node.type) {
|
|
24
|
+
case "text":
|
|
25
|
+
return [{ ...marks, text: node.value.replace(/\n/g, " ") }];
|
|
26
|
+
case "inlineCode":
|
|
27
|
+
return [{ ...marks, code: true, text: node.value }];
|
|
28
|
+
case "strong":
|
|
29
|
+
return inlinesOf(node.children, { ...marks, bold: true });
|
|
30
|
+
case "emphasis":
|
|
31
|
+
return inlinesOf(node.children, { ...marks, italic: true });
|
|
32
|
+
case "delete":
|
|
33
|
+
return inlinesOf(node.children, { ...marks, strike: true });
|
|
34
|
+
case "link":
|
|
35
|
+
return inlinesOf(node.children, { ...marks, link: node.url });
|
|
36
|
+
case "linkReference": {
|
|
37
|
+
const url = definitions.get(node.identifier)?.url;
|
|
38
|
+
return inlinesOf(node.children, url ? { ...marks, link: url } : marks);
|
|
39
|
+
}
|
|
40
|
+
case "break":
|
|
41
|
+
return [{ ...marks, text: "\n" }];
|
|
42
|
+
case "image":
|
|
43
|
+
images.push({ kind: "image", url: node.url, alt: node.alt ?? "" });
|
|
44
|
+
return [];
|
|
45
|
+
case "imageReference": {
|
|
46
|
+
const url = definitions.get(node.identifier)?.url;
|
|
47
|
+
if (url)
|
|
48
|
+
images.push({ kind: "image", url, alt: node.alt ?? "" });
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
case "html":
|
|
52
|
+
return [{ ...marks, text: node.value }];
|
|
53
|
+
default:
|
|
54
|
+
return "children" in node ? inlinesOf(node.children, marks) : [];
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
const trimmed = (inlines) => {
|
|
58
|
+
const out = normalizeInlines(inlines);
|
|
59
|
+
const first = out[0];
|
|
60
|
+
const last = out.at(-1);
|
|
61
|
+
if (first)
|
|
62
|
+
first.text = first.text.trimStart();
|
|
63
|
+
if (last)
|
|
64
|
+
last.text = last.text.trimEnd();
|
|
65
|
+
return normalizeInlines(out);
|
|
66
|
+
};
|
|
67
|
+
const withImages = (block) => {
|
|
68
|
+
const found = images.splice(0);
|
|
69
|
+
if (block?.kind === "paragraph" && !block.inlines.length)
|
|
70
|
+
return found;
|
|
71
|
+
return block ? [block, ...found] : found;
|
|
72
|
+
};
|
|
73
|
+
const textOf = (node) => node.children.flatMap((child) => child.type === "paragraph" || child.type === "heading" ? inlinesOf(child.children) : []);
|
|
74
|
+
const blocks = (nodes, depth = 0) => nodes.flatMap((node) => {
|
|
75
|
+
switch (node.type) {
|
|
76
|
+
case "heading":
|
|
77
|
+
return withImages({ kind: "heading", level: node.depth, inlines: trimmed(inlinesOf(node.children)) });
|
|
78
|
+
case "paragraph":
|
|
79
|
+
return withImages({ kind: "paragraph", inlines: trimmed(inlinesOf(node.children)) });
|
|
80
|
+
case "code":
|
|
81
|
+
return [{ kind: "code", ...(node.lang ? { language: node.lang } : {}), text: node.value }];
|
|
82
|
+
case "blockquote":
|
|
83
|
+
return node.children.flatMap((child) => child.type === "paragraph" || child.type === "heading"
|
|
84
|
+
? withImages({ kind: "quote", inlines: trimmed(inlinesOf(child.children)) })
|
|
85
|
+
: blocks([child], depth));
|
|
86
|
+
case "thematicBreak":
|
|
87
|
+
return [{ kind: "rule" }];
|
|
88
|
+
case "table": {
|
|
89
|
+
const [head, ...rows] = node.children.map((row) => row.children.map((cell) => trimmed(inlinesOf(cell.children))));
|
|
90
|
+
return [{ kind: "table", header: head ?? [], rows }];
|
|
91
|
+
}
|
|
92
|
+
case "list":
|
|
93
|
+
return node.children.flatMap((item) => {
|
|
94
|
+
const [first, ...rest] = item.children;
|
|
95
|
+
const head = {
|
|
96
|
+
kind: "item",
|
|
97
|
+
list: (item.checked != null ? "check" : node.ordered ? "number" : "bullet"),
|
|
98
|
+
depth,
|
|
99
|
+
...(item.checked != null ? { checked: item.checked } : {}),
|
|
100
|
+
inlines: trimmed(first && first.type === "paragraph" ? inlinesOf(first.children) : []),
|
|
101
|
+
};
|
|
102
|
+
const tail = first && first.type !== "paragraph" ? [first, ...rest] : rest;
|
|
103
|
+
return [
|
|
104
|
+
...withImages(head),
|
|
105
|
+
...tail.flatMap((child) => child.type === "list"
|
|
106
|
+
? blocks([child], depth + 1)
|
|
107
|
+
: child.type === "paragraph"
|
|
108
|
+
? withImages({ kind: "paragraph", inlines: trimmed(inlinesOf(child.children)) })
|
|
109
|
+
: blocks([child], depth)),
|
|
110
|
+
];
|
|
111
|
+
});
|
|
112
|
+
case "html":
|
|
113
|
+
return [{ kind: "paragraph", inlines: [{ text: node.value }] }];
|
|
114
|
+
case "definition":
|
|
115
|
+
case "footnoteDefinition":
|
|
116
|
+
case "yaml":
|
|
117
|
+
return [];
|
|
118
|
+
default:
|
|
119
|
+
return "children" in node ? withImages({ kind: "paragraph", inlines: trimmed(textOf(node)) }) : [];
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return blocks(tree.children);
|
|
123
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/** A local file a push mirrors: its path relative to the mirrored folder, with `/` separators. */
|
|
2
|
+
export interface LocalDocument {
|
|
3
|
+
path: string;
|
|
4
|
+
absolute: string;
|
|
5
|
+
hash: string;
|
|
6
|
+
kind: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Pushed<S> {
|
|
9
|
+
id: string;
|
|
10
|
+
url: string;
|
|
11
|
+
/** What the provider reads back after its own write; the next push compares the remote against it. */
|
|
12
|
+
snapshot: S;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* What an update found before writing. Only `write` reaches the remote, so a conflict or a file with nothing to
|
|
16
|
+
* change never counts as a write.
|
|
17
|
+
*/
|
|
18
|
+
export type Prepared<S> = {
|
|
19
|
+
conflict: string;
|
|
20
|
+
} | {
|
|
21
|
+
missing: true;
|
|
22
|
+
} | {
|
|
23
|
+
unchanged: Pushed<S>;
|
|
24
|
+
} | {
|
|
25
|
+
write(): Promise<Pushed<S>>;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* A provider that writes local files as its own native documents. Every write is one provider call per
|
|
29
|
+
* file (or one all-or-nothing batch): a write cut in half would read back as somebody else's edit.
|
|
30
|
+
*/
|
|
31
|
+
export interface DocumentTarget<S> {
|
|
32
|
+
identity(): Promise<string>;
|
|
33
|
+
/** The provider's document kind for a local path, or undefined when the path is not pushed. */
|
|
34
|
+
kindOf(path: string): string | undefined;
|
|
35
|
+
/** Finds the folder carrying `origin` or creates it: a crash between create and save must not duplicate it. */
|
|
36
|
+
ensureFolder(input: {
|
|
37
|
+
parent: string;
|
|
38
|
+
name: string;
|
|
39
|
+
origin: string;
|
|
40
|
+
}): Promise<string>;
|
|
41
|
+
findByOrigin(origin: string): Promise<Pushed<S> | undefined>;
|
|
42
|
+
create(input: {
|
|
43
|
+
file: LocalDocument;
|
|
44
|
+
name: string;
|
|
45
|
+
parent: string;
|
|
46
|
+
origin: string;
|
|
47
|
+
operationId: string;
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}): Promise<Pushed<S>>;
|
|
50
|
+
/** Reads the remote and compares it with `recorded`; a conflict instead of a write unless `force`. */
|
|
51
|
+
prepare(input: {
|
|
52
|
+
id: string;
|
|
53
|
+
file: LocalDocument;
|
|
54
|
+
recorded: S;
|
|
55
|
+
force: boolean;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
}): Promise<Prepared<S>>;
|
|
58
|
+
}
|
|
59
|
+
export interface PushEntry<S> {
|
|
60
|
+
id: string;
|
|
61
|
+
url: string;
|
|
62
|
+
kind: string;
|
|
63
|
+
hash: string;
|
|
64
|
+
snapshot: S;
|
|
65
|
+
pushedAt: string;
|
|
66
|
+
}
|
|
67
|
+
export interface PushState<S> {
|
|
68
|
+
version: 1;
|
|
69
|
+
principal: string;
|
|
70
|
+
root: string;
|
|
71
|
+
target: string;
|
|
72
|
+
folders: Record<string, string>;
|
|
73
|
+
entries: Record<string, PushEntry<S>>;
|
|
74
|
+
/** Saved before a create, so a crash after the provider wrote but before this state was saved finds the file. */
|
|
75
|
+
pending: Record<string, {
|
|
76
|
+
origin: string;
|
|
77
|
+
operationId: string;
|
|
78
|
+
}>;
|
|
79
|
+
}
|
|
80
|
+
export interface PushStatePort<S> {
|
|
81
|
+
read(): PushState<S>;
|
|
82
|
+
save(state: PushState<S>): void;
|
|
83
|
+
}
|
|
84
|
+
export interface PushPlan {
|
|
85
|
+
create: LocalDocument[];
|
|
86
|
+
update: LocalDocument[];
|
|
87
|
+
unchanged: LocalDocument[];
|
|
88
|
+
orphans: string[];
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/documents.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { readSpreadsheet } from "./attachments/documents.js";
|
|
1
2
|
export type { Engine, Extraction, FileHint, LoadEngine } from "./attachments/extract.js";
|
|
2
3
|
export { extractText, importEngine, MAX_FILE_BYTES, MAX_TEXT_CHARS } from "./attachments/extract.js";
|
|
3
4
|
export { attachmentMaxBytes } from "./attachments/limits.js";
|
package/dist/documents.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { readSpreadsheet } from "./attachments/documents.js";
|
|
1
2
|
export { extractText, importEngine, MAX_FILE_BYTES, MAX_TEXT_CHARS } from "./attachments/extract.js";
|
|
2
3
|
export { attachmentMaxBytes } from "./attachments/limits.js";
|
|
3
4
|
export { columnName, sheetText } from "./attachments/sheets.js";
|
package/dist/errors/reasons.js
CHANGED
|
@@ -26,6 +26,9 @@ const OTHERS = [
|
|
|
26
26
|
validation("extraction.text-too-long", "the text is too long for memory extraction"),
|
|
27
27
|
validation("file.locked", "another process holds the file's lock"),
|
|
28
28
|
["profile.wrong-account", "authentication_error", "the signed-in source account does not match this profile"],
|
|
29
|
+
validation("push.too-many-files", "the folder holds more files than one push takes"),
|
|
30
|
+
validation("push.unreadable", "the folder to push could not be read"),
|
|
31
|
+
validation("spreadsheet.unreadable", "the workbook could not be read"),
|
|
29
32
|
validation("store.refused", "the local store refused the document text"),
|
|
30
33
|
["store.not-writable", "permission_error", "the message store cannot be written from this process"],
|
|
31
34
|
validation("catalog.scope-limit", "the selected catalog exceeds its entry limit"),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ItemEnvelope, type ListEnvelope } from "@wirecat/cli-base/standards";
|
|
1
|
+
import { type ItemEnvelope, type ListEnvelope, type Meta } from "@wirecat/cli-base/standards";
|
|
2
2
|
import * as v from "valibot";
|
|
3
3
|
import { type MeetingOwner } from "./item.js";
|
|
4
4
|
import type { Summary, Transcript, TranscriptRow } from "./model.js";
|
|
@@ -160,4 +160,6 @@ export declare const MEETING_STEPS: ({
|
|
|
160
160
|
export declare const meetingHitItem: (hit: SearchHit & {
|
|
161
161
|
ref: string;
|
|
162
162
|
}) => MeetingHitItem;
|
|
163
|
-
export declare const meetingHitsList: (found: MeetingSearchPage, limit: number, command?: string, args?: Record<string, unknown>) => MeetingListBody<MeetingHitItem
|
|
163
|
+
export declare const meetingHitsList: (found: MeetingSearchPage, limit: number, command?: string, args?: Record<string, unknown>, meta?: Partial<Meta>) => MeetingListBody<MeetingHitItem> & {
|
|
164
|
+
meta?: Partial<Meta>;
|
|
165
|
+
};
|
package/dist/meetings/answers.js
CHANGED
|
@@ -201,7 +201,7 @@ export const meetingHitItem = (hit) => ({
|
|
|
201
201
|
},
|
|
202
202
|
}),
|
|
203
203
|
});
|
|
204
|
-
export const meetingHitsList = (found, limit, command = "search meetings", args = {}) => {
|
|
204
|
+
export const meetingHitsList = (found, limit, command = "search meetings", args = {}, meta = {}) => {
|
|
205
205
|
const items = found.items.map(meetingHitItem);
|
|
206
206
|
const cursor = found.hasMore && found.nextCursor !== null ? found.nextCursor : undefined;
|
|
207
207
|
const pending = found.coverage.indexing === "pending";
|
|
@@ -238,5 +238,6 @@ export const meetingHitsList = (found, limit, command = "search meetings", args
|
|
|
238
238
|
],
|
|
239
239
|
}
|
|
240
240
|
: {}),
|
|
241
|
+
...(Object.keys(meta).length === 0 ? {} : { meta }),
|
|
241
242
|
};
|
|
242
243
|
};
|
|
@@ -17,4 +17,4 @@ export declare const addMeetingCommands: (program: Command, deps: MeetingCommand
|
|
|
17
17
|
export { addMeetingPassageCommand, createMeetingPassageCommand, type MeetingPassageFlags, meetingPassageRequest, } from "./passage.js";
|
|
18
18
|
export { addMeetingProjectEvidenceCommand } from "./project-evidence.js";
|
|
19
19
|
export { queryWordsAfterOptions } from "./query-options.js";
|
|
20
|
-
export { addMeetingSearchCommand, createMeetingSearchCommand, type MeetingHitSearch, type MeetingHitSearchOptions, type MeetingSearchFlags, meetingHitSearchRequest, meetingSearchArguments, validateMeetingHitSearch, } from "./search.js";
|
|
20
|
+
export { addMeetingSearchCommand, createMeetingSearchCommand, type MeetingHitSearch, type MeetingHitSearchOptions, type MeetingSearchFlags, meetingHitSearchRequest, meetingSearchArguments, meetingSearchMeta, validateMeetingHitSearch, } from "./search.js";
|
|
@@ -144,4 +144,4 @@ export const addMeetingCommands = (program, deps) => {
|
|
|
144
144
|
export { addMeetingPassageCommand, createMeetingPassageCommand, meetingPassageRequest, } from "./passage.js";
|
|
145
145
|
export { addMeetingProjectEvidenceCommand } from "./project-evidence.js";
|
|
146
146
|
export { queryWordsAfterOptions } from "./query-options.js";
|
|
147
|
-
export { addMeetingSearchCommand, createMeetingSearchCommand, meetingHitSearchRequest, meetingSearchArguments, validateMeetingHitSearch, } from "./search.js";
|
|
147
|
+
export { addMeetingSearchCommand, createMeetingSearchCommand, meetingHitSearchRequest, meetingSearchArguments, meetingSearchMeta, validateMeetingHitSearch, } from "./search.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Meta } from "@wirecat/cli-base/standards";
|
|
1
2
|
import { Command } from "commander";
|
|
2
3
|
import type { MeetingSearchPage } from "../search-store.js";
|
|
3
4
|
import { type MeetingHitSearchOptions } from "../search-validation.js";
|
|
@@ -13,6 +14,8 @@ export interface MeetingSearchFlags {
|
|
|
13
14
|
topic?: string;
|
|
14
15
|
sinceTime?: number;
|
|
15
16
|
untilTime?: number;
|
|
17
|
+
/** The zone a date or time without one was read in; the parser sets it, since the typed text is gone after. */
|
|
18
|
+
timesReadIn?: string;
|
|
16
19
|
limit?: number;
|
|
17
20
|
cursor?: string;
|
|
18
21
|
maxReadBytes?: number;
|
|
@@ -23,6 +26,8 @@ export declare const createMeetingSearchCommand: (program: Command) => Command;
|
|
|
23
26
|
export declare const meetingHitSearchRequest: (query: string, options: MeetingSearchFlags, signal?: AbortSignal) => MeetingHitSearchOptions;
|
|
24
27
|
/** Parsed dates must become ISO values again so the CLI can replay its own continuation. */
|
|
25
28
|
export declare const meetingSearchArguments: (query: string, options: MeetingSearchFlags) => Record<string, unknown>;
|
|
29
|
+
/** What `meta` says about the dates: the zone one without its own was read in, and the window in UTC. */
|
|
30
|
+
export declare const meetingSearchMeta: (options: MeetingSearchFlags) => Pick<Meta, "timezone" | "window">;
|
|
26
31
|
export declare const addMeetingSearchCommand: (program: Command, deps: {
|
|
27
32
|
search: (query: string, request: MeetingHitSearchOptions, options: MeetingSearchFlags & Record<string, unknown>) => Promise<MeetingSearchPage>;
|
|
28
33
|
signal: () => AbortSignal | undefined;
|
|
@@ -5,13 +5,19 @@ import { Command } from "commander";
|
|
|
5
5
|
import { meetingHitsList } from "../answers.js";
|
|
6
6
|
import { validateMeetingHitSearch } from "../search-validation.js";
|
|
7
7
|
export { validateMeetingHitSearch } from "../search-validation.js";
|
|
8
|
-
const
|
|
8
|
+
const ZONED = /(?:Z|[+-]\d{2}:?\d{2})$/i;
|
|
9
|
+
const moment = (flag, end, readIn) => (value) => {
|
|
10
|
+
const zone = timezoneOf();
|
|
11
|
+
let time;
|
|
9
12
|
try {
|
|
10
|
-
|
|
13
|
+
time = zonedMoment(value, zone, { start: 0, end: value.length }, { end });
|
|
11
14
|
}
|
|
12
15
|
catch {
|
|
13
16
|
throw new CliError("validation_error", `${flag} takes an ISO date, or a time with or without an offset`);
|
|
14
17
|
}
|
|
18
|
+
if (!ZONED.test(value.trim()))
|
|
19
|
+
readIn(zone);
|
|
20
|
+
return time;
|
|
15
21
|
};
|
|
16
22
|
const integer = (value) => {
|
|
17
23
|
if (!/^[1-9]\d*$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
@@ -25,6 +31,7 @@ export const createMeetingSearchCommand = (program) => {
|
|
|
25
31
|
search = new Command("search").description("search the stored resources");
|
|
26
32
|
program.addCommand(search);
|
|
27
33
|
}
|
|
34
|
+
const readIn = (zone) => command.setOptionValue("timesReadIn", zone);
|
|
28
35
|
const command = search
|
|
29
36
|
.command("meetings")
|
|
30
37
|
.description("find words in stored meetings, indexing new ones first")
|
|
@@ -37,8 +44,8 @@ export const createMeetingSearchCommand = (program) => {
|
|
|
37
44
|
.option("--project <name>", "meetings confirmed in this project, by name, key or alias")
|
|
38
45
|
.option("--tag <name>", "meetings with this tag, by name or alias")
|
|
39
46
|
.option("--topic <name>", "meetings with this topic, by name or alias")
|
|
40
|
-
.option("--since-time <time>", "meetings starting at or after this ISO 8601 date or time", moment("--since-time", false))
|
|
41
|
-
.option("--until-time <time>", "meetings starting at or before this ISO 8601 date or time; a date alone covers that whole local day", moment("--until-time", true))
|
|
47
|
+
.option("--since-time <time>", "meetings starting at or after this ISO 8601 date or time", moment("--since-time", false, readIn))
|
|
48
|
+
.option("--until-time <time>", "meetings starting at or before this ISO 8601 date or time; a date alone covers that whole local day", moment("--until-time", true, readIn))
|
|
42
49
|
.option("--limit <n>", "how many hits, 1 through 1000 (default 100)", integer)
|
|
43
50
|
.option("--cursor <cursor>", "continue from the previous page's page.next_cursor, with the same query and filters")
|
|
44
51
|
.option("--max-read-bytes <bytes>", "most stored bytes to read, 1 through 67108864 (default 4194304)", integer);
|
|
@@ -86,7 +93,18 @@ export const meetingSearchArguments = (query, options) => Object.fromEntries(Obj
|
|
|
86
93
|
until_time: options.untilTime === undefined ? undefined : new Date(options.untilTime).toISOString(),
|
|
87
94
|
max_read_bytes: options.maxReadBytes ?? 4194304,
|
|
88
95
|
}).filter(([, value]) => value !== undefined));
|
|
96
|
+
/** What `meta` says about the dates: the zone one without its own was read in, and the window in UTC. */
|
|
97
|
+
export const meetingSearchMeta = (options) => {
|
|
98
|
+
const window = {
|
|
99
|
+
...(options.sinceTime === undefined ? {} : { since: new Date(options.sinceTime).toISOString() }),
|
|
100
|
+
...(options.untilTime === undefined ? {} : { until: new Date(options.untilTime).toISOString() }),
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
...(options.timesReadIn === undefined ? {} : { timezone: options.timesReadIn }),
|
|
104
|
+
...(Object.keys(window).length === 0 ? {} : { window }),
|
|
105
|
+
};
|
|
106
|
+
};
|
|
89
107
|
export const addMeetingSearchCommand = (program, deps) => createMeetingSearchCommand(program).action(async (query, options) => {
|
|
90
108
|
const request = meetingHitSearchRequest(query, options, deps.signal());
|
|
91
|
-
deps.output(meetingHitsList(await deps.search(query, request, options), request.limit ?? 100, "search meetings", meetingSearchArguments(query, options)));
|
|
109
|
+
deps.output(meetingHitsList(await deps.search(query, request, options), request.limit ?? 100, "search meetings", meetingSearchArguments(query, options), meetingSearchMeta(options)));
|
|
92
110
|
});
|
|
@@ -150,7 +150,9 @@ export declare const callMeetingSearchTool: (search: MeetingHitSearch, input: un
|
|
|
150
150
|
started_at: string | null;
|
|
151
151
|
url: string | null;
|
|
152
152
|
} | undefined;
|
|
153
|
-
}
|
|
153
|
+
}> & {
|
|
154
|
+
meta?: Partial<import("@wirecat/cli-base/standards").Meta>;
|
|
155
|
+
};
|
|
154
156
|
} & {
|
|
155
157
|
isError?: boolean;
|
|
156
158
|
})>;
|
|
@@ -60,10 +60,11 @@ export const callMeetingSearchTool = async (search, input, signal, failure = {})
|
|
|
60
60
|
const query = args.query;
|
|
61
61
|
validateMeetingHitSearch(query, options);
|
|
62
62
|
signal?.throwIfAborted();
|
|
63
|
-
const
|
|
64
|
-
...
|
|
65
|
-
|
|
66
|
-
}
|
|
63
|
+
const window = {
|
|
64
|
+
...(options.since === undefined ? {} : { since: new Date(options.since).toISOString() }),
|
|
65
|
+
...(options.until === undefined ? {} : { until: new Date(options.until).toISOString() }),
|
|
66
|
+
};
|
|
67
|
+
const result = meetingHitsList(await search(query, options), options.limit ?? 100, "search meetings", { ...args, max_read_bytes: options.maxReadBytes ?? 4194304 }, Object.keys(window).length === 0 ? {} : { window });
|
|
67
68
|
signal?.throwIfAborted();
|
|
68
69
|
return agentResult({
|
|
69
70
|
content: [{ type: "text", text: JSON.stringify(result) }],
|