@chance722/dsh-inbox 0.1.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -0
  3. package/README.zh.md +182 -0
  4. package/cordis.patch.yml +11 -0
  5. package/lib/cli.js +259 -0
  6. package/lib/client.js +3823 -0
  7. package/lib/index.js +3718 -0
  8. package/lib/types/cli.d.ts +49 -0
  9. package/lib/types/client/card.d.ts +52 -0
  10. package/lib/types/client/dock.d.ts +52 -0
  11. package/lib/types/client/heading.d.ts +54 -0
  12. package/lib/types/client/index.d.ts +24 -0
  13. package/lib/types/client/manual.d.ts +22 -0
  14. package/lib/types/client/scheme.d.ts +47 -0
  15. package/lib/types/host/capture.d.ts +117 -0
  16. package/lib/types/host/classify/model.d.ts +112 -0
  17. package/lib/types/host/classify/redact.d.ts +18 -0
  18. package/lib/types/host/classify/rules.d.ts +58 -0
  19. package/lib/types/host/command.d.ts +18 -0
  20. package/lib/types/host/crypto/secret-box.d.ts +60 -0
  21. package/lib/types/host/index.d.ts +21 -0
  22. package/lib/types/host/link-title.d.ts +64 -0
  23. package/lib/types/host/remote/auto-push.d.ts +37 -0
  24. package/lib/types/host/remote/merge.d.ts +80 -0
  25. package/lib/types/host/remote/pull.d.ts +60 -0
  26. package/lib/types/host/remote/push.d.ts +101 -0
  27. package/lib/types/host/remote/remove.d.ts +65 -0
  28. package/lib/types/host/remote/writer.d.ts +43 -0
  29. package/lib/types/host/rpc.d.ts +36 -0
  30. package/lib/types/host/s3/client.d.ts +216 -0
  31. package/lib/types/host/s3/probe.d.ts +28 -0
  32. package/lib/types/host/tools.d.ts +56 -0
  33. package/lib/types/host/ui/config.d.ts +54 -0
  34. package/lib/types/host/vault/lease.d.ts +54 -0
  35. package/lib/types/host/vault/query.d.ts +38 -0
  36. package/lib/types/host/vault/spec.d.ts +193 -0
  37. package/lib/types/host/vault/vault.d.ts +269 -0
  38. package/lib/types/host/webdav/client.d.ts +102 -0
  39. package/lib/types/host/webdav/config.d.ts +129 -0
  40. package/lib/types/host/webdav/probe.d.ts +22 -0
  41. package/lib/types/host/webdav/run.d.ts +34 -0
  42. package/lib/types/shared/constants.d.ts +23 -0
  43. package/lib/types/shared/panel-wire.d.ts +474 -0
  44. package/lib/types/shared/vocabulary.d.ts +39 -0
  45. package/package.json +101 -0
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `dsh-inbox init` — the install that is not a paragraph of instructions.
4
+ *
5
+ * Installing this plugin by hand is three steps in three places, and the third
6
+ * is the one everybody forgets: the panel works as soon as the plugin is in the
7
+ * profile, but the *assistant* only sees `inbox_search` / `inbox_get` once the
8
+ * plugin is also in the session's **agent preset**. A README that says "now copy
9
+ * the standard preset into ~/.dsh/.agent-presets and append two lines" is a
10
+ * README that half its readers will skim past — so this does it.
11
+ *
12
+ * What it does, in order, and it is safe to run twice:
13
+ * 0. with `--create-profile`, create a missing profile first (off by default:
14
+ * a typo in a profile name should say so, not conjure a profile)
15
+ * 1. `dsh plugin --profile <profile> add <package>` (pnpm is idempotent)
16
+ * 2. copy the shipped `standard` preset into `<DSH_HOME>/.agent-presets/<id>`
17
+ * and append this plugin's row to its composition
18
+ * 3. point the user-level default preset at it (backing the settings file up
19
+ * first, because that file is the user's, not ours)
20
+ *
21
+ * The copy is the user's to edit afterwards: re-running adds the row when it is
22
+ * missing, and otherwise leaves the file alone — the one exception being that
23
+ * row's package name, which is ours to keep pointing at the real package (see
24
+ * `PRESET_ROW_PATTERN`).
25
+ */
26
+ /** What one pass over a preset composition had to do to our row. */
27
+ export interface PresetRowOutcome {
28
+ /** The text to write back; identical to the input when nothing changed. */
29
+ body: string;
30
+ change: 'added' | 'unchanged' | 'renamed';
31
+ /** The package the row used to point at, when it was renamed. */
32
+ from?: string;
33
+ }
34
+ /**
35
+ * Make sure the composition carries our row — once, pointing at the real package.
36
+ *
37
+ * @param body - the preset's `agent.cordis.yml` text.
38
+ * @returns the text to write, and what had to happen.
39
+ */
40
+ export declare function ensurePresetRow(body: string): PresetRowOutcome;
41
+ export interface Options {
42
+ profile: string;
43
+ preset: string;
44
+ source: string;
45
+ defaultPreset: boolean;
46
+ /** Create the profile when it is missing, instead of refusing. */
47
+ createProfile: boolean;
48
+ }
49
+ export declare function parse(argv: readonly string[]): Options | undefined;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The conversation cards for `inbox_search` and `inbox_get`.
3
+ *
4
+ * Cards are derived from the raw tool-call block (the model-facing result), not
5
+ * from any host-side presentation intent — so what the reader sees is exactly
6
+ * what the model saw, with two local touches: URLs become links, and
7
+ * `[attachment:<id>]` markers become thumbnails fetched from the panel's own
8
+ * attachment route. The second one is deliberate: the picture is rendered here,
9
+ * on this machine, and never became part of the conversation.
10
+ */
11
+ import React from 'react';
12
+ import { type InboxRpcResult } from '../shared/panel-wire.js';
13
+ /**
14
+ * `id: <uuid>` — how the tool results name a record.
15
+ *
16
+ * Exported because it is the whole of the "click a record and go look at it"
17
+ * feature on this side: the button is only as reliable as finding the id in the
18
+ * text the model was shown.
19
+ */
20
+ export declare const RECORD_ID: RegExp;
21
+ /** Every record id in one tool result, in the order they appear. */
22
+ export declare function recordIdsIn(text: string): string[];
23
+ /** The slice of the tool-call block a card needs. */
24
+ interface CardBlock {
25
+ text?: unknown;
26
+ content?: unknown;
27
+ isError?: unknown;
28
+ }
29
+ /** Owner props this card uses; the rest of the share is not needed here. */
30
+ export interface ToolCardProps {
31
+ toolName: string;
32
+ block: CardBlock;
33
+ }
34
+ /**
35
+ * The card both tools share: a titled, monospace-ish rendering of the result
36
+ * with links and thumbnails.
37
+ *
38
+ * @param props - the tool call identity and its settled block.
39
+ * @returns the card element.
40
+ */
41
+ export declare function InboxToolCard({ toolName, block }: ToolCardProps): React.ReactElement;
42
+ /** Register both cards on the keyed tool view slot. */
43
+ export declare function registerToolCards(slots: {
44
+ inject: (name: string, run: () => unknown) => unknown;
45
+ register: (options: {
46
+ name: string;
47
+ key: string;
48
+ }, component: unknown) => unknown;
49
+ }): void;
50
+ /** The panel answers with this envelope; kept here for the shared shape. */
51
+ export type PanelAnswer = InboxRpcResult<unknown>;
52
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The vault's second home: a tab in the right dock, next to the conversation.
3
+ *
4
+ * A tab type is registered in two stages — the type itself into
5
+ * `ctx.sidebarRightTabs`, its body into the keyed `sidebar.right.pane.tab` seat
6
+ * under the same `id` — and opened by kind through `ctx.sidebarRight.openTab`.
7
+ * That is the documented third-party path (`ui-sidebar-documentpreview` is the
8
+ * shipped proof), not a private door.
9
+ *
10
+ * The dock is a *convenience*, not a source of truth: it fetches the newest
11
+ * records itself and shows them; every action still lives in the panel. It also
12
+ * has to survive compositions with no right dock at all (a headless profile has
13
+ * no browser), so `registerInboxDock` does nothing when the services are absent.
14
+ */
15
+ import type { Context } from '@deepseek-ai/cordis';
16
+ /** Type discriminator: what `openTab` names. */
17
+ export declare const DOCK_KIND = "inbox-vault";
18
+ /** Also the tab-type id and the key its body registers under. */
19
+ export declare const DOCK_TAB_ID = "@chance722/dsh-inbox";
20
+ /**
21
+ * A caller that can reveal the tab — optionally focused on one record.
22
+ *
23
+ * It cannot be used from the inbox panel: the dock belongs to the *session*
24
+ * surface, and showing the panel unmounts that surface — calling `openTab` from
25
+ * there fails with `sidebarRight: no session surface is mounted` (measured).
26
+ * Whoever opens it has to be standing in a conversation — which is exactly where
27
+ * a conversation card is, so `card.tsx` is the caller that matters: click a
28
+ * record the assistant mentioned and the dock opens on it.
29
+ *
30
+ * @param id - the record to focus; omitted opens the tab on its usual list.
31
+ */
32
+ export declare let openVaultDock: ((id?: string) => void) | undefined;
33
+ /**
34
+ * Which record the tab was opened on, and how many times it has been navigated.
35
+ *
36
+ * Pulled out of the component so the reading can be tested: the shape is the
37
+ * platform's, not ours, and getting it wrong is silent — the dock falls back to
38
+ * its list and 「打开 ↗」 looks like it does nothing.
39
+ *
40
+ * @param info - whatever the tab-information hook answered, untyped on purpose.
41
+ * @returns the record id when the opener named one, plus the navigation revision.
42
+ */
43
+ export declare function dockFocusOf(info: unknown): {
44
+ id?: string;
45
+ revision: number;
46
+ };
47
+ /**
48
+ * Register the tab type and its body.
49
+ *
50
+ * @param ctx - the browser plugin context.
51
+ */
52
+ export declare function registerInboxDock(ctx: Context): void;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * What a record is *called* on screen, in one place.
3
+ *
4
+ * Both surfaces that name a record — the panel's list and the conversation-side
5
+ * dock — have to obey the same red line (`AGENTS.md` 3: 账密类列表脱敏), and two
6
+ * copies of that rule are two chances to leak: the dock printed a credential's
7
+ * `preview`, which is the first line of the secret itself.
8
+ *
9
+ * It is also the only place that decides what a record with *no* name of its own
10
+ * is called: an uploaded photo used to render as 「(无标题)」 even though the
11
+ * file name had been stored all along (`attachmentName`).
12
+ *
13
+ * Presentation only, and browser-side only: it reads `EntrySummary` and returns
14
+ * strings. Nothing here may import a host module.
15
+ */
16
+ import type { EntrySummary } from '../shared/panel-wire.js';
17
+ /**
18
+ * How much of a credential's description rides along in its heading.
19
+ *
20
+ * The heading is ellipsised by CSS on top of this; the cap is what keeps a
21
+ * 300-character note out of the title slot at all, so the line still reads as a
22
+ * name rather than as a paragraph.
23
+ */
24
+ export declare const NOTE_IN_HEADING_CHARS = 24;
25
+ /** True when this record's own text must never be printed outside the detail. */
26
+ export declare function isSecret(entry: EntrySummary): boolean;
27
+ /**
28
+ * The one-line heading a card or a dock row shows.
29
+ *
30
+ * The row is the **name**, and only the name: the category glyph beside it
31
+ * already says what kind of thing this is, so a 「密钥 / 账密(…)」 prefix only ate
32
+ * the width a name needs. What the name came from, in order: the user's own
33
+ * word, then the record's own (address, text, file name), then the note.
34
+ *
35
+ * The same shape covers a picture or a file, which has no text to be named by:
36
+ * its file name takes the name slot. A name the user typed into the detail pane
37
+ * outranks everything.
38
+ *
39
+ * @param entry - the record to name.
40
+ * @returns the heading, clamped to one short line.
41
+ */
42
+ export declare function headingOf(entry: EntrySummary): string;
43
+ /**
44
+ * The same heading, plus the note the row no longer shows, for the hover
45
+ * tooltip.
46
+ *
47
+ * The card only ever shows one ellipsised line, so the full text has to be
48
+ * reachable somewhere short of opening the detail pane — and since the note
49
+ * left the row itself, the tooltip is where it lives now.
50
+ *
51
+ * @param entry - the record to name.
52
+ * @returns the heading, with the whole note in the parentheses.
53
+ */
54
+ export declare function headingTooltipOf(entry: EntrySummary): string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The browser half: the sidebar entry, the capture box, the filter bar, the
3
+ * list, and the detail pane.
4
+ *
5
+ * Everything the vault knows lives on the host side; this half only renders and
6
+ * calls the Fetch routes declared in `src/shared/panel-wire.ts`.
7
+ */
8
+ import type { Context } from '@deepseek-ai/cordis';
9
+ /** Stable Cordis plugin name for the browser half. */
10
+ export declare const name = "dsh-inbox-client";
11
+ /**
12
+ * `slots` is a hard dependency: without it Cordis runs `apply` before the slot
13
+ * registry exists and every registration is silently skipped (M0's trap).
14
+ */
15
+ export declare const inject: string[];
16
+ /**
17
+ * Register the sidebar switch and the panel it selects.
18
+ *
19
+ * Both slots belong to other packages, so each registration waits for the
20
+ * declaration through `slots.inject` instead of assuming an order.
21
+ *
22
+ * @param ctx - browser plugin context carrying the slot registry.
23
+ */
24
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The three-minute manual.
3
+ *
4
+ * The panel has grown features faster than it has grown explanations — sync,
5
+ * encryption, naming, the conversation tools — and a user who has to ask "what
6
+ * does this button do" in chat has already lost. This is the answer that lives
7
+ * in the product: one screen, scenarios in the order a person meets them, three
8
+ * lines each at most.
9
+ *
10
+ * Deliberately not a reference manual: the help docs are that. This is the part
11
+ * you read once.
12
+ */
13
+ import React from 'react';
14
+ /**
15
+ * The manual, as a dialog.
16
+ *
17
+ * @param props - how to close it.
18
+ * @returns the dialog.
19
+ */
20
+ export declare function ManualDialog({ onClose }: {
21
+ onClose: () => void;
22
+ }): React.ReactElement;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Which colour scheme the app around this panel is drawn in.
3
+ *
4
+ * The panel lives inside someone else's UI and may not assume either theme. It
5
+ * used to declare `color-scheme: dark` outright — which was a fix for one bug
6
+ * (a native `<select>` drawn light while the app was dark, so its options were
7
+ * white on white) and the cause of three worse ones: in a *light* app, `Canvas`
8
+ * resolves to near-black, so a dialog whose text colour is inherited from the
9
+ * app became dark-on-dark — the settings sheet that "showed nothing".
10
+ *
11
+ * Two ways to find out, in this order:
12
+ *
13
+ * 1. **Ask the app.** `color-scheme` is a real CSS property, it inherits, and
14
+ * dsh declares it on `document.documentElement` (`<html>`), keeping its token
15
+ * variables on `body` — measured 2026-09-20 against the running web app, see
16
+ * `docs/help/panel-theme.md`. That declaration is the app's own answer, so it
17
+ * beats any guess we could make.
18
+ * 2. **Read the light.** An app may declare nothing, or declare
19
+ * `color-scheme: light dark` to mean "whatever the OS says"; neither says
20
+ * which one is on now. The colour the app hands down to our container does: a
21
+ * dark app gives light text.
22
+ *
23
+ * We deliberately do *not* read our own panel root's `color-scheme`: we set that
24
+ * property ourselves, so it would only echo our own last guess back at us.
25
+ */
26
+ /**
27
+ * The scheme a text colour implies.
28
+ *
29
+ * @param cssColor - a computed colour, e.g. `rgb(230, 232, 234)`.
30
+ * @returns `dark` for light text (a dark app), `light` for dark text.
31
+ */
32
+ export declare function schemeOfColor(cssColor: string): 'light' | 'dark';
33
+ /**
34
+ * Decide from both signals, the app's own declaration winning.
35
+ *
36
+ * @param declared - the computed `color-scheme` of the document element.
37
+ * @param inheritedTextColor - the computed text colour our container inherits.
38
+ * @returns the scheme the panel should declare.
39
+ */
40
+ export declare function schemeFrom(declared: string, inheritedTextColor: string): 'light' | 'dark';
41
+ /**
42
+ * Read the scheme off the document element, falling back to inherited text.
43
+ *
44
+ * @param element - the panel's own root, whose inherited text colour is read.
45
+ * @returns the scheme to declare, so native controls match the app.
46
+ */
47
+ export declare function schemeOf(element: Element | null): 'light' | 'dark';
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Capture rules: what a pasted thing is, how to spot a repeat, and how to file
3
+ * it. Everything here is deterministic; classification proper (which category a
4
+ * link or an image belongs to) is M5's job.
5
+ */
6
+ import type { Context } from '@deepseek-ai/cordis';
7
+ import type { Source } from '../shared/vocabulary.js';
8
+ import { platformOf, type Classification } from './classify/rules.js';
9
+ import type { Attachment, Item } from './vault/spec.js';
10
+ import type { Vault } from './vault/vault.js';
11
+ /**
12
+ * Decide what a pasted string is.
13
+ *
14
+ * A single bare `http(s)` URL is a link; anything else is text. A sentence that
15
+ * merely contains a URL stays text: the vault must not reinterpret prose.
16
+ *
17
+ * @param raw - exactly what was pasted.
18
+ * @returns the kind, plus url and (when recognised) platform for links.
19
+ */
20
+ export declare function sniff(raw: string): {
21
+ kind: 'link' | 'text';
22
+ url?: string;
23
+ platform?: string;
24
+ };
25
+ /**
26
+ * Normalise a link for repeat detection: drop the fragment and the parameters
27
+ * that only record where a share came from, and unify the host case.
28
+ *
29
+ * Deliberately conservative — real identity lives in paths (`/s/<id>`,
30
+ * `/video/BV…`) and in the parameters we keep.
31
+ */
32
+ export declare function normalizeLink(raw: string): string;
33
+ /** The outcome of a capture: what was stored, and whether it merged. */
34
+ export interface CaptureOutcome {
35
+ item: Item;
36
+ /** True when an existing record absorbed this capture. */
37
+ merged: boolean;
38
+ /**
39
+ * True when the record it absorbed into was sitting in the recycle bin and
40
+ * has just been taken back out (see `absorb`).
41
+ */
42
+ restored: boolean;
43
+ /** What the rules concluded, so a caller can decide whether to ask a model. */
44
+ verdict: Classification;
45
+ }
46
+ /**
47
+ * File a pasted string.
48
+ *
49
+ * Repeats merge into the existing record: the vault answers "what have I
50
+ * stored", not "how many times did I paste".
51
+ *
52
+ * @param vault - the open vault.
53
+ * @param raw - the pasted string.
54
+ * @param source - which entry point produced it.
55
+ * @param note - optional description the user supplied.
56
+ * @returns the stored (or merged) record, and whether it merged.
57
+ */
58
+ export declare function captureText(vault: Vault, raw: string, source: Source, note?: string): Promise<CaptureOutcome>;
59
+ /** Re-exported so callers do not reach into the rules module for this. */
60
+ export { platformOf };
61
+ /**
62
+ * Attachment reference as it arrives from the composer's durable blocks.
63
+ * `id` is the *store's* id, not ours.
64
+ */
65
+ export type CapturedAttachment = Pick<Attachment, 'mime' | 'bytes'> & {
66
+ id: string;
67
+ } & Partial<Pick<Attachment, 'filename' | 'width' | 'height' | 'sha256'>>;
68
+ /**
69
+ * File one durable attachment the composer handed over.
70
+ *
71
+ * The bytes stay in dsh's own attachment store (content-addressed, never
72
+ * auto-deleted); the vault keeps the reference plus what we can show without
73
+ * reading the bytes back.
74
+ *
75
+ * @param vault - the open vault.
76
+ * @param attachment - the block's attachment reference.
77
+ * @param source - which entry point produced it.
78
+ * @param note - optional description the user supplied.
79
+ * @returns the stored (or merged) record, and whether it merged.
80
+ */
81
+ export declare function captureImage(vault: Vault, attachment: CapturedAttachment, source: Source, note?: string): Promise<CaptureOutcome>;
82
+ /** What one capture submission carried: free text and/or durable attachments. */
83
+ export interface CapturePayload {
84
+ text?: string;
85
+ attachments?: readonly CapturedAttachment[];
86
+ }
87
+ /** Roll-up of one capture submission. */
88
+ export interface CaptureSummary {
89
+ stored: number;
90
+ /** Records that absorbed this capture. */
91
+ merged: number;
92
+ /**
93
+ * Of those, the ones that were in the recycle bin and came back out.
94
+ *
95
+ * Counted separately because it is the difference between "you already had
96
+ * this" and "you had deleted this, so I put it back" — the panel says the
97
+ * second one in its own words.
98
+ */
99
+ restored: number;
100
+ }
101
+ /** What a caller may want to do once something is safely stored. */
102
+ export interface CaptureOptions {
103
+ /** When present, records no rule could judge are handed to the model. */
104
+ ctx?: Context;
105
+ }
106
+ /**
107
+ * File one submission from any entry point.
108
+ *
109
+ * Order is attachments first, then text, so a note that arrived with the
110
+ * submission can still be attached to the item it belongs to.
111
+ *
112
+ * @param vault - the open vault.
113
+ * @param payload - the submitted text and attachments.
114
+ * @param source - which entry point produced them.
115
+ * @returns how many records were stored and how many merged.
116
+ */
117
+ export declare function capture(vault: Vault, payload: CapturePayload, source: Source, options?: CaptureOptions): Promise<CaptureSummary>;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The model fallback: one small call, only when a rule could not decide, always
3
+ * redacted, always capped.
4
+ *
5
+ * Three things this file refuses to do:
6
+ * - it never sends an image (the only way to judge an ID document from a
7
+ * picture is the picture, and that is exactly what must not leave);
8
+ * - it never sends credential text (`redact()` runs first, and a record whose
9
+ * rule verdict is already `secret` is never asked about at all);
10
+ * - it never overrides the user, or a rule that already decided.
11
+ *
12
+ * Spending is capped per local day and persisted in the vault's global slot, so
13
+ * a restart cannot reset the meter.
14
+ */
15
+ import type { Context } from '@deepseek-ai/cordis';
16
+ import { type Category } from '../../shared/vocabulary.js';
17
+ import type { Item } from '../vault/spec.js';
18
+ import type { Vault } from '../vault/vault.js';
19
+ import type { Classification } from './rules.js';
20
+ /** Provider route registered by the shipped DeepSeek adapter. */
21
+ export declare const PROVIDER = "deepseek-official";
22
+ /** The cheap model this vault classifies with. */
23
+ export declare const MODEL = "deepseek-flash";
24
+ /** How much the fallback may spend, and how fast. */
25
+ export interface ModelBudget {
26
+ /** Calls per local day. */
27
+ maxCallsPerDay: number;
28
+ /** Prompt + completion tokens per local day. */
29
+ maxTokensPerDay: number;
30
+ /**
31
+ * Output ceiling for one call. A category word needs almost nothing, but this
32
+ * model spends tokens thinking first — 64 was enough to get cut off before it
33
+ * ever answered, so the ceiling has to fit the reasoning too.
34
+ */
35
+ maxTokensPerCall: number;
36
+ }
37
+ /**
38
+ * The thresholds, chosen so a bad day costs pocket change and a runaway loop
39
+ * cannot happen: 200 calls is far more than a person pastes in a day, and
40
+ * 100k tokens is a few 分 at flash prices.
41
+ */
42
+ export declare const DEFAULT_BUDGET: ModelBudget;
43
+ /** Today's spend, as persisted in the vault's global slot. */
44
+ export interface ModelSpend {
45
+ /** `YYYY-MM-DD` in local time. */
46
+ day: string;
47
+ calls: number;
48
+ tokens: number;
49
+ }
50
+ /** Local calendar day, because the cap is a human's day, not a UTC one. */
51
+ export declare function today(now?: Date): string;
52
+ /** Fresh spend record for a new day. */
53
+ export declare function freshSpend(now?: Date): ModelSpend;
54
+ /** Roll the record over when the day changed. */
55
+ export declare function rollSpend(spend: ModelSpend | undefined, now?: Date): ModelSpend;
56
+ /**
57
+ * Whether one more call fits the budget.
58
+ *
59
+ * @param spend - the rolled-over spend record.
60
+ * @param budget - the configured caps.
61
+ * @returns true when a call may be made.
62
+ */
63
+ export declare function withinBudget(spend: ModelSpend, budget?: ModelBudget): boolean;
64
+ /** Record one finished call. */
65
+ export declare function spendOf(spend: ModelSpend, tokens: number): ModelSpend;
66
+ /**
67
+ * Whether a verdict is worth a model call.
68
+ *
69
+ * Only text and links are ever asked about, and only when no rule decided and
70
+ * there is enough content to be about something.
71
+ *
72
+ * @param verdict - what the rules concluded.
73
+ * @param item - the stored record.
74
+ * @param minimumChars - how short a paste is too trivial to classify. Eight,
75
+ * because a Chinese note carries a sentence in that many characters ("下周三
76
+ * 之前把发票报销掉" is eleven) while "收到" is plainly not worth a call.
77
+ * @returns true when the fallback should run.
78
+ */
79
+ export declare function shouldAskModel(verdict: Classification, item: Item, minimumChars?: number): boolean;
80
+ /**
81
+ * Read one category out of whatever the model replied.
82
+ *
83
+ * @param answer - the model's text.
84
+ * @returns the category, or undefined when the reply names none of them.
85
+ */
86
+ export declare function parseCategory(answer: string): Category | undefined;
87
+ /** What one fallback attempt produced, for the caller to record or ignore. */
88
+ export type FallbackOutcome = {
89
+ kind: 'applied';
90
+ category: Category;
91
+ tokens: number;
92
+ } | {
93
+ kind: 'skipped';
94
+ reason: string;
95
+ } | {
96
+ kind: 'failed';
97
+ reason: string;
98
+ };
99
+ /**
100
+ * Ask the model about one record and patch it when the answer is usable.
101
+ *
102
+ * Never throws: the caller scheduled this as a background pass, and a failed
103
+ * classification must leave the rule's verdict exactly as it was.
104
+ *
105
+ * @param ctx - host context carrying the model runtime.
106
+ * @param vault - the open vault.
107
+ * @param item - the record to classify.
108
+ * @param verdict - the rule verdict that said `unsure`.
109
+ * @param budget - caps for the day.
110
+ * @returns what happened, for logging and tests.
111
+ */
112
+ export declare function classifyWithModel(ctx: Context, vault: Vault, item: Item, verdict: Classification, budget?: ModelBudget): Promise<FallbackOutcome>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Masking for anything that would otherwise leave the machine.
3
+ *
4
+ * The vault's rule is that credential text never reaches a model. Classification
5
+ * only needs the *shape* of a message, so the shape is what we keep: labels stay
6
+ * readable, values become a placeholder of the same rough length.
7
+ *
8
+ * Pure and byte-free — no key store, no provider, no side effects.
9
+ */
10
+ /**
11
+ * Replace credential values with a fixed-width placeholder.
12
+ *
13
+ * @param text - the text that might leave the machine.
14
+ * @returns the same text with values masked; labels survive.
15
+ */
16
+ export declare function redact(text: string): string;
17
+ /** Whether a redacted string still carries a recognisable credential shape. */
18
+ export declare function looksRedacted(text: string): boolean;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The deterministic half of classification.
3
+ *
4
+ * Rules decide what they can prove — a platform's media type, a credential by
5
+ * shape, an ID document by aspect ratio — and say `unsure` about the rest
6
+ * instead of guessing. That word is the seam a model pass can use later without
7
+ * ever overriding something the *user* said.
8
+ *
9
+ * Nothing here reads bytes or calls anything: every input is text or numbers the
10
+ * vault already holds, so the whole layer is a pure function and cheap to run at
11
+ * paste time.
12
+ */
13
+ import type { Category } from '../../shared/vocabulary.js';
14
+ /** One rule's verdict. */
15
+ export interface Classification {
16
+ category: Category;
17
+ /** Set when the rule recognised the host and can name it. */
18
+ platform?: string;
19
+ /** `unsure` means "a model could do better", never "this is wrong". */
20
+ confidence: 'decided' | 'unsure';
21
+ /** Which rule fired, in Chinese, for the record's tooltip and for debugging. */
22
+ reason: string;
23
+ /** Extra tags a rule wants to attach, e.g. a suspected ID document. */
24
+ tags?: readonly string[];
25
+ }
26
+ /** True when the text carries something that must never be echoed. */
27
+ export declare function findSecret(text: string): string | undefined;
28
+ /** The platform a URL belongs to, or undefined when it is just "somewhere". */
29
+ export declare function platformOf(url: string): string | undefined;
30
+ /**
31
+ * Classify a link by its host and path.
32
+ *
33
+ * @param url - the pasted URL.
34
+ * @returns the verdict; `unsure` for a host no rule knows.
35
+ */
36
+ export declare function classifyLink(url: string): Classification;
37
+ /**
38
+ * Classify a pasted string.
39
+ *
40
+ * @param text - exactly what was pasted.
41
+ * @returns a secret verdict, or `unsure` — plain text rarely proves its own kind.
42
+ */
43
+ export declare function classifyText(text: string): Classification;
44
+ /**
45
+ * Classify an image from the dimensions the store already recorded.
46
+ *
47
+ * A ratio is evidence, not proof, so a suspected ID document stays `image` and
48
+ * gains a tag: only the user can promote it to the `document` category, which is
49
+ * exactly what the product decision asks for. No bytes are read, and nothing is
50
+ * uploaded to decide this.
51
+ *
52
+ * @param dimensions - stored width/height, when known.
53
+ * @returns the verdict.
54
+ */
55
+ export declare function classifyImage(dimensions: {
56
+ width?: number;
57
+ height?: number;
58
+ }): Classification;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The `/inbox` command: file what the user just typed or attached, without
3
+ * turning it into a model message.
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ import type { Vault } from './vault/vault.js';
7
+ /**
8
+ * Register `/inbox` against the interactive command surface.
9
+ *
10
+ * `recordInput: false` is load-bearing, not tidiness: by default a command's
11
+ * raw input is written to the session log, and the vault is exactly where
12
+ * credentials are supposed to go — the payload must live in the vault and
13
+ * nowhere else (see the credentials rule in AGENTS.md).
14
+ *
15
+ * @param ctx - host context; the commands service must be mounted.
16
+ * @param vault - reads the currently open vault, which may not be open yet.
17
+ */
18
+ export declare function registerInboxCommand(ctx: Context, vault: () => Vault | undefined): void;