@tomradex/plugin-sdk 0.0.0-stage → 0.2.16

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.
@@ -0,0 +1,624 @@
1
+ import type { z } from "zod";
2
+ import type { ImApi } from "./im.js";
3
+ import type { TerminalsApi } from "./terminals.js";
4
+ import type { WebSearchApi } from "./web-search.js";
5
+ import type { ModelsApi } from "./models.js";
6
+ import type { Author, Board, CreateItemInput, FieldDef, FieldOption, FieldType, FilterClause, Item, ListItemsFilter, OplogEntity, OplogEntry, OplogOp, Patch, Project, SortKey, View } from "@tomradex/domain";
7
+ /**
8
+ * The plugin contract. Everything a plugin reaches returns a Promise, and every
9
+ * function it hands the host may return one: the same code runs in the app's
10
+ * process today and behind a process boundary later. Values that cross are
11
+ * plain data — never a live object of the host's.
12
+ */
13
+ export type Disposer = () => void;
14
+ /** A language the app may be speaking: a BCP 47 tag such as "en", "zh", "zh-Hant" or one a language pack adds ("pt-BR"). SDK 0.2.8 widened it from "en" | "zh". */
15
+ export type Locale = string;
16
+ /** Text in the app's language: a plain string, or one per language tag picked each time it is shown (English is the fallback). */
17
+ export type Localized = string | ({
18
+ en: string;
19
+ } & Partial<Record<Locale, string>>);
20
+ /** A settings section's text in one language; fields are keyed by schema property. */
21
+ export interface SectionWords {
22
+ title?: string;
23
+ description?: string;
24
+ fields?: Record<string, {
25
+ title?: string;
26
+ description?: string;
27
+ options?: Record<string, string>;
28
+ }>;
29
+ }
30
+ /** The services a plugin names in `inject` (required) or `optional`. */
31
+ export type ServiceName = "items" | "fields" | "boards" | "views" | "projects" | "db" | "settings" | "tools" | "contributions" | "agent" | "surfaces" | "alerts" | "oplog" | "im" | "webSearch" | "terminals" | "models";
32
+ export interface Logger {
33
+ debug(...args: unknown[]): void;
34
+ info(...args: unknown[]): void;
35
+ warn(...args: unknown[]): void;
36
+ error(...args: unknown[]): void;
37
+ }
38
+ export interface ItemQuery {
39
+ projectId: string;
40
+ filter?: FilterClause[];
41
+ sortBy?: SortKey[];
42
+ search?: string;
43
+ limit?: number;
44
+ widen?: boolean;
45
+ }
46
+ /** Writes never take an author: the host says who wrote — a person, the agent run it serves, or the plugin itself. */
47
+ export interface ItemsApi {
48
+ list(filter?: ListItemsFilter): Promise<Item[]>;
49
+ query(q: ItemQuery): Promise<Item[]>;
50
+ get(projectId: string, id: string): Promise<Item | null>;
51
+ getByKey(projectId: string, key: string): Promise<Item | null>;
52
+ create(input: Omit<CreateItemInput, "by">): Promise<Item>;
53
+ update(projectId: string, id: string, patch: Patch, opts?: {
54
+ baseVersion?: number;
55
+ }): Promise<Item>;
56
+ delete(projectId: string, id: string): Promise<void>;
57
+ addRelation(projectId: string, id: string, blockedId: string): Promise<void>;
58
+ removeRelation(projectId: string, id: string, blockedId: string): Promise<void>;
59
+ labels(projectId: string): Promise<string[]>;
60
+ /**
61
+ * SDK 0.2.12: a policy over every write before it lands — items, and the workstream and view
62
+ * writes that take the same path — whoever writes. Throw to refuse it. It is asked inside the
63
+ * write's transaction, so it must answer at once: a check that returns a Promise is not waited
64
+ * for, and the write goes on.
65
+ */
66
+ gate(check: (write: PendingWrite) => void): Promise<Disposer>;
67
+ }
68
+ /** SDK 0.2.12: a write about to land, as a write gate sees it. A copy: changing it changes nothing. */
69
+ export interface PendingWrite<T = unknown> {
70
+ projectId: string;
71
+ entity: OplogEntity;
72
+ op: OplogOp;
73
+ id: string;
74
+ by: Author;
75
+ /** The extension making the write, when one is. */
76
+ via: string | null;
77
+ runId: string | null;
78
+ /** The version the writer read, when it asserts one. */
79
+ baseVersion: number | null;
80
+ before: T | null;
81
+ after: T | null;
82
+ /** For an update: only the fields the writer asked to change. */
83
+ patch: Record<string, unknown> | null;
84
+ }
85
+ export interface FieldsApi {
86
+ list(projectId: string): Promise<FieldDef[]>;
87
+ get(projectId: string, id: string): Promise<FieldDef | null>;
88
+ create(input: {
89
+ projectId: string;
90
+ name: string;
91
+ type: FieldType;
92
+ options?: {
93
+ name: string;
94
+ description?: string | null;
95
+ }[];
96
+ }): Promise<FieldDef>;
97
+ update(projectId: string, id: string, patch: {
98
+ name?: string;
99
+ rank?: number;
100
+ }): Promise<FieldDef>;
101
+ remove(projectId: string, id: string): Promise<void>;
102
+ addOption(projectId: string, fieldId: string, input: {
103
+ name: string;
104
+ description?: string | null;
105
+ }): Promise<FieldOption>;
106
+ }
107
+ export interface BoardsApi {
108
+ list(projectId: string): Promise<Board[]>;
109
+ get(projectId: string, id: string): Promise<Board | null>;
110
+ defaultFor(projectId: string): Promise<Board>;
111
+ }
112
+ export interface ViewsApi {
113
+ list(projectId: string): Promise<View[]>;
114
+ get(projectId: string, id: string): Promise<View | null>;
115
+ }
116
+ export interface ProjectsApi {
117
+ list(): Promise<Project[]>;
118
+ get(id: string): Promise<Project | null>;
119
+ }
120
+ /** `"hub"` addresses the Hub's own database, for tables migrated with `scope: "hub"`. */
121
+ export type DbTarget = string;
122
+ export type SqlParams = unknown[] | Record<string, unknown>;
123
+ export interface TableSpec {
124
+ /** Project-scope tables: the column holding the project id, or a chain through another table. */
125
+ project?: string | {
126
+ via: string;
127
+ table: string;
128
+ };
129
+ /**
130
+ * Exported tables travel in the project's folder; `false` keeps them on this machine.
131
+ * `private` rows (conversations, say) never travel to a remote known to be public.
132
+ */
133
+ export: {
134
+ order: string;
135
+ appendOnly?: boolean;
136
+ always?: boolean;
137
+ private?: boolean;
138
+ } | false;
139
+ }
140
+ /**
141
+ * Plain SQL against the caches. The namespace is the plugin's own: tables are
142
+ * `plugin_<namespace>_*`. Reads of the app's tables are allowed; writing them
143
+ * goes around history and undo, and the install dialog says so.
144
+ */
145
+ export interface DbApi {
146
+ migrate(steps: {
147
+ version: number;
148
+ up: string;
149
+ }[], opts: {
150
+ scope?: "project" | "hub";
151
+ tables: Record<string, TableSpec>;
152
+ }): Promise<void>;
153
+ all<T = Record<string, unknown>>(target: DbTarget, sql: string, params?: SqlParams): Promise<T[]>;
154
+ get<T = Record<string, unknown>>(target: DbTarget, sql: string, params?: SqlParams): Promise<T | undefined>;
155
+ run(target: DbTarget, sql: string, params?: SqlParams): Promise<{
156
+ changes: number;
157
+ lastInsertRowid: number;
158
+ }>;
159
+ /** One transaction: every statement lands, or none does. */
160
+ batch(target: DbTarget, statements: {
161
+ sql: string;
162
+ params?: SqlParams;
163
+ }[]): Promise<{
164
+ changes: number;
165
+ }[]>;
166
+ isOpen(projectId: string): Promise<boolean>;
167
+ }
168
+ export interface SettingsSectionSpec<T> {
169
+ /** Defaults to the namespace; otherwise it must start with it. */
170
+ id?: string;
171
+ title: string;
172
+ description?: string;
173
+ /** Drawn from its JSON Schema; a field with `.meta({ advanced: true })` is folded under Advanced. */
174
+ schema: z.ZodType<T>;
175
+ defaults: T;
176
+ scope?: "machine" | "user";
177
+ /** What agents may do with it through get_settings / update_settings. Defaults to read; a section holding a password is none. */
178
+ agent?: "write" | "read" | "none";
179
+ /** The card in other languages; `title`, `description` and the schema's own labels are the English. */
180
+ words?: Partial<Record<Locale, SectionWords>>;
181
+ }
182
+ export interface SettingsApi {
183
+ register<T>(section: SettingsSectionSpec<T>): Promise<void>;
184
+ get<T = unknown>(id?: string): Promise<T>;
185
+ set<T = unknown>(value: T, id?: string): Promise<void>;
186
+ /** Opens the app's Settings on this plugin's section, bringing the window forward — for a click outside the window. */
187
+ show(id?: string): Promise<void>;
188
+ /** The app's language now. It is a setting: `settings.changed` fires when the person switches it. */
189
+ locale(): Promise<Locale>;
190
+ }
191
+ export interface ToolInvocation {
192
+ projectId?: string;
193
+ /** Present when an agent run made the call. */
194
+ runId?: string;
195
+ author: Author;
196
+ }
197
+ export interface ToolSpec<S extends z.ZodObject = z.ZodObject> {
198
+ /** Must start with the plugin's namespace. */
199
+ name: string;
200
+ title: string;
201
+ description: string;
202
+ input: S;
203
+ /** A kit few conversations need: the tool is left out of each request until the model loads the kit. Named like the tool. */
204
+ kit?: string;
205
+ execute(args: z.output<S>, invocation: ToolInvocation): unknown | Promise<unknown>;
206
+ }
207
+ export interface ToolCallInfo {
208
+ name: string;
209
+ args: unknown;
210
+ author: Author;
211
+ projectId?: string;
212
+ runId?: string;
213
+ }
214
+ export interface ToolsApi {
215
+ register<S extends z.ZodObject>(tool: ToolSpec<S>): Promise<Disposer>;
216
+ /** What a kit is for, in a few words the model reads before it loads the kit ("a focus timer"). */
217
+ kit(id: string, purpose: string): Promise<Disposer>;
218
+ list(): Promise<{
219
+ name: string;
220
+ title: string;
221
+ description: string;
222
+ inputSchema: Record<string, unknown>;
223
+ }[]>;
224
+ call(name: string, args: unknown, opts?: {
225
+ projectId?: string;
226
+ }): Promise<unknown>;
227
+ /**
228
+ * A policy over every tool call: throw to refuse it. SDK 0.2.12: or answer `{ args }` to go on with
229
+ * other arguments (checked against the tool's input again), or `{ result }` to answer in the tool's place.
230
+ */
231
+ gate(check: (call: ToolCallInfo) => ToolGateAnswer | void | Promise<ToolGateAnswer | void>): Promise<Disposer>;
232
+ }
233
+ /** SDK 0.2.12: what a tool gate may answer besides letting the call through unchanged. `result` wins over `args`. */
234
+ export type ToolGateAnswer = {
235
+ args: unknown;
236
+ } | {
237
+ result: unknown;
238
+ };
239
+ export type BadgeTone = "faint" | "neutral" | "strong";
240
+ export interface Badge {
241
+ id: string;
242
+ label: string;
243
+ tone?: BadgeTone;
244
+ title?: string;
245
+ }
246
+ export interface CommandScope {
247
+ projectId?: string;
248
+ boardId?: string;
249
+ viewId?: string;
250
+ itemId?: string | null;
251
+ }
252
+ export type SectionState = "live" | "waiting" | "idle" | "ended" | "lost" | "ok" | "warn";
253
+ export interface SectionRow {
254
+ label: string;
255
+ value: string;
256
+ icon?: string;
257
+ mono?: boolean;
258
+ state?: SectionState;
259
+ href?: string;
260
+ command?: string;
261
+ }
262
+ export interface SidebarRow {
263
+ id: string;
264
+ label: string;
265
+ icon?: string;
266
+ detail?: string;
267
+ count?: number | string;
268
+ command?: string;
269
+ href?: string;
270
+ }
271
+ export interface GraphEdge {
272
+ from: string;
273
+ to: string;
274
+ tone?: BadgeTone | "danger";
275
+ title?: string;
276
+ }
277
+ type Maybe<T> = T | Promise<T>;
278
+ /** Ids must equal the namespace or start with it followed by `.`, `-` or `_`. */
279
+ export interface ContributionsApi {
280
+ command(def: {
281
+ id: string;
282
+ title: Localized;
283
+ group?: Localized;
284
+ shortcut?: string;
285
+ run(scope: CommandScope): Maybe<{
286
+ toast?: string;
287
+ } | void>;
288
+ }): Promise<Disposer>;
289
+ badge(def: {
290
+ id: string;
291
+ compute(items: Item[], scope: {
292
+ projectId: string;
293
+ }): Maybe<Record<string, Badge[]>>;
294
+ }): Promise<Disposer>;
295
+ /**
296
+ * A status bar item. `command` (in the plugin's namespace) runs on click, or `menu` lists actions the click
297
+ * opens instead; `until` returns an epoch ms the renderer counts down to after the text, without polling.
298
+ * Everything is re-read when `refresh()` is called.
299
+ */
300
+ statusItem(def: {
301
+ id: string;
302
+ text(scope: {
303
+ projectId?: string;
304
+ }): Maybe<string | null>;
305
+ title?: Localized;
306
+ icon?: string;
307
+ align?: "start" | "end";
308
+ command?: string;
309
+ until?(scope: {
310
+ projectId?: string;
311
+ }): Maybe<number | null>;
312
+ menu?(scope: {
313
+ projectId?: string;
314
+ }): Maybe<{
315
+ title: string;
316
+ command: string;
317
+ icon?: string;
318
+ }[] | null>;
319
+ }): Promise<Disposer>;
320
+ /** Every window re-reads this plugin's contributions — for state that changed outside a command run. */
321
+ refresh(): Promise<void>;
322
+ section(def: {
323
+ id: string;
324
+ title: Localized;
325
+ render(item: Item): Maybe<{
326
+ rows?: SectionRow[];
327
+ text?: string;
328
+ } | null>;
329
+ }): Promise<Disposer>;
330
+ graph(def: {
331
+ id: string;
332
+ title: string;
333
+ icon?: string;
334
+ edits?: "blocks";
335
+ compute(items: Item[], scope: {
336
+ projectId: string;
337
+ }): Maybe<GraphEdge[]>;
338
+ }): Promise<Disposer>;
339
+ /** A module in the project sidebar. Without `rows` it is a single door; rows returning null hide it in that project. */
340
+ sidebarModule(def: {
341
+ id: string;
342
+ title: Localized;
343
+ icon?: string;
344
+ command?: string;
345
+ rows?(scope: {
346
+ projectId: string;
347
+ }): Maybe<SidebarRow[] | null>;
348
+ }): Promise<Disposer>;
349
+ }
350
+ export interface PlaybookSpec {
351
+ /** Must start with the namespace. */
352
+ id: string;
353
+ title: string;
354
+ /** One line the agent reads to decide when to open it. */
355
+ when: string;
356
+ body: string;
357
+ tools?: string[];
358
+ }
359
+ /** SDK 0.2.14: one block of a model message, as an in-app conversation sends it. */
360
+ export type ModelContent = {
361
+ type: "text";
362
+ text: string;
363
+ } | {
364
+ type: "media";
365
+ media: {
366
+ mime: string;
367
+ base64: string;
368
+ name: string;
369
+ };
370
+ } | {
371
+ type: "tool_call";
372
+ id: string;
373
+ name: string;
374
+ args: unknown;
375
+ } | {
376
+ type: "tool_result";
377
+ id: string;
378
+ content: string;
379
+ isError?: boolean;
380
+ } | {
381
+ type: "native";
382
+ provider: string;
383
+ data: unknown;
384
+ };
385
+ export interface ModelMessage {
386
+ role: "user" | "assistant";
387
+ content: ModelContent[];
388
+ }
389
+ /** A tool as the model is offered it; `inputSchema` is JSON Schema. */
390
+ export interface ModelTool {
391
+ name: string;
392
+ description: string;
393
+ inputSchema: Record<string, unknown>;
394
+ }
395
+ /** Which turn, step and model profile a request belongs to. */
396
+ export interface TurnInfo {
397
+ runId: string;
398
+ threadId: string;
399
+ projectId: string;
400
+ purpose: string;
401
+ profile: string;
402
+ step: number;
403
+ }
404
+ /** SDK 0.2.14: one model request an in-app conversation is about to send. A copy: what the hook answers is what changes. */
405
+ export interface ModelRequest {
406
+ system: string;
407
+ tools: ModelTool[];
408
+ messages: ModelMessage[];
409
+ turn: TurnInfo;
410
+ }
411
+ export type ModelRequestChange = Partial<Pick<ModelRequest, "system" | "tools" | "messages">>;
412
+ export interface ModelRequestFailure {
413
+ /** What the provider or the network said. */
414
+ message: string;
415
+ request: ModelRequest;
416
+ /** How many times this step was already sent again on a hook's answer. */
417
+ attempt: number;
418
+ }
419
+ /** Send the request again: changed, on another model profile (an id from the person's profiles), or both. */
420
+ export interface ModelRetry {
421
+ request?: ModelRequestChange;
422
+ profile?: string;
423
+ }
424
+ export interface TurnEnding {
425
+ /** What the model said last. */
426
+ text: string;
427
+ turn: TurnInfo;
428
+ /** How many times this turn was already kept going. */
429
+ continued: number;
430
+ }
431
+ /**
432
+ * The agent's in-app conversations. The three hooks (SDK 0.2.14) reach the
433
+ * conversation Tomrade runs itself (`engine-loop`); CLI agents — Claude Code,
434
+ * Codex, dsh and the rest — run their own loop and send nothing through them.
435
+ */
436
+ export interface AgentApi {
437
+ playbook(spec: PlaybookSpec): Promise<Disposer>;
438
+ /** Before each model request: answer the parts to send instead, or nothing to leave it as it is. */
439
+ request(hook: (request: ModelRequest) => Maybe<ModelRequestChange | void>): Promise<Disposer>;
440
+ /** A model request failed: answer what to try instead, or nothing to let the failure stand. The first plugin to answer decides. */
441
+ requestError(hook: (failure: ModelRequestFailure) => Maybe<ModelRetry | void>): Promise<Disposer>;
442
+ /** A turn is about to end: answer `{ continue }` with a message to the model to keep it going. Bounded, so it cannot run forever. */
443
+ turnEnding(hook: (ending: TurnEnding) => Maybe<{
444
+ continue: string;
445
+ } | void>): Promise<Disposer>;
446
+ /**
447
+ * SDK 0.2.15: a hook in the CLI agents Tomrade works with — Claude Code, Codex, dsh and the rest. It
448
+ * hears their sessions and, in one Tomrade started, may answer: see `CLI_HOOK_COVERAGE` for which.
449
+ */
450
+ cliHook(spec: CliHookSpec): Promise<Disposer>;
451
+ }
452
+ export type { CliHookEvent } from "./cli-hooks.js";
453
+ /** SDK 0.2.15: what a CLI agent's hook says happened, whichever agent sent it. */
454
+ export interface CliHookCall {
455
+ event: import("./cli-hooks.js").CliHookEvent;
456
+ /** `claude-code`, `codex`, `dsh`, `pi`, `workbuddy`, `cursor`, `opencode`, `hermes`… */
457
+ agent: string;
458
+ /** Whether an answer reaches the agent: a session Tomrade started, by an agent that acts on this event. */
459
+ answerable: boolean;
460
+ projectId: string;
461
+ /** Tomrade's code session the hook belongs to. */
462
+ sessionId: string;
463
+ /** The ticket the session works, when it has one. */
464
+ ticket: string | null;
465
+ cwd: string | null;
466
+ tool?: {
467
+ name: string;
468
+ input: unknown;
469
+ output?: string;
470
+ error?: string;
471
+ };
472
+ /** What the model said last, at `stop`. */
473
+ text?: string;
474
+ /** The hook's own payload, as the agent sent it. */
475
+ raw: Record<string, unknown>;
476
+ }
477
+ /** Refuse the tool (`pre-tool`), add context for the model (`session-start`, `prompt`, `post-tool`), or keep the turn going (`stop`). */
478
+ export interface CliHookAnswer {
479
+ deny?: string;
480
+ context?: string;
481
+ continue?: string;
482
+ }
483
+ export interface CliHookSpec {
484
+ /** The events it hears; all of them when left out. */
485
+ events?: import("./cli-hooks.js").CliHookEvent[];
486
+ /** A regular expression the tool's name must match, for `pre-tool` and `post-tool`. */
487
+ tools?: string;
488
+ /** The agents it hears; all of them when left out. */
489
+ agents?: string[];
490
+ handle(call: CliHookCall): Maybe<CliHookAnswer | void>;
491
+ }
492
+ export interface SurfaceSpec {
493
+ id: string;
494
+ title: string;
495
+ /**
496
+ * SDK 0.2.11: `"window"` is a working window — title bar, resizable, closable, in the Dock or
497
+ * taskbar, its bounds remembered — and needs `permissions.window`. The default `"widget"` is
498
+ * the fixed, frameless surface it always was. A window's title carries the plugin's own.
499
+ */
500
+ kind?: "widget" | "window";
501
+ /** SDK 0.2.11, windows only: how small the person may make it. */
502
+ minWidth?: number;
503
+ minHeight?: number;
504
+ /** A document inside the plugin's package, relative to its root. Remote pages are refused. */
505
+ document: string;
506
+ width: number;
507
+ height: number;
508
+ transparent?: boolean;
509
+ alwaysOnTop?: boolean;
510
+ /** Allow keyboard input; defaults to false for passive widgets. */
511
+ focusable?: boolean;
512
+ clickThrough?: boolean;
513
+ /** First placed on top of an app element by name — `"sidebar-foot"`: just above the sidebar's Project settings. */
514
+ anchor?: string;
515
+ initialState?: unknown;
516
+ /** Messages the document sends; a person is driving, so writes made here are theirs. */
517
+ onMessage?(message: unknown): Maybe<void>;
518
+ /** Once, when the window is gone — the person closed it, `close()` did, or the plugin unloaded. The handle's other calls then reject. */
519
+ onClose?(): Maybe<void>;
520
+ }
521
+ export interface SurfaceHandle {
522
+ readonly id: string;
523
+ setState(state: unknown): Promise<void>;
524
+ /** SDK 0.2.11: one message to the document's `tomradeSurface.onMessage`, for frequent small updates; `setState` replaces the whole state. */
525
+ post(message: unknown): Promise<void>;
526
+ setPosition(x: number, y: number): Promise<void>;
527
+ bounds(): Promise<{
528
+ x: number;
529
+ y: number;
530
+ width: number;
531
+ height: number;
532
+ }>;
533
+ workArea(): Promise<{
534
+ x: number;
535
+ y: number;
536
+ width: number;
537
+ height: number;
538
+ }>;
539
+ close(): Promise<void>;
540
+ }
541
+ export interface SurfacesApi {
542
+ open(spec: SurfaceSpec): Promise<SurfaceHandle>;
543
+ }
544
+ /** A sound the machine offers: `system:<name>`. A file the person chose is played by its absolute path. */
545
+ export interface AlertSound {
546
+ id: string;
547
+ title: string;
548
+ }
549
+ /** Alerts the system raises for the plugin. The plugin chooses when; it never reads the sound file itself. */
550
+ export interface AlertsApi {
551
+ /** The system sounds on this machine, the system's own alert sound first. */
552
+ sounds(): Promise<AlertSound[]>;
553
+ /** Plays a system sound or an audio file once; volume runs 0–1. */
554
+ play(sound: string, opts?: {
555
+ volume?: number;
556
+ }): Promise<void>;
557
+ /** A silent system notification; clicking it brings Tomrade forward. */
558
+ notify(notice: {
559
+ title: string;
560
+ body?: string;
561
+ }): Promise<void>;
562
+ }
563
+ export interface OplogApi {
564
+ recent(projectId: string, limit?: number): Promise<OplogEntry[]>;
565
+ history(projectId: string, entity: OplogEntity, id: string, limit?: number): Promise<OplogEntry[]>;
566
+ }
567
+ /** What landed, as the host tells every listener after the commit. */
568
+ export interface WriteEvent {
569
+ projectId: string;
570
+ entity: OplogEntity;
571
+ op: OplogOp;
572
+ id: string;
573
+ by: Author;
574
+ via: string | null;
575
+ runId: string | null;
576
+ }
577
+ export interface Events {
578
+ write: (event: WriteEvent) => Maybe<void>;
579
+ "tool.result": (call: ToolCallInfo, outcome: {
580
+ ok: boolean;
581
+ error?: string;
582
+ }) => Maybe<void>;
583
+ "project.opened": (projectId: string) => Maybe<void>;
584
+ "project.closed": (projectId: string) => Maybe<void>;
585
+ "settings.changed": () => Maybe<void>;
586
+ }
587
+ export interface PluginContext {
588
+ /** `publisher/name` for an extension; the row id for a built-in. */
589
+ readonly id: string;
590
+ readonly namespace: string;
591
+ readonly logger: Logger;
592
+ /** A directory only this plugin writes to, kept across updates; null for a built-in. */
593
+ readonly dataDir: string | null;
594
+ readonly items: ItemsApi;
595
+ readonly fields: FieldsApi;
596
+ readonly boards: BoardsApi;
597
+ readonly views: ViewsApi;
598
+ readonly projects: ProjectsApi;
599
+ readonly db: DbApi;
600
+ readonly settings: SettingsApi;
601
+ readonly tools: ToolsApi;
602
+ readonly contributions: ContributionsApi;
603
+ readonly agent: AgentApi;
604
+ readonly im: ImApi;
605
+ readonly surfaces: SurfacesApi;
606
+ readonly alerts: AlertsApi;
607
+ readonly oplog: OplogApi;
608
+ /** SDK 0.2.9: answers the agent's web search, as the app's own providers do. */
609
+ readonly webSearch: WebSearchApi;
610
+ /** SDK 0.2.10: live terminals of the herdr panes code sessions run in. */
611
+ readonly terminals: TerminalsApi;
612
+ /** SDK 0.2.16: model providers, as the app's own are. */
613
+ readonly models: ModelsApi;
614
+ on<K extends keyof Events>(event: K, listener: Events[K]): Disposer;
615
+ /** Runs `fn` while an optional service is present; what it returns is disposed when the service goes. */
616
+ when(name: ServiceName, fn: (ctx: PluginContext) => Maybe<Disposer | void>): Disposer;
617
+ /** Whether a service is present right now. */
618
+ has(name: ServiceName): boolean;
619
+ /** Cleanup that runs when the plugin unloads. */
620
+ effect(fn: () => Maybe<Disposer>): void;
621
+ /** A timer cleared with the plugin. */
622
+ interval(fn: () => Maybe<void>, ms: number): Disposer;
623
+ }
624
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=contract.js.map
@@ -0,0 +1,27 @@
1
+ import type { z } from "zod";
2
+ import type { PluginContext, ServiceName, ToolSpec } from "./contract.js";
3
+ /** Shared by every copy of the SDK, so a plugin that bundled one is still recognised. */
4
+ export declare const SDK_PLUGIN: unique symbol;
5
+ export interface PluginSpec<C = undefined> {
6
+ /** Loaded when every one is present; unloaded when one goes. */
7
+ inject?: ServiceName[];
8
+ /** Reached through `ctx.when` / `ctx.has`. */
9
+ optional?: ServiceName[];
10
+ /** Validates the row's config before `apply` runs. */
11
+ config?: z.ZodType<C>;
12
+ apply(ctx: PluginContext, config: C): void | Promise<void>;
13
+ }
14
+ export interface SdkPlugin<C = undefined> extends PluginSpec<C> {
15
+ readonly [SDK_PLUGIN]: 1;
16
+ }
17
+ export declare const definePlugin: <C = undefined>(spec: PluginSpec<C>) => SdkPlugin<C>;
18
+ export declare const isSdkPlugin: (value: unknown) => value is SdkPlugin<unknown>;
19
+ export declare const defineTool: <S extends z.ZodObject>(tool: ToolSpec<S>) => ToolSpec<S>;
20
+ export declare class ToolInputError extends Error {
21
+ name: string;
22
+ }
23
+ /** Tools scope to the caller's project; without one there is nothing safe to default to. */
24
+ export declare function requireProjectId(tool: string, given: string | undefined, invocation: {
25
+ projectId?: string;
26
+ }): string;
27
+ //# sourceMappingURL=define.d.ts.map
package/dist/define.js ADDED
@@ -0,0 +1,16 @@
1
+ /** Shared by every copy of the SDK, so a plugin that bundled one is still recognised. */
2
+ export const SDK_PLUGIN = Symbol.for("tomrade.sdk-plugin");
3
+ export const definePlugin = (spec) => Object.freeze({ ...spec, [SDK_PLUGIN]: 1 });
4
+ export const isSdkPlugin = (value) => typeof value === "object" && value !== null && value[SDK_PLUGIN] === 1 && typeof value.apply === "function";
5
+ export const defineTool = (tool) => tool;
6
+ export class ToolInputError extends Error {
7
+ name = "ToolInputError";
8
+ }
9
+ /** Tools scope to the caller's project; without one there is nothing safe to default to. */
10
+ export function requireProjectId(tool, given, invocation) {
11
+ const id = given ?? invocation.projectId;
12
+ if (!id)
13
+ throw new ToolInputError(`${tool}: projectId is required here — call list_projects and pass one`);
14
+ return id;
15
+ }
16
+ //# sourceMappingURL=define.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The contract each value the SDK exports first shipped in. A plugin imports
3
+ * the SDK from the Tomrade it runs in, not from its own bundle, so a name the
4
+ * oldest Tomrade its `engines.sdk` accepts does not have fails the plugin as it
5
+ * loads. A new export is listed here under the contract it ships in.
6
+ */
7
+ export declare const SDK_EXPORTS: Readonly<Record<string, string>>;
8
+ /** The oldest contract `range` accepts, up to this SDK's own; null when it accepts none of them. */
9
+ export declare function lowestContract(range: string): string | null;
10
+ //# sourceMappingURL=exports.d.ts.map