@wirecat/cli-core 0.449.0 → 0.451.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 +64 -0
- package/README.md +7 -5
- package/dist/cli/command-availability.d.ts +2 -1
- package/dist/cli/command-availability.js +8 -5
- package/dist/cli/config-command.d.ts +3 -3
- package/dist/cli/config-command.js +2 -2
- package/dist/cli/early-refusal.d.ts +13 -0
- package/dist/cli/early-refusal.js +44 -2
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/mcp-server-command.js +9 -5
- package/dist/cli/messenger/mcp-command.d.ts +4 -1
- package/dist/cli/messenger/mcp-command.js +108 -12
- package/dist/cli/permission-keys.d.ts +21 -4
- package/dist/cli/permission-keys.js +31 -1
- package/dist/cli/preview.d.ts +2 -1
- package/dist/cli/preview.js +8 -6
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +10 -3
- package/dist/cli/settings.d.ts +9 -2
- package/dist/cli/settings.js +21 -6
- package/dist/cli/shared-command-exports.d.ts +7 -5
- package/dist/embeddings/embed.js +4 -21
- package/dist/embeddings/onnx-runtime.d.ts +14 -1
- package/dist/embeddings/onnx-runtime.js +50 -2
- package/dist/http/public-url.d.ts +5 -1
- package/dist/http/public-url.js +11 -3
- package/dist/images/picture/embedder.js +9 -8
- package/dist/mcp/agent.d.ts +5 -0
- package/dist/mcp/agent.js +7 -4
- package/dist/mcp/all-profiles.d.ts +47 -0
- package/dist/mcp/all-profiles.js +257 -0
- package/dist/mcp/http/oauth.d.ts +2 -2
- package/dist/mcp/http/oauth.js +8 -7
- package/dist/mcp/http/serve.d.ts +6 -1
- package/dist/mcp/http/serve.js +24 -7
- package/dist/mcp/index.d.ts +2 -1
- package/dist/mcp/index.js +2 -1
- package/dist/mcp/instructions.d.ts +2 -1
- package/dist/mcp/instructions.js +35 -29
- package/dist/mcp/server.js +3 -73
- package/dist/mcp/stdio-wire.d.ts +14 -0
- package/dist/mcp/stdio-wire.js +72 -0
- package/dist/sends/index.d.ts +1 -1
- package/dist/sends/index.js +1 -1
- package/dist/sends/permissions.d.ts +10 -2
- package/dist/sends/permissions.js +15 -7
- package/dist/store/sqlite/note-search.js +66 -22
- package/package.json +4 -4
package/dist/mcp/server.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { PassThrough, Writable } from "node:stream";
|
|
2
1
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3
2
|
import { StdioServerTransport, serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
4
3
|
import { CliError, mcpErrorResult } from "@wirecat/cli-base";
|
|
@@ -19,6 +18,7 @@ import { syncsFirst } from "../cli/messenger/search-sync-options.js";
|
|
|
19
18
|
import { stdinFieldOf } from "../cli/stdin-field.js";
|
|
20
19
|
import { rootOf } from "../cli/steps.js";
|
|
21
20
|
import { warmEmbedders } from "../embeddings/embed.js";
|
|
21
|
+
import { mcpUrlOf } from "../http/public-url.js";
|
|
22
22
|
import { readOnlyLockOf } from "../ports/app.js";
|
|
23
23
|
import { guardFor } from "../sends/guard.js";
|
|
24
24
|
import { levelFor, readKeysForCommand } from "../sends/permissions.js";
|
|
@@ -30,6 +30,7 @@ import { instructions } from "./instructions.js";
|
|
|
30
30
|
import { registerPrompts } from "./prompts.js";
|
|
31
31
|
import { registerResources } from "./resources.js";
|
|
32
32
|
import { harmless, MessengerSession } from "./session.js";
|
|
33
|
+
import { answeredWire } from "./stdio-wire.js";
|
|
33
34
|
import { normal, outputOf, registerCommands, surfaceCommands } from "./surface.js";
|
|
34
35
|
import { agentJson } from "./text.js";
|
|
35
36
|
import { answered, commandOf, failed, outcomeOf, READ, tierOf, tool, toolKey, WRITE, } from "./tool.js";
|
|
@@ -357,77 +358,6 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
357
358
|
};
|
|
358
359
|
return { session, embedders, build, log, offered: [...commands.map((one) => one.command), ...pictured] };
|
|
359
360
|
};
|
|
360
|
-
const lineReader = (onMessage) => {
|
|
361
|
-
let rest = "";
|
|
362
|
-
return (chunk) => {
|
|
363
|
-
const lines = (rest + chunk.toString()).split("\n");
|
|
364
|
-
rest = lines.pop() ?? "";
|
|
365
|
-
for (const line of lines) {
|
|
366
|
-
try {
|
|
367
|
-
const message = JSON.parse(line);
|
|
368
|
-
if (message !== null && typeof message === "object")
|
|
369
|
-
onMessage(message);
|
|
370
|
-
}
|
|
371
|
-
catch {
|
|
372
|
-
// Not a message; the transport answers it as a parse error.
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
};
|
|
377
|
-
/**
|
|
378
|
-
* Stdin and stdout as the transport sees them, noting each request that comes in and each answer that goes out, so
|
|
379
|
-
* the server can tell when everything asked has been answered. The transport's stdin never ends by itself: the server
|
|
380
|
-
* closes it, after the client's stdin ended and the last answer went out.
|
|
381
|
-
*/
|
|
382
|
-
const answeredWire = (source, sink) => {
|
|
383
|
-
const asked = new Set();
|
|
384
|
-
let settle;
|
|
385
|
-
const input = new PassThrough();
|
|
386
|
-
const onData = lineReader(({ id, method }) => {
|
|
387
|
-
if (id !== undefined && typeof method === "string")
|
|
388
|
-
asked.add(JSON.stringify(id));
|
|
389
|
-
});
|
|
390
|
-
const output = new Writable({
|
|
391
|
-
write: (chunk, _encoding, done) => {
|
|
392
|
-
noteAnswer(chunk);
|
|
393
|
-
sink.write(chunk, done);
|
|
394
|
-
},
|
|
395
|
-
});
|
|
396
|
-
const noteAnswer = lineReader(({ id, method }) => {
|
|
397
|
-
if (id === undefined || method !== undefined)
|
|
398
|
-
return;
|
|
399
|
-
asked.delete(JSON.stringify(id));
|
|
400
|
-
if (asked.size === 0)
|
|
401
|
-
settle?.();
|
|
402
|
-
});
|
|
403
|
-
const forward = (chunk) => {
|
|
404
|
-
onData(chunk);
|
|
405
|
-
input.write(chunk);
|
|
406
|
-
};
|
|
407
|
-
source.on("data", forward);
|
|
408
|
-
const ended = new Promise((resolve) => {
|
|
409
|
-
source.once("end", resolve);
|
|
410
|
-
source.once("close", resolve);
|
|
411
|
-
});
|
|
412
|
-
return {
|
|
413
|
-
input,
|
|
414
|
-
output,
|
|
415
|
-
ended,
|
|
416
|
-
answered: () => new Promise((resolve) => {
|
|
417
|
-
if (asked.size === 0)
|
|
418
|
-
resolve();
|
|
419
|
-
else
|
|
420
|
-
settle = resolve;
|
|
421
|
-
}),
|
|
422
|
-
/** Lets go of the real stdin, so it no longer holds the process open. */
|
|
423
|
-
release: () => {
|
|
424
|
-
source.off("data", forward);
|
|
425
|
-
if (source.listenerCount("data") === 0)
|
|
426
|
-
source.pause();
|
|
427
|
-
input.end();
|
|
428
|
-
},
|
|
429
|
-
};
|
|
430
|
-
};
|
|
431
361
|
/**
|
|
432
362
|
* Serves until the client closes stdin or the process is told to stop, then closes the connection.
|
|
433
363
|
* **Returning is what lets the process exit**: the transport lets go of stdin, and the session is
|
|
@@ -487,7 +417,7 @@ export const serveOverHttpUntilStopped = async (command, context, messenger, opt
|
|
|
487
417
|
throw new CliError("configuration_error", `port ${http.port} is in use — pass another with --port`);
|
|
488
418
|
throw error;
|
|
489
419
|
});
|
|
490
|
-
context.renderer.note(`serving on ${listening.url.href} — point your tunnel at it; connectors use ${
|
|
420
|
+
context.renderer.note(`serving on ${listening.url.href} — point your tunnel at it; connectors use ${mcpUrlOf(http.publicUrl).href}`);
|
|
491
421
|
await new Promise((resolve) => {
|
|
492
422
|
process.once("SIGINT", resolve);
|
|
493
423
|
process.once("SIGTERM", resolve);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { PassThrough, type Readable, Writable } from "node:stream";
|
|
2
|
+
/**
|
|
3
|
+
* Stdin and stdout as the transport sees them, noting each request that comes in and each answer that goes out, so
|
|
4
|
+
* the server can tell when everything asked has been answered. The transport's stdin never ends by itself: the server
|
|
5
|
+
* closes it, after the client's stdin ended and the last answer went out.
|
|
6
|
+
*/
|
|
7
|
+
export declare const answeredWire: (source: Readable, sink: Writable) => {
|
|
8
|
+
input: PassThrough;
|
|
9
|
+
output: Writable;
|
|
10
|
+
ended: Promise<void>;
|
|
11
|
+
answered: () => Promise<void>;
|
|
12
|
+
/** Lets go of the real stdin, so it no longer holds the process open. */
|
|
13
|
+
release: () => void;
|
|
14
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { PassThrough, Writable } from "node:stream";
|
|
2
|
+
const lineReader = (onMessage) => {
|
|
3
|
+
let rest = "";
|
|
4
|
+
return (chunk) => {
|
|
5
|
+
const lines = (rest + chunk.toString()).split("\n");
|
|
6
|
+
rest = lines.pop() ?? "";
|
|
7
|
+
for (const line of lines) {
|
|
8
|
+
try {
|
|
9
|
+
const message = JSON.parse(line);
|
|
10
|
+
if (message !== null && typeof message === "object")
|
|
11
|
+
onMessage(message);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Not a message; the transport answers it as a parse error.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Stdin and stdout as the transport sees them, noting each request that comes in and each answer that goes out, so
|
|
21
|
+
* the server can tell when everything asked has been answered. The transport's stdin never ends by itself: the server
|
|
22
|
+
* closes it, after the client's stdin ended and the last answer went out.
|
|
23
|
+
*/
|
|
24
|
+
export const answeredWire = (source, sink) => {
|
|
25
|
+
const asked = new Set();
|
|
26
|
+
let settle;
|
|
27
|
+
const input = new PassThrough();
|
|
28
|
+
const onData = lineReader(({ id, method }) => {
|
|
29
|
+
if (id !== undefined && typeof method === "string")
|
|
30
|
+
asked.add(JSON.stringify(id));
|
|
31
|
+
});
|
|
32
|
+
const output = new Writable({
|
|
33
|
+
write: (chunk, _encoding, done) => {
|
|
34
|
+
noteAnswer(chunk);
|
|
35
|
+
sink.write(chunk, done);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
const noteAnswer = lineReader(({ id, method }) => {
|
|
39
|
+
if (id === undefined || method !== undefined)
|
|
40
|
+
return;
|
|
41
|
+
asked.delete(JSON.stringify(id));
|
|
42
|
+
if (asked.size === 0)
|
|
43
|
+
settle?.();
|
|
44
|
+
});
|
|
45
|
+
const forward = (chunk) => {
|
|
46
|
+
onData(chunk);
|
|
47
|
+
input.write(chunk);
|
|
48
|
+
};
|
|
49
|
+
source.on("data", forward);
|
|
50
|
+
const ended = new Promise((resolve) => {
|
|
51
|
+
source.once("end", resolve);
|
|
52
|
+
source.once("close", resolve);
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
input,
|
|
56
|
+
output,
|
|
57
|
+
ended,
|
|
58
|
+
answered: () => new Promise((resolve) => {
|
|
59
|
+
if (asked.size === 0)
|
|
60
|
+
resolve();
|
|
61
|
+
else
|
|
62
|
+
settle = resolve;
|
|
63
|
+
}),
|
|
64
|
+
/** Lets go of the real stdin, so it no longer holds the process open. */
|
|
65
|
+
release: () => {
|
|
66
|
+
source.off("data", forward);
|
|
67
|
+
if (source.listenerCount("data") === 0)
|
|
68
|
+
source.pause();
|
|
69
|
+
input.end();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
};
|
package/dist/sends/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { type FloodDeadline, FloodMemory, type FloodState, FROZEN_HOLD_MS, flood
|
|
|
3
3
|
export { type Asker, type GuardRequest, guardFor, type SendGuard, type SendGuardOptions, sendGuard, sharedJournal, } from "./guard.js";
|
|
4
4
|
export { currentOperation, guardedWrite, type Operated } from "./guarded.js";
|
|
5
5
|
export { type AccountAction, type ChatAction, type SendEntry, SendJournal, type SendKind, type SendOutcome, sendsPathFor, } from "./journal.js";
|
|
6
|
-
export { CEILING_KEY, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, type Level, layerPermissions, levelFor, type PermissionKey, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
|
|
6
|
+
export { assertOverridesTighten, CEILING_KEY, COMMAND_PATH_KEY, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, type Level, layerPermissions, levelFor, type PermissionKey, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
|
|
7
7
|
export { type Recipient, RecipientList, recipientsPathFor } from "./recipients.js";
|
|
8
8
|
export { newOperationId, newSendId } from "./send-id.js";
|
|
9
9
|
export { MAX_BUFFERED_UPLOAD_BYTES, readUpload, refusedPlace, uploadBlob, uploadBytes, uploadSize, } from "./upload.js";
|
package/dist/sends/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { FloodMemory, FROZEN_HOLD_MS, floodPathFor, LIMITED_HOLD_MS, } from "./f
|
|
|
2
2
|
export { guardFor, sendGuard, sharedJournal, } from "./guard.js";
|
|
3
3
|
export { currentOperation, guardedWrite } from "./guarded.js";
|
|
4
4
|
export { SendJournal, sendsPathFor, } from "./journal.js";
|
|
5
|
-
export { CEILING_KEY, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, layerPermissions, levelFor, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
|
|
5
|
+
export { assertOverridesTighten, CEILING_KEY, COMMAND_PATH_KEY, DEFAULT_PERMISSIONS, keyForCommand, keyForWrite, LEVELS, layerPermissions, levelFor, permissionOverrides, readKeysForCommand, skipFlagFor, WRITE_KEYS, } from "./permissions.js";
|
|
6
6
|
export { RecipientList, recipientsPathFor } from "./recipients.js";
|
|
7
7
|
export { newOperationId, newSendId } from "./send-id.js";
|
|
8
8
|
export { MAX_BUFFERED_UPLOAD_BYTES, readUpload, refusedPlace, uploadBlob, uploadBytes, uploadSize, } from "./upload.js";
|
|
@@ -62,8 +62,16 @@ export declare const readKeysForCommand: (path: readonly string[]) => Permission
|
|
|
62
62
|
* `undefined` for a path this package does not know — a CLI's own command, which its CLI maps.
|
|
63
63
|
*/
|
|
64
64
|
export declare const keyForCommand: (path: readonly string[]) => PermissionKey | null | undefined;
|
|
65
|
+
/** A messenger's key starts with one of its resources, so a misspelled resource is refused by its shape alone. */
|
|
66
|
+
export declare const RESOURCE_KEY: RegExp;
|
|
67
|
+
/**
|
|
68
|
+
* A key of a CLI that declares its own commands (`commandPermissions`): any command path. Which paths exist is
|
|
69
|
+
* checked against the program, not here. Past the first word a key may hold capitals, as Google's method ids do
|
|
70
|
+
* (`api.calendar.calendarList.list`).
|
|
71
|
+
*/
|
|
72
|
+
export declare const COMMAND_PATH_KEY: RegExp;
|
|
65
73
|
/** Startup overrides are a nearer layer than the saved profile, and never write that profile. */
|
|
66
|
-
export declare const permissionOverrides: (entries?: readonly string[]) => Record<PermissionKey, Level>;
|
|
74
|
+
export declare const permissionOverrides: (entries?: readonly string[], shape?: RegExp) => Record<PermissionKey, Level>;
|
|
67
75
|
/** No startup flag grants access: an override may narrow the profile's level, never lift it or a key under it. */
|
|
68
|
-
export declare const assertOverridesTighten: (overrides: Readonly<Record<PermissionKey, Level>>, profile: Readonly<Record<PermissionKey, Level>>) => void;
|
|
76
|
+
export declare const assertOverridesTighten: (overrides: Readonly<Record<PermissionKey, Level>>, profile: Readonly<Record<PermissionKey, Level>>, defaults?: Readonly<Record<PermissionKey, Level>>) => void;
|
|
69
77
|
export declare const assertRetentionEvidenceRead: (permissions: Readonly<Record<string, Level>>) => void;
|
|
@@ -409,25 +409,33 @@ export const keyForCommand = (path) => {
|
|
|
409
409
|
return path.join(".");
|
|
410
410
|
return undefined;
|
|
411
411
|
};
|
|
412
|
+
/** A messenger's key starts with one of its resources, so a misspelled resource is refused by its shape alone. */
|
|
413
|
+
export const RESOURCE_KEY = new RegExp(`^(${RESOURCES.join("|")})(\\.[a-z][a-z-]*)*$`);
|
|
414
|
+
/**
|
|
415
|
+
* A key of a CLI that declares its own commands (`commandPermissions`): any command path. Which paths exist is
|
|
416
|
+
* checked against the program, not here. Past the first word a key may hold capitals, as Google's method ids do
|
|
417
|
+
* (`api.calendar.calendarList.list`).
|
|
418
|
+
*/
|
|
419
|
+
export const COMMAND_PATH_KEY = /^[a-z][a-z-]*(\.[a-zA-Z][a-zA-Z-]*)*$/;
|
|
412
420
|
/** Startup overrides are a nearer layer than the saved profile, and never write that profile. */
|
|
413
|
-
export const permissionOverrides = (entries = []) => {
|
|
421
|
+
export const permissionOverrides = (entries = [], shape = RESOURCE_KEY) => {
|
|
414
422
|
const levels = {};
|
|
415
|
-
const
|
|
423
|
+
const example = shape === RESOURCE_KEY ? "messages.send=allow" : "api=readonly";
|
|
416
424
|
for (const entry of entries) {
|
|
417
425
|
const [key = "", level = "", extra] = entry.split("=");
|
|
418
|
-
if (!
|
|
419
|
-
throw new CliError("validation_error",
|
|
426
|
+
if (!shape.test(key) || !LEVELS.includes(level) || extra !== undefined)
|
|
427
|
+
throw new CliError("validation_error", `--permission takes a command path and level, e.g. ${example} (deny, readonly, ask, allow)`);
|
|
420
428
|
levels[key] = level;
|
|
421
429
|
}
|
|
422
430
|
return levels;
|
|
423
431
|
};
|
|
424
432
|
/** No startup flag grants access: an override may narrow the profile's level, never lift it or a key under it. */
|
|
425
|
-
export const assertOverridesTighten = (overrides, profile) => {
|
|
433
|
+
export const assertOverridesTighten = (overrides, profile, defaults = DEFAULT_PERMISSIONS) => {
|
|
426
434
|
for (const [key, level] of Object.entries(overrides)) {
|
|
427
435
|
const under = Object.keys(profile).filter((named) => named.startsWith(`${key}.`));
|
|
428
|
-
const lifted = [key, ...under].find((named) => ORDER[level] > ORDER[levelFor(profile, named).level]);
|
|
436
|
+
const lifted = [key, ...under].find((named) => ORDER[level] > ORDER[levelFor(profile, named, defaults).level]);
|
|
429
437
|
if (lifted !== undefined)
|
|
430
|
-
throw new CliError("validation_error", `--permission ${key}=${level} would loosen ${lifted} from ${levelFor(profile, lifted).level}; ` +
|
|
438
|
+
throw new CliError("validation_error", `--permission ${key}=${level} would loosen ${lifted} from ${levelFor(profile, lifted, defaults).level}; ` +
|
|
431
439
|
"it only tightens — change the profile's permissions to loosen it", { key, lifted });
|
|
432
440
|
}
|
|
433
441
|
};
|
|
@@ -12,6 +12,7 @@ import { documentOf, noteOf } from "./notes.js";
|
|
|
12
12
|
import { stemmerCache } from "./stems.js";
|
|
13
13
|
import { canonicalTagNamed } from "./tags.js";
|
|
14
14
|
import { read } from "./transaction.js";
|
|
15
|
+
import { searchIndex } from "./vector-index.js";
|
|
15
16
|
import { dot } from "./vectors.js";
|
|
16
17
|
/** The fields a note has. A message-only field (`from:`, `chat:`, …) is refused rather than matching nothing. */
|
|
17
18
|
export const NOTE_FIELDS = [
|
|
@@ -375,29 +376,72 @@ export const noteSearchOver = (context) => {
|
|
|
375
376
|
nearest: async (model, query, { limit, folderIds, withInternal, source }) => {
|
|
376
377
|
drain();
|
|
377
378
|
const scope = live(folderIds, source, withInternal);
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
379
|
+
const columns = `k.id, k.chunkable_type AS type, k.chunkable_id AS owner, k.start_offset AS start,
|
|
380
|
+
k.end_offset AS end, k.content_hash AS hash, v.vector
|
|
381
|
+
FROM chunks k JOIN embeddings v ON v.model = ? AND v.content_hash = k.content_hash`;
|
|
382
|
+
const keep = (best, row) => {
|
|
383
|
+
const score = dot(query, row.vector);
|
|
384
|
+
const key = `${row.type}:${row.owner}`;
|
|
385
|
+
const held = best.get(key);
|
|
386
|
+
if (!held || score > held.score)
|
|
387
|
+
best.set(key, {
|
|
388
|
+
score,
|
|
389
|
+
first: held?.first ?? Number(row.id),
|
|
390
|
+
chunk: Number(row.id),
|
|
391
|
+
start: Number(row.start),
|
|
392
|
+
end: Number(row.end),
|
|
393
|
+
});
|
|
394
|
+
};
|
|
395
|
+
// The scan meets each note first at its lowest chunk, and keeps that order among equal scores.
|
|
396
|
+
const ranked = (best) => [...best.entries()].sort(([, a], [, b]) => b.score - a.score || a.first - b.first).slice(0, limit);
|
|
397
|
+
const indexed = () => {
|
|
398
|
+
const types = source === undefined ? ["document", "note"] : source === "file" ? ["document"] : ["note"];
|
|
399
|
+
const accounts = database.prepare("SELECT DISTINCT coalesce(account_id, 0) AS account FROM chunks WHERE chunkable_type = ?");
|
|
400
|
+
const wanted = folderIds?.length ? new Set(folderIds.map(Number)) : undefined;
|
|
401
|
+
const candidates = database.prepare(`SELECT ${columns} WHERE k.id IN (SELECT value FROM json_each(?))${scope.sql} ORDER BY k.id`);
|
|
402
|
+
const first = database.prepare(`SELECT min(k.id) AS first FROM chunks k JOIN embeddings v ON v.model = ? AND v.content_hash = k.content_hash
|
|
403
|
+
WHERE k.chunkable_type = ? AND k.chunkable_id = ?${scope.sql}`);
|
|
404
|
+
const merged = new Map();
|
|
405
|
+
for (const type of types)
|
|
406
|
+
for (const { account } of accounts.all(type)) {
|
|
407
|
+
if (type === "document" && wanted && !wanted.has(Number(account)))
|
|
408
|
+
continue;
|
|
409
|
+
const found = searchIndex(database, model, { kind: type, account: Number(account) }, query, limit, (near) => {
|
|
410
|
+
const hashes = new Map(near.map(({ id, hash }) => [id, hash]));
|
|
411
|
+
const best = new Map();
|
|
412
|
+
for (const row of candidates.all(model, JSON.stringify([...hashes.keys()]), ...scope.params))
|
|
413
|
+
if (hashes.get(Number(row.id)) === row.hash)
|
|
414
|
+
keep(best, row);
|
|
415
|
+
for (const [key, held] of best) {
|
|
416
|
+
const [kind, owner] = key.split(":");
|
|
417
|
+
held.first = Number(first.get(model, String(kind), Number(owner), ...scope.params)?.first);
|
|
418
|
+
}
|
|
419
|
+
const results = ranked(best);
|
|
420
|
+
return { results, best: results.map(([, { chunk }]) => chunk) };
|
|
421
|
+
});
|
|
422
|
+
if (!found)
|
|
423
|
+
return undefined;
|
|
424
|
+
for (const [key, held] of found)
|
|
425
|
+
merged.set(key, held);
|
|
426
|
+
}
|
|
427
|
+
return ranked(merged);
|
|
428
|
+
};
|
|
429
|
+
const scanned = () => {
|
|
430
|
+
const page = database.prepare(`SELECT ${columns} WHERE k.id > ?${scope.sql} ORDER BY k.id LIMIT ?`);
|
|
431
|
+
const best = new Map();
|
|
432
|
+
let after = 0;
|
|
433
|
+
for (;;) {
|
|
434
|
+
const rows = page.all(model, after, ...scope.params, SCAN_PAGE);
|
|
435
|
+
for (const row of rows)
|
|
436
|
+
keep(best, row);
|
|
437
|
+
const last = rows.at(-1);
|
|
438
|
+
if (!last || rows.length < SCAN_PAGE)
|
|
439
|
+
break;
|
|
440
|
+
after = Number(last.id);
|
|
391
441
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
after = Number(last.id);
|
|
396
|
-
}
|
|
397
|
-
return [...best.entries()]
|
|
398
|
-
.sort(([, a], [, b]) => b.score - a.score)
|
|
399
|
-
.slice(0, limit)
|
|
400
|
-
.flatMap(([key, { score, start, end }]) => {
|
|
442
|
+
return ranked(best);
|
|
443
|
+
};
|
|
444
|
+
return (indexed() ?? scanned()).flatMap(([key, { score, start, end }]) => {
|
|
401
445
|
const [type, id] = key.split(":");
|
|
402
446
|
const note = read(String(type), Number(id));
|
|
403
447
|
return note ? [{ ref: note.ref, note, score, range: { start, end } }] : [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.451.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
"@modelcontextprotocol/server": "^2.4.0",
|
|
177
177
|
"@resvg/resvg-js": "2.6.2",
|
|
178
178
|
"@valibot/to-json-schema": "1.8.0",
|
|
179
|
-
"@wirecat/cli-core-onnx": "1.
|
|
179
|
+
"@wirecat/cli-core-onnx": "1.2.0",
|
|
180
180
|
"@wirecat/cli-core-sqlite": "1.1.0",
|
|
181
181
|
"@xmldom/xmldom": "0.9.12",
|
|
182
182
|
"chardet": "2.2.0",
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
"@types/node": "^22.20.5",
|
|
218
218
|
"@types/pngjs": "6.0.5",
|
|
219
219
|
"@vitest/coverage-v8": "^5.0.5",
|
|
220
|
-
"@wirecat/cli-base": "0.
|
|
220
|
+
"@wirecat/cli-base": "0.63.0",
|
|
221
221
|
"@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
|
|
222
222
|
"commander": "15.0.0",
|
|
223
223
|
"cspell": "10.4.0",
|
|
@@ -237,7 +237,7 @@
|
|
|
237
237
|
},
|
|
238
238
|
"peerDependencies": {
|
|
239
239
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
240
|
-
"@wirecat/cli-base": ">=0.
|
|
240
|
+
"@wirecat/cli-base": ">=0.63.0 <0.64.0",
|
|
241
241
|
"commander": ">=15"
|
|
242
242
|
},
|
|
243
243
|
"bin": {
|