@tomradex/plugin-sdk 0.2.0 → 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,48 @@
1
+ import { z } from "zod";
2
+ import { type Dictionaries } from "@tomradex/domain";
3
+ import type { Locale } from "./contract.js";
4
+ export { languageCoverage, pickLanguage, slotsOf, type Dictionaries, type LanguageCoverage } from "@tomradex/domain";
5
+ /**
6
+ * A plugin's own words in the app's language (`ctx.settings.locale()`): the
7
+ * closest language it has, picked as Tomrade picks its own (zh-TW finds
8
+ * zh-Hant, never zh), and English when it has none of that language.
9
+ */
10
+ export declare function wordsFor<T>(locale: Locale, words: {
11
+ en: T;
12
+ } & Partial<Record<Locale, T>>): T;
13
+ /**
14
+ * A language pack: data only. The manifest names each language a plugin adds
15
+ * and the JSON file holding its words; nothing in it runs, and every string
16
+ * is shown as plain text in place of the English it translates.
17
+ */
18
+ /** The largest pack file Tomrade reads, and the longest single template. */
19
+ export declare const LANGUAGE_PACK_LIMITS: {
20
+ fileBytes: number;
21
+ templateChars: number;
22
+ languages: number;
23
+ };
24
+ export declare const LanguageEntrySchema: z.ZodObject<{
25
+ tag: z.ZodString;
26
+ name: z.ZodString;
27
+ file: z.ZodString;
28
+ }, z.core.$strip>;
29
+ export type LanguageEntry = z.output<typeof LanguageEntrySchema>;
30
+ /**
31
+ * What a pack file holds: `ui` translates the interface's dictionaries,
32
+ * `backend` the backend's books, each `namespace → key → template`, keyed as
33
+ * English is. A template names its values as `{{slot}}`, exactly the slots
34
+ * English's does; an entry that does not, or a key English lacks, reads English.
35
+ */
36
+ export declare const LanguagePackFileSchema: z.ZodObject<{
37
+ ui: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
38
+ backend: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
39
+ }, z.core.$strict>;
40
+ export interface LanguagePack {
41
+ tag: string;
42
+ name: string;
43
+ ui: Dictionaries;
44
+ backend: Dictionaries;
45
+ }
46
+ /** A pack file's text checked against the format; throws with the first problem. */
47
+ export declare function parseLanguagePack(entry: LanguageEntry, text: string): LanguagePack;
48
+ //# sourceMappingURL=language.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import { canonicalTag, pickLanguage } from "@tomradex/domain";
3
+ export { languageCoverage, pickLanguage, slotsOf } from "@tomradex/domain";
4
+ /**
5
+ * A plugin's own words in the app's language (`ctx.settings.locale()`): the
6
+ * closest language it has, picked as Tomrade picks its own (zh-TW finds
7
+ * zh-Hant, never zh), and English when it has none of that language.
8
+ */
9
+ export function wordsFor(locale, words) {
10
+ return words[pickLanguage(locale, Object.keys(words)) ?? "en"] ?? words.en;
11
+ }
12
+ /**
13
+ * A language pack: data only. The manifest names each language a plugin adds
14
+ * and the JSON file holding its words; nothing in it runs, and every string
15
+ * is shown as plain text in place of the English it translates.
16
+ */
17
+ /** The largest pack file Tomrade reads, and the longest single template. */
18
+ export const LANGUAGE_PACK_LIMITS = { fileBytes: 2 * 1024 * 1024, templateChars: 4_000, languages: 16 };
19
+ export const LanguageEntrySchema = z.object({
20
+ /** A BCP 47 tag: "pt-BR", "ko", "zh-Hant". */
21
+ tag: z
22
+ .string()
23
+ .refine((t) => canonicalTag(t) === t, "a canonical BCP 47 language tag such as \"pt-BR\" or \"ko\"")
24
+ .refine((t) => t !== "en", "English is what every pack translates; it cannot be replaced"),
25
+ /** The language's name in itself, as Settings → Language lists it: "Português (Brasil)". */
26
+ name: z.string().trim().min(1).max(60),
27
+ /** The pack's words, a JSON file inside the plugin: "languages/pt-BR.json". */
28
+ file: z
29
+ .string()
30
+ .regex(/^[A-Za-z0-9_-][A-Za-z0-9._/-]*\.json$/, "a relative path to a .json file inside the plugin")
31
+ .refine((f) => !f.split("/").includes(".."), "the file must stay inside the plugin")
32
+ });
33
+ const Words = z.record(z.string().min(1), z.record(z.string().min(1), z.string().max(LANGUAGE_PACK_LIMITS.templateChars)));
34
+ /**
35
+ * What a pack file holds: `ui` translates the interface's dictionaries,
36
+ * `backend` the backend's books, each `namespace → key → template`, keyed as
37
+ * English is. A template names its values as `{{slot}}`, exactly the slots
38
+ * English's does; an entry that does not, or a key English lacks, reads English.
39
+ */
40
+ export const LanguagePackFileSchema = z
41
+ .object({
42
+ ui: Words.default({}),
43
+ backend: Words.default({})
44
+ })
45
+ .strict();
46
+ /** A pack file's text checked against the format; throws with the first problem. */
47
+ export function parseLanguagePack(entry, text) {
48
+ let raw;
49
+ try {
50
+ raw = JSON.parse(text);
51
+ }
52
+ catch (error) {
53
+ throw new Error(`${entry.file} is not JSON: ${error instanceof Error ? error.message : String(error)}`);
54
+ }
55
+ const parsed = LanguagePackFileSchema.safeParse(raw);
56
+ if (!parsed.success) {
57
+ const issue = parsed.error.issues[0];
58
+ throw new Error(`${entry.file} ${issue.path.join(".") || "(root)"}: ${issue.message}`);
59
+ }
60
+ return { tag: entry.tag, name: entry.name, ui: parsed.data.ui, backend: parsed.data.backend };
61
+ }
62
+ //# sourceMappingURL=language.js.map
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  /** The contract this SDK describes. A plugin's `engines.sdk` range is checked against the host's. */
3
- export declare const SDK_CONTRACT = "0.2.0";
3
+ export declare const SDK_CONTRACT = "0.2.16";
4
4
  export declare const PLUGIN_ID: RegExp;
5
5
  export declare const NAMESPACE: RegExp;
6
6
  /** The app's own names, and the official publisher's. */
@@ -9,12 +9,20 @@ export declare const OFFICIAL_PUBLISHER = "tomrade";
9
9
  export declare const PermissionsSchema: z.ZodDefault<z.ZodObject<{
10
10
  network: z.ZodDefault<z.ZodArray<z.ZodString>>;
11
11
  fs: z.ZodDefault<z.ZodEnum<{
12
+ none: "none";
12
13
  project: "project";
13
- plugin: "plugin";
14
14
  any: "any";
15
- none: "none";
15
+ plugin: "plugin";
16
16
  }>>;
17
17
  exec: z.ZodDefault<z.ZodBoolean>;
18
+ window: z.ZodDefault<z.ZodBoolean>;
19
+ why: z.ZodOptional<z.ZodObject<{
20
+ network: z.ZodOptional<z.ZodString>;
21
+ fs: z.ZodOptional<z.ZodString>;
22
+ exec: z.ZodOptional<z.ZodString>;
23
+ window: z.ZodOptional<z.ZodString>;
24
+ full: z.ZodOptional<z.ZodString>;
25
+ }, z.core.$strip>>;
18
26
  }, z.core.$strip>>;
19
27
  export declare const ContributesSchema: z.ZodObject<{
20
28
  tools: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
@@ -23,11 +31,30 @@ export declare const ContributesSchema: z.ZodObject<{
23
31
  statusItems: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
24
32
  sections: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
25
33
  graphs: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
34
+ sidebarModules: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
26
35
  playbooks: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
27
36
  settings: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
28
37
  surfaces: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
29
38
  tables: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
39
+ imAdapters: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
40
+ webSearchProviders: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
41
+ agentHooks: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
42
+ modelProviders: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
43
+ }, z.core.$strip>;
44
+ /**
45
+ * One row a full-mode plugin lays over Tomrade's composition, merged by id like a
46
+ * `tomrade.patch.yml` row: an app row's id switches it off, replaces its plugin or
47
+ * changes its config; a new id (carrying the namespace) adds a row. A relative
48
+ * `name` is a file inside the package.
49
+ */
50
+ export declare const RowSchema: z.ZodObject<{
51
+ id: z.ZodString;
52
+ name: z.ZodOptional<z.ZodString>;
53
+ title: z.ZodOptional<z.ZodString>;
54
+ config: z.ZodOptional<z.ZodUnknown>;
55
+ disabled: z.ZodOptional<z.ZodBoolean>;
30
56
  }, z.core.$strip>;
57
+ export type ManifestRow = z.output<typeof RowSchema>;
31
58
  export declare const ManifestSchema: z.ZodObject<{
32
59
  id: z.ZodString;
33
60
  namespace: z.ZodOptional<z.ZodString>;
@@ -43,18 +70,43 @@ export declare const ManifestSchema: z.ZodObject<{
43
70
  }, z.core.$strip>>;
44
71
  engines: z.ZodObject<{
45
72
  sdk: z.ZodString;
73
+ tomrade: z.ZodOptional<z.ZodString>;
46
74
  }, z.core.$strip>;
47
- main: z.ZodDefault<z.ZodString>;
75
+ context: z.ZodDefault<z.ZodEnum<{
76
+ full: "full";
77
+ sdk: "sdk";
78
+ }>>;
79
+ rows: z.ZodDefault<z.ZodArray<z.ZodObject<{
80
+ id: z.ZodString;
81
+ name: z.ZodOptional<z.ZodString>;
82
+ title: z.ZodOptional<z.ZodString>;
83
+ config: z.ZodOptional<z.ZodUnknown>;
84
+ disabled: z.ZodOptional<z.ZodBoolean>;
85
+ }, z.core.$strip>>>;
86
+ main: z.ZodDefault<z.ZodNullable<z.ZodString>>;
48
87
  icon: z.ZodOptional<z.ZodString>;
88
+ languages: z.ZodDefault<z.ZodArray<z.ZodObject<{
89
+ tag: z.ZodString;
90
+ name: z.ZodString;
91
+ file: z.ZodString;
92
+ }, z.core.$strip>>>;
49
93
  permissions: z.ZodDefault<z.ZodObject<{
50
94
  network: z.ZodDefault<z.ZodArray<z.ZodString>>;
51
95
  fs: z.ZodDefault<z.ZodEnum<{
96
+ none: "none";
52
97
  project: "project";
53
- plugin: "plugin";
54
98
  any: "any";
55
- none: "none";
99
+ plugin: "plugin";
56
100
  }>>;
57
101
  exec: z.ZodDefault<z.ZodBoolean>;
102
+ window: z.ZodDefault<z.ZodBoolean>;
103
+ why: z.ZodOptional<z.ZodObject<{
104
+ network: z.ZodOptional<z.ZodString>;
105
+ fs: z.ZodOptional<z.ZodString>;
106
+ exec: z.ZodOptional<z.ZodString>;
107
+ window: z.ZodOptional<z.ZodString>;
108
+ full: z.ZodOptional<z.ZodString>;
109
+ }, z.core.$strip>>;
58
110
  }, z.core.$strip>>;
59
111
  inject: z.ZodDefault<z.ZodArray<z.ZodEnum<{
60
112
  items: "items";
@@ -68,7 +120,12 @@ export declare const ManifestSchema: z.ZodObject<{
68
120
  contributions: "contributions";
69
121
  agent: "agent";
70
122
  surfaces: "surfaces";
123
+ alerts: "alerts";
71
124
  oplog: "oplog";
125
+ im: "im";
126
+ webSearch: "webSearch";
127
+ terminals: "terminals";
128
+ models: "models";
72
129
  }>>>;
73
130
  optional: z.ZodDefault<z.ZodArray<z.ZodEnum<{
74
131
  items: "items";
@@ -82,7 +139,12 @@ export declare const ManifestSchema: z.ZodObject<{
82
139
  contributions: "contributions";
83
140
  agent: "agent";
84
141
  surfaces: "surfaces";
142
+ alerts: "alerts";
85
143
  oplog: "oplog";
144
+ im: "im";
145
+ webSearch: "webSearch";
146
+ terminals: "terminals";
147
+ models: "models";
86
148
  }>>>;
87
149
  contributes: z.ZodDefault<z.ZodObject<{
88
150
  tools: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
@@ -91,10 +153,15 @@ export declare const ManifestSchema: z.ZodObject<{
91
153
  statusItems: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
92
154
  sections: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
93
155
  graphs: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
156
+ sidebarModules: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
94
157
  playbooks: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
95
158
  settings: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
96
159
  surfaces: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
97
160
  tables: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
161
+ imAdapters: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
162
+ webSearchProviders: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
163
+ agentHooks: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
164
+ modelProviders: z.ZodOptional<z.ZodDefault<z.ZodArray<z.ZodString>>>;
98
165
  }, z.core.$strip>>;
99
166
  authoredBy: z.ZodDefault<z.ZodEnum<{
100
167
  agent: "agent";
@@ -120,14 +187,15 @@ export declare function assertOwned(namespace: string, id: string, kind: string)
120
187
  export type RiskLevel = "low" | "elevated" | "high";
121
188
  export interface Risk {
122
189
  level: RiskLevel;
123
- /** Stable keys the UI words: `exec`, `fs-any`, `network-any`, `network`, `fs-project`, `db`. */
190
+ /** Stable keys the UI words: `full`, `rows`, `exec`, `fs-any`, `network-any`, `network`, `fs-project`, `db`, `models`, `window`. */
124
191
  reasons: string[];
125
192
  }
126
193
  /**
127
- * High risk is labelled, not refused: running programs, reading anywhere on
128
- * disk, or talking to any host. Elevated is what a person should still see.
194
+ * High risk is labelled, not refused: running with Tomrade's whole context or
195
+ * changing its own rows, running programs, reading anywhere on disk, or talking
196
+ * to any host. Elevated is what a person should still see.
129
197
  */
130
- export declare function riskOf(manifest: Pick<Manifest, "permissions" | "inject" | "optional">): Risk;
198
+ export declare function riskOf(manifest: Pick<Manifest, "permissions" | "inject" | "optional"> & Partial<Pick<Manifest, "context" | "rows">>): Risk;
131
199
  /** OSI-approved licenses the store accepts without a second look. */
132
200
  export declare const OSI_LICENSES: Set<string>;
133
201
  export declare const isOpenSource: (license: string) => boolean;
package/dist/manifest.js CHANGED
@@ -1,16 +1,17 @@
1
1
  import { z } from "zod";
2
+ import { LANGUAGE_PACK_LIMITS, LanguageEntrySchema } from "./language.js";
2
3
  import { isRange, isVersion } from "./semver.js";
3
4
  /** The contract this SDK describes. A plugin's `engines.sdk` range is checked against the host's. */
4
- export const SDK_CONTRACT = "0.2.0";
5
+ export const SDK_CONTRACT = "0.2.16";
5
6
  export const PLUGIN_ID = /^[a-z0-9][a-z0-9-]{0,38}\/[a-z0-9][a-z0-9-]{0,63}$/;
6
7
  export const NAMESPACE = /^[a-z][a-z0-9_]{1,31}$/;
7
8
  /** The app's own names, and the official publisher's. */
8
9
  export const RESERVED_NAMESPACES = new Set([
9
- "agent", "archify", "boards", "code_link", "contributions", "core", "db", "deps", "deps_graph", "fields", "files", "hub", "ipc", "items",
10
- "llm", "mcp", "oplog", "pet", "plugin", "plugins", "projects", "repos", "settings", "storage", "surfaces", "system", "tomrade", "tools", "views", "write"
10
+ "agent", "alerts", "archify", "boards", "code_link", "contributions", "core", "db", "deps", "deps_graph", "fields", "files", "hub", "im", "ipc", "items",
11
+ "llm", "mcp", "oplog", "pet", "plugin", "plugins", "pomodoro", "projects", "repos", "settings", "storage", "surfaces", "system", "tomrade", "tools", "views", "write"
11
12
  ]);
12
13
  export const OFFICIAL_PUBLISHER = "tomrade";
13
- const SERVICE = z.enum(["items", "fields", "boards", "views", "projects", "db", "settings", "tools", "contributions", "agent", "surfaces", "oplog"]);
14
+ const SERVICE = z.enum(["items", "fields", "boards", "views", "projects", "db", "settings", "tools", "contributions", "agent", "surfaces", "alerts", "oplog", "im", "webSearch", "terminals", "models"]);
14
15
  export const PermissionsSchema = z
15
16
  .object({
16
17
  /** Hosts the plugin talks to; `*` means any. */
@@ -18,9 +19,13 @@ export const PermissionsSchema = z
18
19
  /** `plugin`: its own data directory. `project`: the project folders. `any`: anywhere on disk. */
19
20
  fs: z.enum(["none", "plugin", "project", "any"]).default("none"),
20
21
  /** Starts other programs. */
21
- exec: z.boolean().default(false)
22
+ exec: z.boolean().default(false),
23
+ /** SDK 0.2.11: opens working windows of its own (a surface of kind "window"), which take keys like any app window. */
24
+ window: z.boolean().default(false),
25
+ /** Why the plugin needs a wide permission, in a sentence a person installing it can judge. Official plugins must say. */
26
+ why: z.object({ network: z.string().min(1), fs: z.string().min(1), exec: z.string().min(1), window: z.string().min(1), full: z.string().min(1) }).partial().optional()
22
27
  })
23
- .default({ network: [], fs: "none", exec: false });
28
+ .default({ network: [], fs: "none", exec: false, window: false });
24
29
  export const ContributesSchema = z
25
30
  .object({
26
31
  tools: z.array(z.string()).default([]),
@@ -29,12 +34,34 @@ export const ContributesSchema = z
29
34
  statusItems: z.array(z.string()).default([]),
30
35
  sections: z.array(z.string()).default([]),
31
36
  graphs: z.array(z.string()).default([]),
37
+ sidebarModules: z.array(z.string()).default([]),
32
38
  playbooks: z.array(z.string()).default([]),
33
39
  settings: z.array(z.string()).default([]),
34
40
  surfaces: z.array(z.string()).default([]),
35
- tables: z.array(z.string()).default([])
41
+ tables: z.array(z.string()).default([]),
42
+ imAdapters: z.array(z.string()).default([]),
43
+ webSearchProviders: z.array(z.string()).default([]),
44
+ /** SDK 0.2.15: the CLI agent hook events it hears. */
45
+ agentHooks: z.array(z.string()).default([]),
46
+ /** SDK 0.2.16: the model providers it registers. */
47
+ modelProviders: z.array(z.string()).default([])
36
48
  })
37
49
  .partial();
50
+ /**
51
+ * One row a full-mode plugin lays over Tomrade's composition, merged by id like a
52
+ * `tomrade.patch.yml` row: an app row's id switches it off, replaces its plugin or
53
+ * changes its config; a new id (carrying the namespace) adds a row. A relative
54
+ * `name` is a file inside the package.
55
+ */
56
+ export const RowSchema = z
57
+ .object({
58
+ id: z.string().min(1),
59
+ name: z.string().min(1).optional(),
60
+ title: z.string().min(1).optional(),
61
+ config: z.unknown().optional(),
62
+ disabled: z.boolean().optional()
63
+ })
64
+ .refine((row) => row.name !== undefined || row.config !== undefined || row.disabled !== undefined, "a row changes something: name, config or disabled");
38
65
  export const ManifestSchema = z.object({
39
66
  id: z.string().regex(PLUGIN_ID, "publisher/name, lowercase letters, digits and dashes"),
40
67
  namespace: z.string().regex(NAMESPACE, "lowercase letters, digits and underscores, 2–32 long, starting with a letter").optional(),
@@ -46,9 +73,23 @@ export const ManifestSchema = z.object({
46
73
  repository: z.url().optional(),
47
74
  homepage: z.url().optional(),
48
75
  author: z.object({ name: z.string().min(1), url: z.url().optional() }).optional(),
49
- engines: z.object({ sdk: z.string().refine(isRange, "a version range such as ^0.2.0") }),
50
- main: z.string().default("dist/index.js"),
76
+ engines: z.object({
77
+ sdk: z.string().refine(isRange, "a version range such as ^0.2.0"),
78
+ /** SDK 0.2.13: the Tomrade versions a full-mode plugin works with; it reaches internals the SDK range does not cover. */
79
+ tomrade: z.string().refine(isRange, "a version range such as >=0.13.0 <0.14.0").optional()
80
+ }),
81
+ /**
82
+ * SDK 0.2.13: what `main` receives. `"sdk"` is a `definePlugin` plugin behind the async facade;
83
+ * `"full"` is a plain cordis plugin given Tomrade's whole Context, as the app's own modules are.
84
+ */
85
+ context: z.enum(["sdk", "full"]).default("sdk"),
86
+ /** SDK 0.2.13, full mode only: rows laid over Tomrade's own, as `tomrade.patch.yml` rows are. */
87
+ rows: z.array(RowSchema).default([]),
88
+ /** The built code; `null` for a plugin that is data only, such as a language pack. */
89
+ main: z.string().nullable().default("dist/index.js"),
51
90
  icon: z.string().optional(),
91
+ /** Languages the plugin adds, each a JSON file of words (SDK 0.2.8). */
92
+ languages: z.array(LanguageEntrySchema).max(LANGUAGE_PACK_LIMITS.languages).default([]),
52
93
  permissions: PermissionsSchema,
53
94
  /** Stamped by `tomrade-plugin build` from the exported plugin. */
54
95
  inject: z.array(SERVICE).default([]),
@@ -66,6 +107,26 @@ export function parseManifest(raw) {
66
107
  throw new Error(`${manifest.id}: the name part does not make a valid namespace ("${ns}"); set "namespace" in ${MANIFEST_FILE}`);
67
108
  if (RESERVED_NAMESPACES.has(ns) && publisherOf(manifest.id) !== OFFICIAL_PUBLISHER)
68
109
  throw new Error(`${manifest.id}: namespace "${ns}" is reserved`);
110
+ if (manifest.main === null && !manifest.languages.length)
111
+ throw new Error(`${manifest.id}: "main" is null, so the plugin has no code; it must add a language under "languages"`);
112
+ if (manifest.context === "full") {
113
+ if (manifest.main === null)
114
+ throw new Error(`${manifest.id}: a full-mode plugin needs "main", the cordis plugin it runs`);
115
+ if (!manifest.engines.tomrade)
116
+ throw new Error(`${manifest.id}: a full-mode plugin names the Tomrade versions it works with in "engines.tomrade"`);
117
+ if (manifest.languages.length)
118
+ throw new Error(`${manifest.id}: a full-mode plugin adds its languages from its own code (ctx.settings.addLanguage), not under "languages"`);
119
+ }
120
+ else if (manifest.rows.length)
121
+ throw new Error(`${manifest.id}: "rows" change Tomrade's own composition, which takes "context": "full"`);
122
+ const ids = manifest.rows.map((r) => r.id);
123
+ const repeated = ids.find((id, i) => ids.indexOf(id) !== i);
124
+ if (repeated)
125
+ throw new Error(`${manifest.id}: the row "${repeated}" is listed twice`);
126
+ const tags = manifest.languages.map((l) => l.tag);
127
+ const twice = tags.find((t, i) => tags.indexOf(t) !== i);
128
+ if (twice)
129
+ throw new Error(`${manifest.id}: the language "${twice}" is listed twice`);
69
130
  return manifest;
70
131
  }
71
132
  export const publisherOf = (id) => id.split("/")[0];
@@ -78,16 +139,22 @@ export function assertOwned(namespace, id, kind) {
78
139
  throw new Error(`${kind} "${id}" must be named "${namespace}" or start with "${namespace}." "${namespace}-" or "${namespace}_"`);
79
140
  }
80
141
  /**
81
- * High risk is labelled, not refused: running programs, reading anywhere on
82
- * disk, or talking to any host. Elevated is what a person should still see.
142
+ * High risk is labelled, not refused: running with Tomrade's whole context or
143
+ * changing its own rows, running programs, reading anywhere on disk, or talking
144
+ * to any host. Elevated is what a person should still see.
83
145
  */
84
146
  export function riskOf(manifest) {
85
147
  const p = manifest.permissions;
86
- const high = [p.exec && "exec", p.fs === "any" && "fs-any", p.network.includes("*") && "network-any"].filter(Boolean);
148
+ const full = manifest.context === "full";
149
+ const high = [full && "full", full && manifest.rows?.length && "rows", p.exec && "exec", p.fs === "any" && "fs-any", p.network.includes("*") && "network-any"].filter(Boolean);
87
150
  const elevated = [
88
151
  p.network.length && !p.network.includes("*") && "network",
89
152
  p.fs === "project" && "fs-project",
90
- [...manifest.inject, ...manifest.optional].includes("db") && "db"
153
+ [...manifest.inject, ...manifest.optional].includes("db") && "db",
154
+ // A model provider reads the conversations of the profiles on it, and their keys.
155
+ [...manifest.inject, ...manifest.optional].includes("models") && "models",
156
+ // A window that takes keys could pass itself off as another app's: worth a look before installing.
157
+ p.window && "window"
91
158
  ].filter(Boolean);
92
159
  return { level: high.length ? "high" : elevated.length ? "elevated" : "low", reasons: [...high, ...elevated] };
93
160
  }
@@ -0,0 +1,219 @@
1
+ import type { Disposer, Localized, ModelMessage, ModelTool } from "./contract.js";
2
+ type Maybe<T> = T | Promise<T>;
3
+ /** What a model reads besides text. */
4
+ export type ModelInput = "image" | "audio" | "video" | "pdf";
5
+ /** What a model can answer with besides text. */
6
+ export type ModelOutput = "image" | "audio";
7
+ /** SDK 0.2.16. One assistant turn a profile on the provider is asked for. */
8
+ export interface ModelChatRequest {
9
+ system: string;
10
+ messages: ModelMessage[];
11
+ tools?: ModelTool[];
12
+ toolChoice?: "auto" | "none" | {
13
+ name: string;
14
+ };
15
+ maxTokens?: number;
16
+ temperature?: number;
17
+ /** Reasoning effort ("low", "medium", "high"…) where the model has the knob; ignore it otherwise. */
18
+ effort?: string;
19
+ /** Answer with a single JSON document. */
20
+ json?: boolean;
21
+ /** Search the web yourself; asked only of a model whose capabilities say `nativeWebSearch`. */
22
+ webSearch?: boolean;
23
+ /** Media the reply should carry; asked only of a model whose capabilities list it in `mediaOutput`. */
24
+ mediaOutput?: ModelOutput[];
25
+ }
26
+ /** What a turn streams, in order, ending with `stop`. A `native` block comes back in later requests as the provider's own record (server-side tool calls, signatures). */
27
+ export type ModelChatEvent = {
28
+ type: "text";
29
+ delta: string;
30
+ } | {
31
+ type: "media";
32
+ media: {
33
+ mime: string;
34
+ base64: string;
35
+ name: string;
36
+ };
37
+ } | {
38
+ type: "tool_call";
39
+ id: string;
40
+ name: string;
41
+ args: unknown;
42
+ } | {
43
+ type: "native";
44
+ provider: string;
45
+ data: unknown;
46
+ }
47
+ /** `inputTokens` counts every prompt token read, cached ones included. */
48
+ | {
49
+ type: "usage";
50
+ inputTokens?: number;
51
+ outputTokens?: number;
52
+ cacheReadTokens?: number;
53
+ cacheWriteTokens?: number;
54
+ }
55
+ /** The request failed before anything streamed and is sent again after `delayMs`. */
56
+ | {
57
+ type: "retry";
58
+ attempt: number;
59
+ max: number;
60
+ reason: string;
61
+ delayMs: number;
62
+ } | {
63
+ type: "stop";
64
+ reason: "end" | "tool_use" | "max_tokens" | "refusal";
65
+ };
66
+ export interface ModelChatCapabilities {
67
+ /** MIME types it reads (`image/png`, `application/pdf`…). */
68
+ mediaInput?: string[];
69
+ mediaOutput?: string[];
70
+ tools: boolean;
71
+ streaming: boolean;
72
+ json: boolean;
73
+ /** It searches the web on its own when a request sets `webSearch`. */
74
+ nativeWebSearch?: boolean;
75
+ }
76
+ /** One model as Tomrade's conversations, worker and tools talk to it. Throw with what the provider said when a request fails. */
77
+ export interface ModelChat {
78
+ capabilities: ModelChatCapabilities;
79
+ chat(request: ModelChatRequest, signal?: AbortSignal): AsyncIterable<ModelChatEvent>;
80
+ }
81
+ /** A saved model profile naming the provider. */
82
+ export interface ModelProfileInfo {
83
+ id: string;
84
+ title: string;
85
+ provider: string;
86
+ /** Absent: the provider's default address. */
87
+ baseURL?: string;
88
+ apiKey: string;
89
+ model: string;
90
+ inputModalities?: ModelInput[];
91
+ outputModalities?: ModelOutput[];
92
+ }
93
+ /** A well-known service the provider serves, offered in the model setup. */
94
+ export interface ModelPresetSpec {
95
+ /** Namespaced like every id a plugin registers. */
96
+ id: string;
97
+ title: string;
98
+ /** Absent: the provider's own address. */
99
+ baseURL?: string;
100
+ /** The model to start with; otherwise the newest listed one matching `prefer`, then the first listed. */
101
+ model?: string;
102
+ prefer?: RegExp[];
103
+ /** Where people get a key. */
104
+ keyPage?: string;
105
+ /** The variable people export the key as, which Tomrade looks for on this computer. */
106
+ env?: string;
107
+ /** What its keys look like, so a pasted key picks this preset. */
108
+ keyShapes?: RegExp[];
109
+ /** Runs on this computer and needs no key. */
110
+ local?: boolean;
111
+ /** One line under the preset in the setup. */
112
+ hint?: Localized;
113
+ }
114
+ export interface ModelMedia {
115
+ inputModalities: ModelInput[];
116
+ outputModalities: ModelOutput[];
117
+ }
118
+ /** The models an endpoint lists (null: it has no listing), or why the key or address does not work. */
119
+ export type ModelListing = {
120
+ ok: true;
121
+ models: string[] | null;
122
+ capabilities?: Record<string, ModelMedia>;
123
+ } | {
124
+ ok: false;
125
+ status?: number;
126
+ detail: string;
127
+ };
128
+ /**
129
+ * A sign-in the provider allows third-party apps instead of pasting a key: an
130
+ * OAuth flow that ends in a key (OpenRouter's PKCE), or credentials already on
131
+ * this computer. Never a consumer subscription its terms keep to the vendor's own apps.
132
+ */
133
+ export interface ModelSignIn {
134
+ /** The button, such as "Sign in with OpenRouter". */
135
+ title: Localized;
136
+ /** Runs the sign-in; `open` shows a page in the person's browser. What it answers is kept as the profile's key. */
137
+ run(helpers: {
138
+ open(url: string): void;
139
+ signal: AbortSignal;
140
+ }): Promise<{
141
+ apiKey: string;
142
+ }>;
143
+ }
144
+ /** SDK 0.2.16. One way to talk to models, as the app's own providers are: what a profile's `provider` names. */
145
+ export interface ModelProviderSpec {
146
+ /** Namespaced like every id a plugin registers; profiles store it. */
147
+ id: string;
148
+ title: Localized;
149
+ /** False for an endpoint that works without a key, such as a local one. Default true. */
150
+ keyRequired?: boolean;
151
+ /** Where a profile without its own address talks; `editable: false` leaves the address out of the form. */
152
+ baseURL: {
153
+ default: string;
154
+ editable?: boolean;
155
+ };
156
+ /** False for an endpoint that only makes images. Default true. */
157
+ chat?: boolean;
158
+ presets?: ModelPresetSpec[];
159
+ /** Doubles as the key check when a profile is saved. */
160
+ list(endpoint: {
161
+ baseURL?: string;
162
+ apiKey: string;
163
+ }, signal: AbortSignal): Maybe<ModelListing>;
164
+ /** Of what the endpoint lists, the models a profile can use, best first. Default: all of them, as listed. */
165
+ usable?(listed: string[]): string[];
166
+ /** What its models read besides text when a profile stores nothing. */
167
+ defaultInput?: ModelInput[];
168
+ /** A model's media: what its listing entry says, else what the provider knows; undefined keeps a profile's stored values. */
169
+ capabilities?(model: string, listed?: ModelMedia): ModelMedia | undefined;
170
+ signIn?: ModelSignIn;
171
+ /** Called synchronously when a profile on it is first used, and again once the profile changes. */
172
+ build(profile: ModelProfileInfo): ModelChat;
173
+ }
174
+ /** A model on the app's own OpenAI-compatible adapter. */
175
+ export interface CompatibleModelOptions {
176
+ baseURL: string;
177
+ apiKey: string;
178
+ model: string;
179
+ inputModalities?: ModelInput[];
180
+ outputModalities?: ModelOutput[];
181
+ /**
182
+ * The model searches the web on its own when a request asks: `"chat"` on chat/completions, with
183
+ * the vendor's own flag set by `body`; `"responses"` on the Responses API. Absent, it does not.
184
+ */
185
+ webSearch?: "chat" | "responses";
186
+ /** The last word on each chat/completions body (a copy of the request beside it): a vendor's own fields. Throw to fail the request. */
187
+ body?(json: Record<string, unknown>, request: ModelChatRequest): Record<string, unknown>;
188
+ /** The last word on each Responses body, where a searching request goes with `webSearch: "responses"`: the vendor's effort scale, its limits. Throw to fail the request. */
189
+ responses?(json: Record<string, unknown>, request: ModelChatRequest): Record<string, unknown>;
190
+ /**
191
+ * The headers of each request in place of `Authorization: Bearer <apiKey>`: a vendor's own key
192
+ * header, a token that expires, a signature over `url` and the exact `body` sent. Asked again
193
+ * before every try, retries included. Throw to fail the request.
194
+ */
195
+ headers?(request: {
196
+ url: string;
197
+ body: string;
198
+ }): Maybe<Record<string, string>>;
199
+ }
200
+ export interface ModelsApi {
201
+ /** Serves the profiles naming `provider.id` and offers its presets until disposed or the plugin unloads. */
202
+ registerProvider(provider: ModelProviderSpec): Promise<Disposer>;
203
+ /**
204
+ * Synchronous, like `build`: a model on the app's own OpenAI-compatible adapter — streaming, tool
205
+ * calls, retries, media, the token-field quirks — for a vendor that serves chat/completions.
206
+ */
207
+ compatible(options: CompatibleModelOptions): ModelChat;
208
+ /**
209
+ * The app's own listing of a chat/completions endpoint (`GET /models`), media read from it; one
210
+ * without a listing answers `models: null`. `headers` go in place of the bearer key.
211
+ */
212
+ listCompatible(endpoint: {
213
+ baseURL: string;
214
+ apiKey: string;
215
+ headers?: Record<string, string>;
216
+ }, signal?: AbortSignal): Promise<ModelListing>;
217
+ }
218
+ export {};
219
+ //# sourceMappingURL=models.d.ts.map
package/dist/models.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=models.js.map