@getfamiliar/shared 0.1.4 → 0.1.6
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/build/EventBus.d.ts +2 -2
- package/build/EventBus.js +2 -2
- package/build/Mail.d.ts +18 -2
- package/build/Plugin.d.ts +23 -11
- package/build/Storage.d.ts +263 -0
- package/build/Storage.js +33 -0
- package/build/ToolRunner.d.ts +13 -2
- package/build/ToolRunner.js +28 -12
- package/build/index.d.ts +3 -1
- package/build/index.js +2 -0
- package/build/markdownOutput.d.ts +39 -0
- package/build/markdownOutput.js +50 -0
- package/build/markdownTerminal.js +7 -2
- package/build/testing/FakeStorageProvider.d.ts +133 -0
- package/build/testing/FakeStorageProvider.js +459 -0
- package/build/testing/StorageProviderContract.d.ts +32 -0
- package/build/testing/StorageProviderContract.js +168 -0
- package/build/testing/index.d.ts +2 -0
- package/build/testing/index.js +2 -0
- package/package.json +1 -1
package/build/EventBus.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export declare class EventBus {
|
|
|
108
108
|
listSince(since: Date): Promise<EventRow[]>;
|
|
109
109
|
/**
|
|
110
110
|
* Fetch the most recently created N event rows, newest first. Used by
|
|
111
|
-
*
|
|
111
|
+
* `familiar events list` to surface recent bus activity for operators
|
|
112
112
|
* looking for an event id to inspect with `events report <id>`.
|
|
113
113
|
*
|
|
114
114
|
* @param limit Maximum number of rows to return. Must be a positive integer.
|
|
@@ -118,7 +118,7 @@ export declare class EventBus {
|
|
|
118
118
|
/**
|
|
119
119
|
* Fetch the most recently created N event rows whose topic, start_handler,
|
|
120
120
|
* prompt, or payload (cast to text) case-insensitively contains `needle`.
|
|
121
|
-
* Used by
|
|
121
|
+
* Used by `familiar events list <search>` so the operator can grep the
|
|
122
122
|
* recent bus without piping through `grep`.
|
|
123
123
|
*
|
|
124
124
|
* Substring match via `ILIKE '%needle%'`; the needle is escaped for the
|
package/build/EventBus.js
CHANGED
|
@@ -229,7 +229,7 @@ export class EventBus {
|
|
|
229
229
|
}
|
|
230
230
|
/**
|
|
231
231
|
* Fetch the most recently created N event rows, newest first. Used by
|
|
232
|
-
*
|
|
232
|
+
* `familiar events list` to surface recent bus activity for operators
|
|
233
233
|
* looking for an event id to inspect with `events report <id>`.
|
|
234
234
|
*
|
|
235
235
|
* @param limit Maximum number of rows to return. Must be a positive integer.
|
|
@@ -244,7 +244,7 @@ export class EventBus {
|
|
|
244
244
|
/**
|
|
245
245
|
* Fetch the most recently created N event rows whose topic, start_handler,
|
|
246
246
|
* prompt, or payload (cast to text) case-insensitively contains `needle`.
|
|
247
|
-
* Used by
|
|
247
|
+
* Used by `familiar events list <search>` so the operator can grep the
|
|
248
248
|
* recent bus without piping through `grep`.
|
|
249
249
|
*
|
|
250
250
|
* Substring match via `ILIKE '%needle%'`; the needle is escaped for the
|
package/build/Mail.d.ts
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* (e.g. ms365 → `inbox`, `archive`, `deleteditems`; gmail eventually →
|
|
21
21
|
* `INBOX`, `[Gmail]/All Mail`, `[Gmail]/Trash`).
|
|
22
22
|
*/
|
|
23
|
+
import type { NewEvent } from "./Event.js";
|
|
24
|
+
import type { EmitHandle } from "./Plugin.js";
|
|
23
25
|
export type MailFolder = "inbox" | "archive" | "trash" | "sent";
|
|
24
26
|
/**
|
|
25
27
|
* One downloaded attachment. The core tool stages these into the
|
|
@@ -211,8 +213,8 @@ export interface MailProvider {
|
|
|
211
213
|
/**
|
|
212
214
|
* Capabilities the host exposes to plugins for mail data. Reached via
|
|
213
215
|
* `ctx.mail` on a plugin's `HostContext`. Tiny by design: there is no
|
|
214
|
-
* mail cache in core, so
|
|
215
|
-
*
|
|
216
|
+
* mail cache in core, so plugins only register their provider during
|
|
217
|
+
* `start()` and hand new-mail events to the host for emission.
|
|
216
218
|
*/
|
|
217
219
|
export interface MailApi {
|
|
218
220
|
/**
|
|
@@ -221,6 +223,20 @@ export interface MailApi {
|
|
|
221
223
|
* wiring bug, not a feature.
|
|
222
224
|
*/
|
|
223
225
|
registerProvider(provider: MailProvider): void;
|
|
226
|
+
/**
|
|
227
|
+
* Emit a new-mail bus event. Mail pollers must use this instead of
|
|
228
|
+
* `ctx.events.emit` so the host can apply cross-provider policy:
|
|
229
|
+
* on dev instances the event is dropped unless
|
|
230
|
+
* `mail.emitEventsInDev` is set, so a fresh dev database doesn't
|
|
231
|
+
* replay the whole inbox. Otherwise behaves exactly like
|
|
232
|
+
* `ctx.events.emit` (idempotency, `files` staging).
|
|
233
|
+
*
|
|
234
|
+
* @param event - Event to emit; `topic` must be `mail` or start with `mail:`.
|
|
235
|
+
* @returns The emit handle, or `null` when the host suppressed the event.
|
|
236
|
+
* @throws If `topic` is not a mail topic, or whatever `ctx.events.emit`
|
|
237
|
+
* throws (e.g. `DuplicateIdempotencyKeyError`).
|
|
238
|
+
*/
|
|
239
|
+
emitMailEvent(event: NewEvent): Promise<EmitHandle | null>;
|
|
224
240
|
}
|
|
225
241
|
/**
|
|
226
242
|
* Build a self-routing mail id from the three segments. Format:
|
package/build/Plugin.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { MailApi } from "./Mail.js";
|
|
|
11
11
|
import type { MailStyleTemplate } from "./MailStyleTemplate.js";
|
|
12
12
|
import type { ModelMetaData, ModelProviderDescriptor } from "./ModelMetaData.js";
|
|
13
13
|
import type { StepResultRow } from "./StepResult.js";
|
|
14
|
+
import type { StorageApi } from "./Storage.js";
|
|
14
15
|
import type { ToolLevel } from "./ToolLevel.js";
|
|
15
16
|
import type { ToolRunContext } from "./ToolRunner.js";
|
|
16
17
|
import type { WorkspaceWatcherApi } from "./WorkspaceFile.js";
|
|
@@ -266,7 +267,7 @@ export interface HostContext {
|
|
|
266
267
|
*/
|
|
267
268
|
readonly tmpDir: string;
|
|
268
269
|
/**
|
|
269
|
-
* Cheap synchronous probe for whether the host daemon (
|
|
270
|
+
* Cheap synchronous probe for whether the host daemon (`familiar
|
|
270
271
|
* start`) is currently running on this machine. Inspects the
|
|
271
272
|
* daemon's pidfile under `<tmpDir>/.daemon.pid` and confirms the
|
|
272
273
|
* recorded pid is still alive — no network calls, no MCP traffic.
|
|
@@ -349,10 +350,21 @@ export interface HostContext {
|
|
|
349
350
|
*
|
|
350
351
|
* Unlike `calendar`, there is no read-side surface here — the
|
|
351
352
|
* core does not cache mail bodies or metadata. Pollers emit
|
|
352
|
-
* `mail:<plugin>` events with a prefixed `mail_id` in the payload
|
|
353
|
-
*
|
|
353
|
+
* `mail:<plugin>` events with a prefixed `mail_id` in the payload
|
|
354
|
+
* through `ctx.mail.emitMailEvent` (not `ctx.events.emit`, so the
|
|
355
|
+
* dev-instance gate applies); the agent reaches the body via
|
|
356
|
+
* `mail_fetch_body` on demand.
|
|
354
357
|
*/
|
|
355
358
|
readonly mail: MailApi;
|
|
359
|
+
/**
|
|
360
|
+
* Shared cloud-storage layer. Plugins that act as storage providers
|
|
361
|
+
* register a {@link StorageProvider} during **`prepare()`** (not
|
|
362
|
+
* `start()`) via `ctx.storage.registerProvider(provider)`, so the
|
|
363
|
+
* `familiar storage` CLI can reach them without the daemon. The core
|
|
364
|
+
* `storage_*` agent tools route through the host `StorageService`,
|
|
365
|
+
* which maps the user's configured mounts onto providers.
|
|
366
|
+
*/
|
|
367
|
+
readonly storage: StorageApi;
|
|
356
368
|
/**
|
|
357
369
|
* Read the per-mailbox style template the user (or the
|
|
358
370
|
* extract-style handler) wrote at `data/mail/templates/<mailbox>/<name>.json`.
|
|
@@ -376,7 +388,7 @@ export interface HostContext {
|
|
|
376
388
|
* {@link WorkspaceWatcherApi} for the full contract.
|
|
377
389
|
*
|
|
378
390
|
* Only available inside the daemon: one-shot CLI invocations
|
|
379
|
-
* (
|
|
391
|
+
* (`familiar <plugin> …`) do not spin up the watcher, so calling
|
|
380
392
|
* either method from a CLI command throws synchronously. Daemon
|
|
381
393
|
* plugins are the intended consumer.
|
|
382
394
|
*/
|
|
@@ -403,11 +415,11 @@ export interface HostContext {
|
|
|
403
415
|
};
|
|
404
416
|
/**
|
|
405
417
|
* Aborts when the host daemon is no longer running — i.e. when the
|
|
406
|
-
*
|
|
418
|
+
* `familiar start` process backing this context has stopped or
|
|
407
419
|
* crashed. Always present; consumers don't need to null-check.
|
|
408
420
|
*
|
|
409
|
-
* Intended use: one-shot CLI commands (
|
|
410
|
-
*
|
|
421
|
+
* Intended use: one-shot CLI commands (`familiar <plugin> ...`,
|
|
422
|
+
* `familiar cli-chat`, etc.) race long waits (`emit().settled`,
|
|
411
423
|
* polling loops, interactive prompts) against this signal and exit
|
|
412
424
|
* cleanly when it fires instead of hanging on a postgres connection
|
|
413
425
|
* the dying daemon took with it.
|
|
@@ -644,20 +656,20 @@ export interface PluginHostManifest {
|
|
|
644
656
|
getModelProviders?(ctx: HostContext): readonly ModelProviderDescriptor[];
|
|
645
657
|
/**
|
|
646
658
|
* Default command for the plugin's CLI root. When set, invoking
|
|
647
|
-
* the plugin id with no subcommand (e.g. `
|
|
659
|
+
* the plugin id with no subcommand (e.g. `familiar cli-chat`) runs
|
|
648
660
|
* this command's `run`. Its `args` are also lifted onto the root.
|
|
649
661
|
* Useful for plugins whose primary mode is interactive (REPL-like)
|
|
650
662
|
* — the user shouldn't have to remember a subcommand name.
|
|
651
663
|
*
|
|
652
664
|
* Subcommands declared in {@link commands} are still available
|
|
653
|
-
* under the same root, so `
|
|
665
|
+
* under the same root, so `familiar cli-chat send "hi"` keeps
|
|
654
666
|
* working alongside the bare-root invocation.
|
|
655
667
|
*/
|
|
656
668
|
main?(ctx: HostContext): AnyCommandDef;
|
|
657
669
|
/**
|
|
658
670
|
* Citty commands contributed by this plugin. They are mounted
|
|
659
671
|
* under `subCommands[<plugin.id>]` in the root CLI by default
|
|
660
|
-
* (e.g. `
|
|
672
|
+
* (e.g. `familiar cli-chat send "hi"`).
|
|
661
673
|
*/
|
|
662
674
|
commands?(ctx: HostContext): readonly AnyCommandDef[];
|
|
663
675
|
}
|
|
@@ -673,7 +685,7 @@ export interface PluginHostManifest {
|
|
|
673
685
|
export interface PluginManifest {
|
|
674
686
|
/**
|
|
675
687
|
* Plugin id, matching `[a-z0-9-]+`. Used to namespace the
|
|
676
|
-
* plugin's CLI commands (`
|
|
688
|
+
* plugin's CLI commands (`familiar <id> <subcommand>`). A plugin
|
|
677
689
|
* that contributes {@link PluginHostManifest.tools | tools} must
|
|
678
690
|
* additionally satisfy {@link IDENT_PATTERN} so the id is usable
|
|
679
691
|
* as a tool group name; the tools registry enforces that at
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud-storage provider SPI. Lives in `shared/` because the host (which
|
|
3
|
+
* owns the `StorageService`, the `storage_*` agent tools and the
|
|
4
|
+
* `familiar storage` CLI) and the provider plugins (ms365, later gdrive,
|
|
5
|
+
* dropbox, …) trade in the same shapes.
|
|
6
|
+
*
|
|
7
|
+
* Division of labour:
|
|
8
|
+
*
|
|
9
|
+
* - **Providers** speak their API: they address items by their native
|
|
10
|
+
* id (`realId`), stream bytes between the provider and a host-side
|
|
11
|
+
* file, and map API failures onto {@link StorageError} codes. They
|
|
12
|
+
* never see mounts, refs, policy, quotas or the agent.
|
|
13
|
+
* - **The host `StorageService`** owns everything that must behave the
|
|
14
|
+
* same for every provider: mount aliases, ref parsing, path
|
|
15
|
+
* resolution for id-addressed providers, write policy, search
|
|
16
|
+
* fan-out, cross-mount copies and the per-run byte quota.
|
|
17
|
+
*
|
|
18
|
+
* Credentials never leave the provider: the agent only ever sees mount
|
|
19
|
+
* aliases, refs, metadata and files placed in its scratch directory.
|
|
20
|
+
* Hard delete is deliberately absent from the SPI.
|
|
21
|
+
*/
|
|
22
|
+
/** Kind of a storage item. `native` = provider document without bytes (Google Docs, …). */
|
|
23
|
+
export type StorageItemKind = "file" | "folder" | "native";
|
|
24
|
+
/** Hash algorithms a provider may report for file content. */
|
|
25
|
+
export type StorageHashAlgo = "sha1" | "sha256" | "md5" | "quickXor" | "dropbox";
|
|
26
|
+
/** Provider-reported content hash. */
|
|
27
|
+
export interface StorageContentHash {
|
|
28
|
+
readonly algo: StorageHashAlgo;
|
|
29
|
+
readonly value: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Normalized item metadata. `realId` / `parentRealId` are provider-native
|
|
33
|
+
* and only ever reach the agent wrapped inside an id ref (`mount#<id>`).
|
|
34
|
+
*/
|
|
35
|
+
export interface StorageItem {
|
|
36
|
+
readonly realId: string;
|
|
37
|
+
/** `null` for the drive root and for parentless items (gdrive "shared with me"). */
|
|
38
|
+
readonly parentRealId: string | null;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
/**
|
|
41
|
+
* Absolute path within the drive (`/folder/file.ext`, root = `/`),
|
|
42
|
+
* or `null` when the provider cannot tell cheaply. The service fills
|
|
43
|
+
* it in where it can.
|
|
44
|
+
*/
|
|
45
|
+
readonly path: string | null;
|
|
46
|
+
readonly kind: StorageItemKind;
|
|
47
|
+
readonly mimeType: string | null;
|
|
48
|
+
/** Bytes; `null` for folders and native docs. */
|
|
49
|
+
readonly size: number | null;
|
|
50
|
+
/** UTC ISO-8601. */
|
|
51
|
+
readonly createdUtc: string | null;
|
|
52
|
+
/** UTC ISO-8601. */
|
|
53
|
+
readonly modifiedUtc: string | null;
|
|
54
|
+
readonly modifiedBy: string | null;
|
|
55
|
+
/** Opaque concurrency token (OneDrive eTag, Dropbox rev, gdrive version). */
|
|
56
|
+
readonly revision: string | null;
|
|
57
|
+
readonly contentHash: StorageContentHash | null;
|
|
58
|
+
readonly webUrl: string | null;
|
|
59
|
+
readonly isShared: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Feature flags the service adapts to. Reported to the agent in summary form. */
|
|
62
|
+
export interface StorageCapabilities {
|
|
63
|
+
/** `path` = the provider resolves `/a/b` natively; `id` = the service walks the tree. */
|
|
64
|
+
readonly addressing: "path" | "id";
|
|
65
|
+
/** `false` when siblings may share a name (Google Drive). */
|
|
66
|
+
readonly uniqueNames: boolean;
|
|
67
|
+
/** Full-text search over file contents. */
|
|
68
|
+
readonly contentSearch: boolean;
|
|
69
|
+
/** Search hits can carry a snippet. */
|
|
70
|
+
readonly searchSnippets: boolean;
|
|
71
|
+
/** Soft delete into a recoverable trash / recycle bin. */
|
|
72
|
+
readonly trash: boolean;
|
|
73
|
+
/** Byte-less provider documents exist and are exported on download. */
|
|
74
|
+
readonly nativeDocs: boolean;
|
|
75
|
+
/** Same-drive copies happen server-side. */
|
|
76
|
+
readonly serverSideCopy: boolean;
|
|
77
|
+
}
|
|
78
|
+
/** Kind of a drive as offered to the user in `familiar storage list`. */
|
|
79
|
+
export type StorageDriveKind = "personal" | "shared" | "team";
|
|
80
|
+
/**
|
|
81
|
+
* One drive of one account: a OneDrive, a SharePoint document library,
|
|
82
|
+
* a Google shared drive, … Opaque to the service beyond these fields.
|
|
83
|
+
*/
|
|
84
|
+
export interface StorageDrive {
|
|
85
|
+
/** Account key the drive belongs to (UPN, e-mail, dbid, …). */
|
|
86
|
+
readonly account: string;
|
|
87
|
+
/**
|
|
88
|
+
* Value to put into a mount's `drive:` field. `null` = the account's
|
|
89
|
+
* primary drive (the default when `drive:` is omitted).
|
|
90
|
+
*/
|
|
91
|
+
readonly selector: string | null;
|
|
92
|
+
/** Provider-native drive id. Never shown to the agent. */
|
|
93
|
+
readonly id: string;
|
|
94
|
+
/** Human-readable name, e.g. "OneDrive" or "Verwaltung / Dokumente". */
|
|
95
|
+
readonly name: string;
|
|
96
|
+
readonly kind: StorageDriveKind;
|
|
97
|
+
}
|
|
98
|
+
/** Search criteria. The service post-filters kind / mime / modified / subtree, so providers may ignore them. */
|
|
99
|
+
export interface StorageSearchQuery {
|
|
100
|
+
/** Plain keywords; providers must escape their own query syntax. */
|
|
101
|
+
readonly text: string;
|
|
102
|
+
/** Folder to scope the search to (provider-native id). */
|
|
103
|
+
readonly underRealId?: string;
|
|
104
|
+
/** Absolute path of that folder, when known. */
|
|
105
|
+
readonly underPath?: string;
|
|
106
|
+
readonly kind?: StorageItemKind;
|
|
107
|
+
readonly mimePrefix?: string;
|
|
108
|
+
/** Inclusive lower bound, UTC ISO. */
|
|
109
|
+
readonly modifiedFromUtc?: string;
|
|
110
|
+
/** Exclusive upper bound, UTC ISO. */
|
|
111
|
+
readonly modifiedToUtc?: string;
|
|
112
|
+
}
|
|
113
|
+
/** Where a search matched. */
|
|
114
|
+
export type StorageMatchedIn = "name" | "content" | "unknown";
|
|
115
|
+
/** One search hit. */
|
|
116
|
+
export interface StorageSearchHit {
|
|
117
|
+
readonly item: StorageItem;
|
|
118
|
+
readonly matchedIn: StorageMatchedIn;
|
|
119
|
+
readonly snippet?: string;
|
|
120
|
+
}
|
|
121
|
+
/** One page of results with an opaque continuation cursor (`null` on the last page). */
|
|
122
|
+
export interface StoragePage<T> {
|
|
123
|
+
readonly entries: readonly T[];
|
|
124
|
+
readonly nextCursor: string | null;
|
|
125
|
+
}
|
|
126
|
+
/** Paging options. */
|
|
127
|
+
export interface StoragePageOptions {
|
|
128
|
+
readonly cursor?: string;
|
|
129
|
+
readonly limit: number;
|
|
130
|
+
readonly signal?: AbortSignal;
|
|
131
|
+
}
|
|
132
|
+
/** Order for folder listings. Providers that cannot sort leave it to the service. */
|
|
133
|
+
export type StorageListOrder = "name" | "modified_desc";
|
|
134
|
+
/** Conflict behaviour on create. */
|
|
135
|
+
export type StorageConflict = "fail" | "rename" | "replace";
|
|
136
|
+
/** Host-side file to upload. */
|
|
137
|
+
export interface StorageUploadSource {
|
|
138
|
+
/** Absolute host path of a regular file. */
|
|
139
|
+
readonly path: string;
|
|
140
|
+
readonly size: number;
|
|
141
|
+
readonly mimeType?: string;
|
|
142
|
+
}
|
|
143
|
+
/** Result of a download. */
|
|
144
|
+
export interface StorageDownloadResult {
|
|
145
|
+
readonly bytes: number;
|
|
146
|
+
readonly mimeType: string | null;
|
|
147
|
+
/** File name to store under; for native docs it carries the export extension. */
|
|
148
|
+
readonly fileName: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The provider contract. One instance per plugin, covering every
|
|
152
|
+
* account the plugin is logged into. Implementations must be **lazy**:
|
|
153
|
+
* providers register during `prepare()` (so the CLI can use them
|
|
154
|
+
* without the daemon), which forbids network access, so the first
|
|
155
|
+
* network call happens inside a method.
|
|
156
|
+
*
|
|
157
|
+
* Every method may throw {@link StorageError}; anything else is treated
|
|
158
|
+
* as `ProviderUnavailable` by the service.
|
|
159
|
+
*/
|
|
160
|
+
export interface StorageProvider {
|
|
161
|
+
readonly pluginId: string;
|
|
162
|
+
/** Capabilities for one drive (content search may depend on the tenant / plan). */
|
|
163
|
+
capabilities(drive: StorageDrive): StorageCapabilities;
|
|
164
|
+
/** Accounts this plugin is logged into (lower-cased keys). */
|
|
165
|
+
listAccounts(): Promise<readonly string[]>;
|
|
166
|
+
/** CLI hint telling the user how to (re-)login `account`. */
|
|
167
|
+
loginCommand(account: string): string;
|
|
168
|
+
/** Drives the account can reach: personal, shared and team drives / libraries. */
|
|
169
|
+
listDrives(account: string, options: {
|
|
170
|
+
readonly limit: number;
|
|
171
|
+
readonly search?: string;
|
|
172
|
+
}): Promise<readonly StorageDrive[]>;
|
|
173
|
+
/**
|
|
174
|
+
* Open one drive by its config selector (`null` = primary drive).
|
|
175
|
+
*
|
|
176
|
+
* @throws StorageError `NotFound` when the selector matches nothing.
|
|
177
|
+
*/
|
|
178
|
+
openDrive(account: string, selector: string | null): Promise<StorageDrive>;
|
|
179
|
+
/** Provider id of the drive's root folder. */
|
|
180
|
+
rootRealId(drive: StorageDrive): Promise<string>;
|
|
181
|
+
/** Metadata of one item, or `null` if it does not exist. */
|
|
182
|
+
stat(drive: StorageDrive, realId: string): Promise<StorageItem | null>;
|
|
183
|
+
/**
|
|
184
|
+
* Resolve an absolute path natively. Only required when
|
|
185
|
+
* `capabilities.addressing === "path"`. Returns `[]` when not found,
|
|
186
|
+
* several items when names are ambiguous.
|
|
187
|
+
*/
|
|
188
|
+
resolvePath?(drive: StorageDrive, path: string): Promise<readonly StorageItem[]>;
|
|
189
|
+
/** One page of the direct children of a folder. */
|
|
190
|
+
list(drive: StorageDrive, folderRealId: string, options: StoragePageOptions & {
|
|
191
|
+
readonly order?: StorageListOrder;
|
|
192
|
+
}): Promise<StoragePage<StorageItem>>;
|
|
193
|
+
/** One page of search hits. */
|
|
194
|
+
search(drive: StorageDrive, query: StorageSearchQuery, options: StoragePageOptions): Promise<StoragePage<StorageSearchHit>>;
|
|
195
|
+
/**
|
|
196
|
+
* Stream the item's bytes into `destPath` (created / truncated). Native
|
|
197
|
+
* docs are exported server-side in a provider-chosen format; a native
|
|
198
|
+
* type without an export mapping throws `Unsupported`.
|
|
199
|
+
*/
|
|
200
|
+
download(drive: StorageDrive, realId: string, destPath: string, options?: {
|
|
201
|
+
readonly signal?: AbortSignal;
|
|
202
|
+
}): Promise<StorageDownloadResult>;
|
|
203
|
+
/**
|
|
204
|
+
* Upload a host-side file as `name` into `parentRealId`. The provider
|
|
205
|
+
* picks simple vs. resumable upload.
|
|
206
|
+
*
|
|
207
|
+
* - `conflict: "fail"` — throw `NameConflict` if the name is taken
|
|
208
|
+
* (only meaningful with `uniqueNames`).
|
|
209
|
+
* - `conflict: "rename"` — let the provider pick a free name.
|
|
210
|
+
* - `conflict: "replace"` — overwrite the content of the item
|
|
211
|
+
* `replaceRealId` (required; replacing by name is ambiguous on
|
|
212
|
+
* id-addressed providers). With `ifRevision`, the replace must be
|
|
213
|
+
* guarded by that revision (atomically where the API allows) and
|
|
214
|
+
* throw `RevisionMismatch` otherwise.
|
|
215
|
+
*/
|
|
216
|
+
upload(drive: StorageDrive, parentRealId: string, name: string, source: StorageUploadSource, options: {
|
|
217
|
+
readonly conflict: StorageConflict;
|
|
218
|
+
readonly replaceRealId?: string;
|
|
219
|
+
readonly ifRevision?: string;
|
|
220
|
+
readonly signal?: AbortSignal;
|
|
221
|
+
}): Promise<StorageItem>;
|
|
222
|
+
/** Create a folder. `reuse` returns an existing folder of that name. */
|
|
223
|
+
createFolder(drive: StorageDrive, parentRealId: string, name: string, options: {
|
|
224
|
+
readonly conflict: "fail" | "rename" | "reuse";
|
|
225
|
+
}): Promise<StorageItem>;
|
|
226
|
+
/** Move and / or rename within one drive. */
|
|
227
|
+
move(drive: StorageDrive, realId: string, target: {
|
|
228
|
+
readonly parentRealId?: string;
|
|
229
|
+
readonly name?: string;
|
|
230
|
+
}, options: {
|
|
231
|
+
readonly ifRevision?: string;
|
|
232
|
+
}): Promise<StorageItem>;
|
|
233
|
+
/** Server-side copy within one drive; resolves once the copy is complete. */
|
|
234
|
+
copy(drive: StorageDrive, realId: string, target: {
|
|
235
|
+
readonly parentRealId: string;
|
|
236
|
+
readonly name?: string;
|
|
237
|
+
}, options: {
|
|
238
|
+
readonly conflict: "fail" | "rename";
|
|
239
|
+
}): Promise<StorageItem>;
|
|
240
|
+
/** Soft delete into the provider's trash. Folders go with their contents. */
|
|
241
|
+
trash(drive: StorageDrive, realId: string, options: {
|
|
242
|
+
readonly ifRevision?: string;
|
|
243
|
+
}): Promise<void>;
|
|
244
|
+
}
|
|
245
|
+
/** Normalized provider failure codes. */
|
|
246
|
+
export type StorageErrorCode = "NotFound" | "NameConflict" | "RevisionMismatch" | "Unsupported" | "AuthExpired" | "RateLimited" | "QuotaExceeded" | "ProviderUnavailable";
|
|
247
|
+
/**
|
|
248
|
+
* Error a provider throws with a normalized code. The service maps it
|
|
249
|
+
* onto an agent-readable `ToolError`.
|
|
250
|
+
*/
|
|
251
|
+
export declare class StorageError extends Error {
|
|
252
|
+
readonly code: StorageErrorCode;
|
|
253
|
+
constructor(code: StorageErrorCode, message: string);
|
|
254
|
+
}
|
|
255
|
+
/** Plugin-facing registration surface (`ctx.storage`). */
|
|
256
|
+
export interface StorageApi {
|
|
257
|
+
/**
|
|
258
|
+
* Register the plugin's storage provider. Call from `prepare()` so
|
|
259
|
+
* CLI commands reach it too. Throws if the same `pluginId`
|
|
260
|
+
* registers twice.
|
|
261
|
+
*/
|
|
262
|
+
registerProvider(provider: StorageProvider): void;
|
|
263
|
+
}
|
package/build/Storage.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud-storage provider SPI. Lives in `shared/` because the host (which
|
|
3
|
+
* owns the `StorageService`, the `storage_*` agent tools and the
|
|
4
|
+
* `familiar storage` CLI) and the provider plugins (ms365, later gdrive,
|
|
5
|
+
* dropbox, …) trade in the same shapes.
|
|
6
|
+
*
|
|
7
|
+
* Division of labour:
|
|
8
|
+
*
|
|
9
|
+
* - **Providers** speak their API: they address items by their native
|
|
10
|
+
* id (`realId`), stream bytes between the provider and a host-side
|
|
11
|
+
* file, and map API failures onto {@link StorageError} codes. They
|
|
12
|
+
* never see mounts, refs, policy, quotas or the agent.
|
|
13
|
+
* - **The host `StorageService`** owns everything that must behave the
|
|
14
|
+
* same for every provider: mount aliases, ref parsing, path
|
|
15
|
+
* resolution for id-addressed providers, write policy, search
|
|
16
|
+
* fan-out, cross-mount copies and the per-run byte quota.
|
|
17
|
+
*
|
|
18
|
+
* Credentials never leave the provider: the agent only ever sees mount
|
|
19
|
+
* aliases, refs, metadata and files placed in its scratch directory.
|
|
20
|
+
* Hard delete is deliberately absent from the SPI.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Error a provider throws with a normalized code. The service maps it
|
|
24
|
+
* onto an agent-readable `ToolError`.
|
|
25
|
+
*/
|
|
26
|
+
export class StorageError extends Error {
|
|
27
|
+
code;
|
|
28
|
+
constructor(code, message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.name = "StorageError";
|
|
32
|
+
}
|
|
33
|
+
}
|
package/build/ToolRunner.d.ts
CHANGED
|
@@ -84,6 +84,17 @@ export interface OffloadedJson {
|
|
|
84
84
|
* the runner does not catch.
|
|
85
85
|
*/
|
|
86
86
|
export declare function runJsonTool(body: () => Promise<object>, ctx: ToolRunContext): Promise<object>;
|
|
87
|
+
/**
|
|
88
|
+
* Optional presentation settings for {@link runJsonLinesTool} and
|
|
89
|
+
* {@link runTextTool}.
|
|
90
|
+
*
|
|
91
|
+
* - `header`: one line prepended to the result (provenance,
|
|
92
|
+
* pagination, …). It is always kept inline, also when the body gets
|
|
93
|
+
* truncated, and is included at the top of the spilled file.
|
|
94
|
+
*/
|
|
95
|
+
export interface ToolRunOptions {
|
|
96
|
+
readonly header?: string;
|
|
97
|
+
}
|
|
87
98
|
/**
|
|
88
99
|
* Run a tool whose result is a stream of JSON objects, serialized as
|
|
89
100
|
* newline-delimited JSON. If the total fits within the limit, returns
|
|
@@ -97,7 +108,7 @@ export declare function runJsonTool(body: () => Promise<object>, ctx: ToolRunCon
|
|
|
97
108
|
* limit, only the marker is returned (`omittedLines` then equals the
|
|
98
109
|
* total line count).
|
|
99
110
|
*/
|
|
100
|
-
export declare function runJsonLinesTool(body: () => Promise<Iterable<object> | AsyncIterable<object>>, ctx: ToolRunContext): Promise<string>;
|
|
111
|
+
export declare function runJsonLinesTool(body: () => Promise<Iterable<object> | AsyncIterable<object>>, ctx: ToolRunContext, options?: ToolRunOptions | (() => ToolRunOptions)): Promise<string>;
|
|
101
112
|
/**
|
|
102
113
|
* Run a tool whose result is free text. If the text fits the limit,
|
|
103
114
|
* returns it verbatim. Otherwise spills the full text to scratch and
|
|
@@ -109,7 +120,7 @@ export declare function runJsonLinesTool(body: () => Promise<Iterable<object> |
|
|
|
109
120
|
* The truncation respects UTF-8 code-point boundaries so multi-byte
|
|
110
121
|
* characters never get split.
|
|
111
122
|
*/
|
|
112
|
-
export declare function runTextTool(body: () => Promise<string>, ctx: ToolRunContext): Promise<string>;
|
|
123
|
+
export declare function runTextTool(body: () => Promise<string>, ctx: ToolRunContext, options?: ToolRunOptions | (() => ToolRunOptions)): Promise<string>;
|
|
113
124
|
/**
|
|
114
125
|
* Truncate `str` to at most `maxBytes` UTF-8 bytes without splitting a
|
|
115
126
|
* code point. Surrogate pairs (code points beyond the BMP) are kept
|
package/build/ToolRunner.js
CHANGED
|
@@ -89,15 +89,17 @@ export async function runJsonTool(body, ctx) {
|
|
|
89
89
|
* limit, only the marker is returned (`omittedLines` then equals the
|
|
90
90
|
* total line count).
|
|
91
91
|
*/
|
|
92
|
-
export async function runJsonLinesTool(body, ctx) {
|
|
92
|
+
export async function runJsonLinesTool(body, ctx, options) {
|
|
93
93
|
const source = await body();
|
|
94
94
|
const serialized = [];
|
|
95
95
|
for await (const item of asAsyncIterable(source)) {
|
|
96
96
|
serialized.push(JSON.stringify(item));
|
|
97
97
|
}
|
|
98
|
-
const
|
|
98
|
+
const header = resolveOptions(options).header;
|
|
99
|
+
const headerPrefix = header !== undefined ? `${header}\n` : "";
|
|
100
|
+
const inline = headerPrefix + serialized.join("\n");
|
|
99
101
|
if (estimateTokens(inline) <= ctx.limit) {
|
|
100
|
-
return inline;
|
|
102
|
+
return header !== undefined && serialized.length === 0 ? header : inline;
|
|
101
103
|
}
|
|
102
104
|
const fullText = inline;
|
|
103
105
|
const fullResultAt = await ctx.spill("result.jsonl", Buffer.from(fullText, "utf8"));
|
|
@@ -111,7 +113,7 @@ export async function runJsonLinesTool(body, ctx) {
|
|
|
111
113
|
});
|
|
112
114
|
const markerBudget = estimateTokens(upperBoundMarker);
|
|
113
115
|
const kept = [];
|
|
114
|
-
let usedTokens =
|
|
116
|
+
let usedTokens = estimateTokens(headerPrefix);
|
|
115
117
|
for (const line of serialized) {
|
|
116
118
|
const separator = kept.length > 0 ? 1 : 0; // newline before this line (~1 token)
|
|
117
119
|
const newlineBeforeMarker = 1; // newline between last kept line and marker
|
|
@@ -125,9 +127,9 @@ export async function runJsonLinesTool(body, ctx) {
|
|
|
125
127
|
const omittedLines = serialized.length - kept.length;
|
|
126
128
|
const marker = JSON.stringify({ truncated: true, fullResultAt, omittedLines });
|
|
127
129
|
if (kept.length === 0) {
|
|
128
|
-
return marker
|
|
130
|
+
return `${headerPrefix}${marker}`;
|
|
129
131
|
}
|
|
130
|
-
return `${kept.join("\n")}\n${marker}`;
|
|
132
|
+
return `${headerPrefix}${kept.join("\n")}\n${marker}`;
|
|
131
133
|
}
|
|
132
134
|
/**
|
|
133
135
|
* Run a tool whose result is free text. If the text fits the limit,
|
|
@@ -140,22 +142,25 @@ export async function runJsonLinesTool(body, ctx) {
|
|
|
140
142
|
* The truncation respects UTF-8 code-point boundaries so multi-byte
|
|
141
143
|
* characters never get split.
|
|
142
144
|
*/
|
|
143
|
-
export async function runTextTool(body, ctx) {
|
|
144
|
-
const
|
|
145
|
+
export async function runTextTool(body, ctx, options) {
|
|
146
|
+
const bodyText = await body();
|
|
147
|
+
const header = resolveOptions(options).header;
|
|
148
|
+
const headerPrefix = header !== undefined ? `${header}\n` : "";
|
|
149
|
+
const text = headerPrefix + bodyText;
|
|
145
150
|
if (estimateTokens(text) <= ctx.limit) {
|
|
146
|
-
return text;
|
|
151
|
+
return header !== undefined && bodyText.length === 0 ? header : text;
|
|
147
152
|
}
|
|
148
153
|
const fullResultAt = await ctx.spill("result.txt", Buffer.from(text, "utf8"));
|
|
149
154
|
const footer = `\n\n[truncated; full result at ${fullResultAt}]`;
|
|
150
|
-
const footerTokens = estimateTokens(footer);
|
|
155
|
+
const footerTokens = estimateTokens(footer) + estimateTokens(headerPrefix);
|
|
151
156
|
// The budget is in tokens; `truncateUtf8` bounds by character count.
|
|
152
157
|
// estimateTokens(s) = ceil(len/4), so the char budget for the prefix
|
|
153
158
|
// is the remaining token budget × 4. Bounding `truncateUtf8` by that
|
|
154
159
|
// char count is slightly conservative for multi-byte text — fine for
|
|
155
160
|
// a placeholder estimator backing a truncation marker.
|
|
156
161
|
const prefixCharBudget = Math.max(0, (ctx.limit - footerTokens) * 4);
|
|
157
|
-
const prefix = truncateUtf8(
|
|
158
|
-
return `${prefix}${footer}`;
|
|
162
|
+
const prefix = truncateUtf8(bodyText, prefixCharBudget);
|
|
163
|
+
return `${headerPrefix}${prefix}${footer}`;
|
|
159
164
|
}
|
|
160
165
|
/**
|
|
161
166
|
* Truncate `str` to at most `maxBytes` UTF-8 bytes without splitting a
|
|
@@ -182,6 +187,17 @@ export function truncateUtf8(str, maxBytes) {
|
|
|
182
187
|
}
|
|
183
188
|
return str.slice(0, charIdx);
|
|
184
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Normalize the options argument. A function form lets a tool compute
|
|
192
|
+
* its header *after* the body ran (e.g. from pagination state the body
|
|
193
|
+
* produced).
|
|
194
|
+
*/
|
|
195
|
+
function resolveOptions(options) {
|
|
196
|
+
if (options === undefined) {
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
return typeof options === "function" ? options() : options;
|
|
200
|
+
}
|
|
185
201
|
/** Lift a sync iterable into an async one so we can `for await` over both. */
|
|
186
202
|
async function* asAsyncIterable(source) {
|
|
187
203
|
if (Symbol.asyncIterator in source) {
|
package/build/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { buildMailId, type ForwardInput, type MailAddress, type MailApi, type Ma
|
|
|
18
18
|
export { MAIL_STYLE_TEMPLATE_DEFAULTS, type MailStyleTemplate, mailStyleTemplatePath, signatureToPlainText, } from "./MailStyleTemplate.js";
|
|
19
19
|
export { type ModelMetaData, ModelNotSupported, type ModelProviderDescriptor, } from "./ModelMetaData.js";
|
|
20
20
|
export { renderMarkdownToHtml } from "./markdownHtml.js";
|
|
21
|
+
export { escapeTableCell, markdownTable, writeMarkdown } from "./markdownOutput.js";
|
|
21
22
|
export { renderMarkdown } from "./markdownTerminal.js";
|
|
22
23
|
export { matchesAnyGlob, matchesGlob } from "./PathGlob.js";
|
|
23
24
|
export type { AnyCommandDef, EmitHandle, EmitOptions, EventContextProvider, HostContext, McpClient, McpInfo, PluginHostManifest, PluginManifest, PluginTool, PluginToolCallContext, } from "./Plugin.js";
|
|
@@ -28,10 +29,11 @@ export { ScheduledSubagentBus, type ScheduledSubagentNotification, type Schedule
|
|
|
28
29
|
export { AGENTRUNS_CHANNEL, CHATMESSAGES_NEW_CHANNEL, EVENT_TERMINAL_UPDATE_SQL, EVENTS_NEW_CHANNEL, EVENTS_STATE_CHANNEL, SCHEDULED_SUBAGENTS_CHANNEL, SCHEMA_SQL, STEPRESULTS_NEW_CHANNEL, TOPIC_PATTERN, } from "./Schema.js";
|
|
29
30
|
export type { NewStepResult, StepResultRow } from "./StepResult.js";
|
|
30
31
|
export { StepResultBus, type StepResultUnsubscribe } from "./StepResultBus.js";
|
|
32
|
+
export { type StorageApi, type StorageCapabilities, type StorageConflict, type StorageContentHash, type StorageDownloadResult, type StorageDrive, type StorageDriveKind, StorageError, type StorageErrorCode, type StorageHashAlgo, type StorageItem, type StorageItemKind, type StorageListOrder, type StorageMatchedIn, type StoragePage, type StoragePageOptions, type StorageProvider, type StorageSearchHit, type StorageSearchQuery, type StorageUploadSource, } from "./Storage.js";
|
|
31
33
|
export { dayBoundsInZone, type ParseInZoneResult, parseInZone, renderInZone, } from "./Timezone.js";
|
|
32
34
|
export type { NewToolCall, ToolCallRow, ToolUsageCount } from "./ToolCall.js";
|
|
33
35
|
export { ToolCallBus } from "./ToolCallBus.js";
|
|
34
36
|
export { DEFAULT_TOOL_LEVEL, type ToolLevel } from "./ToolLevel.js";
|
|
35
|
-
export { DEFAULT_TOOL_CALL_OFFLOADING_LIMIT, type OffloadedJson, runJsonLinesTool, runJsonTool, runTextTool, ToolError, type ToolRunContext, truncateUtf8, } from "./ToolRunner.js";
|
|
37
|
+
export { DEFAULT_TOOL_CALL_OFFLOADING_LIMIT, type OffloadedJson, runJsonLinesTool, runJsonTool, runTextTool, ToolError, type ToolRunContext, type ToolRunOptions, truncateUtf8, } from "./ToolRunner.js";
|
|
36
38
|
export { ALL_GROUP_NAME, CORE_PLUGIN_ID, IDENT_PATTERN, MCP_GROUP_NAME, matchesAnyToolPattern, NONE_GROUP_NAME, RESERVED_GROUP_NAMES, toolPatternMatches, validateGroupName, } from "./ToolsExpressionUtils.js";
|
|
37
39
|
export type { WorkspaceFile, WorkspaceFileFilter, WorkspaceWatcherApi, } from "./WorkspaceFile.js";
|
package/build/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { buildMailId, parseMailId, } from "./Mail.js";
|
|
|
12
12
|
export { MAIL_STYLE_TEMPLATE_DEFAULTS, mailStyleTemplatePath, signatureToPlainText, } from "./MailStyleTemplate.js";
|
|
13
13
|
export { ModelNotSupported, } from "./ModelMetaData.js";
|
|
14
14
|
export { renderMarkdownToHtml } from "./markdownHtml.js";
|
|
15
|
+
export { escapeTableCell, markdownTable, writeMarkdown } from "./markdownOutput.js";
|
|
15
16
|
export { renderMarkdown } from "./markdownTerminal.js";
|
|
16
17
|
export { matchesAnyGlob, matchesGlob } from "./PathGlob.js";
|
|
17
18
|
export { DaemonStoppedError, definePlugin } from "./Plugin.js";
|
|
@@ -19,6 +20,7 @@ export { POSTGRES_DB, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_USER, PostgresConne
|
|
|
19
20
|
export { ScheduledSubagentBus, } from "./ScheduledSubagentBus.js";
|
|
20
21
|
export { AGENTRUNS_CHANNEL, CHATMESSAGES_NEW_CHANNEL, EVENT_TERMINAL_UPDATE_SQL, EVENTS_NEW_CHANNEL, EVENTS_STATE_CHANNEL, SCHEDULED_SUBAGENTS_CHANNEL, SCHEMA_SQL, STEPRESULTS_NEW_CHANNEL, TOPIC_PATTERN, } from "./Schema.js";
|
|
21
22
|
export { StepResultBus } from "./StepResultBus.js";
|
|
23
|
+
export { StorageError, } from "./Storage.js";
|
|
22
24
|
export { dayBoundsInZone, parseInZone, renderInZone, } from "./Timezone.js";
|
|
23
25
|
export { ToolCallBus } from "./ToolCallBus.js";
|
|
24
26
|
export { DEFAULT_TOOL_LEVEL } from "./ToolLevel.js";
|