@nyte-ai/host 0.0.0-stage → 0.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/THIRD-PARTY-NOTICES.md +866 -0
  4. package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
  5. package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
  6. package/dist/chunks/canvas-preview-11nadadc.js +241 -0
  7. package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
  8. package/dist/chunks/canvas-preview-2jwbe14k.js +130 -0
  9. package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
  10. package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
  11. package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
  12. package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
  13. package/dist/chunks/canvas-preview-518dh8by.js +169 -0
  14. package/dist/chunks/canvas-preview-55s0y68g.js +387 -0
  15. package/dist/chunks/canvas-preview-5ekjx6sw.js +613 -0
  16. package/dist/chunks/canvas-preview-5mex725c.js +14 -0
  17. package/dist/chunks/canvas-preview-5z63am63.js +295 -0
  18. package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
  19. package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
  20. package/dist/chunks/canvas-preview-8xc0vn7v.js +458 -0
  21. package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
  22. package/dist/chunks/canvas-preview-af86jz8t.js +141 -0
  23. package/dist/chunks/canvas-preview-aq5j4p25.js +240 -0
  24. package/dist/chunks/canvas-preview-b2v8wfnz.js +36 -0
  25. package/dist/chunks/canvas-preview-befkjea2.js +60 -0
  26. package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
  27. package/dist/chunks/canvas-preview-cawy6vbz.js +352 -0
  28. package/dist/chunks/canvas-preview-crmzxxt8.js +518 -0
  29. package/dist/chunks/canvas-preview-drctegz4.js +591 -0
  30. package/dist/chunks/canvas-preview-e3vnkkc5.js +134 -0
  31. package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
  32. package/dist/chunks/canvas-preview-fxa0rfb0.js +61 -0
  33. package/dist/chunks/canvas-preview-g5fkjs34.js +598 -0
  34. package/dist/chunks/canvas-preview-jeddjex8.js +166 -0
  35. package/dist/chunks/canvas-preview-k1yg4fgp.js +571 -0
  36. package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
  37. package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
  38. package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
  39. package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
  40. package/dist/chunks/canvas-preview-n2a35zv5.js +81 -0
  41. package/dist/chunks/canvas-preview-pkzqg96v.js +63 -0
  42. package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
  43. package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
  44. package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
  45. package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
  46. package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
  47. package/dist/chunks/canvas-preview-tnqe7k0b.js +374 -0
  48. package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
  49. package/dist/chunks/canvas-preview-vm9zdp2t.js +398 -0
  50. package/dist/chunks/canvas-preview-w54ymtb3.js +272 -0
  51. package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
  52. package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
  53. package/dist/chunks/canvas-preview-ycepb05s.js +176 -0
  54. package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
  55. package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
  56. package/dist/src/canvas-preview.d.ts +29 -0
  57. package/dist/src/canvas-preview.js +271 -0
  58. package/dist/src/credentials.d.ts +44 -0
  59. package/dist/src/credentials.js +10 -0
  60. package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
  61. package/dist/src/deploy/cloudflare/worker.js +196 -0
  62. package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
  63. package/dist/src/deploy/vercel/nitro.config.js +30 -0
  64. package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
  65. package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
  66. package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
  67. package/dist/src/deploy/vercel/server/utils/host.js +7 -0
  68. package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
  69. package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
  70. package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
  71. package/dist/src/deploy/vercel/server/utils/step.js +6 -0
  72. package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
  73. package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
  74. package/dist/src/durable-object.d.ts +14 -0
  75. package/dist/src/durable-object.js +231 -0
  76. package/dist/src/hash.d.ts +1 -0
  77. package/dist/src/hash.js +6 -0
  78. package/dist/src/identity-key.d.ts +11 -0
  79. package/dist/src/identity-key.js +9 -0
  80. package/dist/src/identity.d.ts +12 -0
  81. package/dist/src/identity.js +6 -0
  82. package/dist/src/images/exif.d.ts +1 -0
  83. package/dist/src/images/exif.js +6 -0
  84. package/dist/src/images/node.d.ts +37 -0
  85. package/dist/src/images/node.js +12 -0
  86. package/dist/src/images/photon.d.ts +3 -0
  87. package/dist/src/images/photon.js +7 -0
  88. package/dist/src/images/worker.d.ts +1 -0
  89. package/dist/src/images/worker.js +23 -0
  90. package/dist/src/index.d.ts +20 -0
  91. package/dist/src/index.js +121 -0
  92. package/dist/src/key-file.d.ts +1 -0
  93. package/dist/src/key-file.js +7 -0
  94. package/dist/src/listen-fetch.d.ts +8 -0
  95. package/dist/src/listen-fetch.js +9 -0
  96. package/dist/src/listen.d.ts +4 -0
  97. package/dist/src/listen.js +10 -0
  98. package/dist/src/local/command.d.ts +20 -0
  99. package/dist/src/local/command.js +12 -0
  100. package/dist/src/local/feed.d.ts +7 -0
  101. package/dist/src/local/feed.js +6 -0
  102. package/dist/src/local/files.d.ts +18 -0
  103. package/dist/src/local/files.js +13 -0
  104. package/dist/src/local/git-watch.d.ts +15 -0
  105. package/dist/src/local/git-watch.js +7 -0
  106. package/dist/src/local/git.d.ts +5 -0
  107. package/dist/src/local/git.js +13 -0
  108. package/dist/src/local/github.d.ts +30 -0
  109. package/dist/src/local/github.js +9 -0
  110. package/dist/src/local/identity.d.ts +3 -0
  111. package/dist/src/local/identity.js +7 -0
  112. package/dist/src/local/jobs.d.ts +7 -0
  113. package/dist/src/local/jobs.js +7 -0
  114. package/dist/src/local/legacy-trust.d.ts +10 -0
  115. package/dist/src/local/legacy-trust.js +6 -0
  116. package/dist/src/local/patch.d.ts +16 -0
  117. package/dist/src/local/patch.js +17 -0
  118. package/dist/src/local/provider.d.ts +11 -0
  119. package/dist/src/local/provider.js +20 -0
  120. package/dist/src/local/snapshots.d.ts +7 -0
  121. package/dist/src/local/snapshots.js +11 -0
  122. package/dist/src/local/text.d.ts +1 -0
  123. package/dist/src/local/text.js +6 -0
  124. package/dist/src/local/tree.d.ts +50 -0
  125. package/dist/src/local/tree.js +16 -0
  126. package/dist/src/local/workspace-migration.d.ts +2 -0
  127. package/dist/src/local/workspace-migration.js +7 -0
  128. package/dist/src/local/worktrees.d.ts +46 -0
  129. package/dist/src/local/worktrees.js +14 -0
  130. package/dist/src/mcp/config.d.ts +75 -0
  131. package/dist/src/mcp/config.js +12 -0
  132. package/dist/src/mcp/connect.d.ts +15 -0
  133. package/dist/src/mcp/connect.js +9 -0
  134. package/dist/src/mcp/hub.d.ts +19 -0
  135. package/dist/src/mcp/hub.js +11 -0
  136. package/dist/src/mcp/oauth.d.ts +46 -0
  137. package/dist/src/mcp/oauth.js +12 -0
  138. package/dist/src/mcp/tools.d.ts +29 -0
  139. package/dist/src/mcp/tools.js +12 -0
  140. package/dist/src/open.d.ts +14 -0
  141. package/dist/src/open.js +46 -0
  142. package/dist/src/plugins/loader.d.ts +21 -0
  143. package/dist/src/plugins/loader.js +11 -0
  144. package/dist/src/postgres.d.ts +19 -0
  145. package/dist/src/postgres.js +8 -0
  146. package/dist/src/providers.d.ts +16 -0
  147. package/dist/src/providers.js +112 -0
  148. package/dist/src/secrets.d.ts +12 -0
  149. package/dist/src/secrets.js +8 -0
  150. package/dist/src/serve/binding.d.ts +39 -0
  151. package/dist/src/serve/binding.js +6 -0
  152. package/dist/src/serve/handler.d.ts +15 -0
  153. package/dist/src/serve/handler.js +8 -0
  154. package/dist/src/serve/stream-server.d.ts +6 -0
  155. package/dist/src/serve/stream-server.js +6 -0
  156. package/dist/src/session-index.d.ts +11 -0
  157. package/dist/src/session-index.js +11 -0
  158. package/dist/src/shell-env.d.ts +14 -0
  159. package/dist/src/shell-env.js +108 -0
  160. package/dist/src/sqlite-workers.d.ts +6 -0
  161. package/dist/src/sqlite-workers.js +6 -0
  162. package/dist/src/sqlite.d.ts +8 -0
  163. package/dist/src/sqlite.js +11 -0
  164. package/dist/src/store-rows.d.ts +13 -0
  165. package/dist/src/store-rows.js +26 -0
  166. package/dist/src/store-usage.d.ts +25 -0
  167. package/dist/src/store-usage.js +19 -0
  168. package/dist/src/usage-history.d.ts +54 -0
  169. package/dist/src/usage-history.js +14 -0
  170. package/dist/src/usage-scan.d.ts +24 -0
  171. package/dist/src/usage-scan.js +65 -0
  172. package/dist/src/usage-service.d.ts +23 -0
  173. package/dist/src/usage-service.js +16 -0
  174. package/dist/src/usage-summary.d.ts +9 -0
  175. package/dist/src/usage-summary.js +16 -0
  176. package/dist/src/usage-types.d.ts +86 -0
  177. package/dist/src/usage-types.js +0 -0
  178. package/dist/src/usage-worker.d.ts +1 -0
  179. package/dist/src/usage-worker.js +29 -0
  180. package/dist/src/usage.d.ts +195 -0
  181. package/dist/src/usage.js +23 -0
  182. package/package.json +140 -3
  183. package/src/canvas-preview.ts +403 -0
  184. package/src/credentials.ts +294 -0
  185. package/src/deploy/cloudflare/README.md +3 -0
  186. package/src/deploy/cloudflare/worker.ts +306 -0
  187. package/src/deploy/cloudflare/wrangler.jsonc +12 -0
  188. package/src/deploy/vercel/README.md +3 -0
  189. package/src/deploy/vercel/nitro.config.ts +29 -0
  190. package/src/deploy/vercel/server/routes/[...].ts +10 -0
  191. package/src/deploy/vercel/server/utils/host.ts +104 -0
  192. package/src/deploy/vercel/server/utils/repair.ts +35 -0
  193. package/src/deploy/vercel/server/utils/step.ts +25 -0
  194. package/src/deploy/vercel/server/workflows/drive.ts +46 -0
  195. package/src/deploy/vercel/tsconfig.json +9 -0
  196. package/src/deploy/vercel/types/bun.d.ts +2 -0
  197. package/src/durable-object.ts +386 -0
  198. package/src/hash.ts +5 -0
  199. package/src/identity-key.ts +20 -0
  200. package/src/identity.ts +66 -0
  201. package/src/images/exif.ts +69 -0
  202. package/src/images/node.ts +163 -0
  203. package/src/images/photon.ts +141 -0
  204. package/src/images/worker.ts +29 -0
  205. package/src/index.ts +39 -0
  206. package/src/key-file.ts +53 -0
  207. package/src/listen-fetch.ts +409 -0
  208. package/src/listen.ts +228 -0
  209. package/src/local/command.ts +161 -0
  210. package/src/local/feed.ts +70 -0
  211. package/src/local/files.ts +817 -0
  212. package/src/local/git-watch.ts +199 -0
  213. package/src/local/git.ts +919 -0
  214. package/src/local/github.ts +691 -0
  215. package/src/local/identity.ts +21 -0
  216. package/src/local/jobs.ts +573 -0
  217. package/src/local/legacy-trust.ts +105 -0
  218. package/src/local/patch.ts +334 -0
  219. package/src/local/provider.ts +184 -0
  220. package/src/local/snapshots.ts +778 -0
  221. package/src/local/text.ts +9 -0
  222. package/src/local/tree.ts +398 -0
  223. package/src/local/workspace-migration.ts +95 -0
  224. package/src/local/worktrees.ts +239 -0
  225. package/src/mcp/config.ts +330 -0
  226. package/src/mcp/connect.ts +92 -0
  227. package/src/mcp/hub.ts +836 -0
  228. package/src/mcp/oauth.ts +436 -0
  229. package/src/mcp/tools.ts +333 -0
  230. package/src/open.ts +449 -0
  231. package/src/plugins/loader.ts +186 -0
  232. package/src/postgres.ts +492 -0
  233. package/src/providers.ts +135 -0
  234. package/src/secrets.ts +31 -0
  235. package/src/serve/binding.ts +629 -0
  236. package/src/serve/handler.ts +362 -0
  237. package/src/serve/stream-server.ts +176 -0
  238. package/src/session-index.ts +99 -0
  239. package/src/shell-env.ts +153 -0
  240. package/src/sqlite-workers.ts +283 -0
  241. package/src/sqlite.ts +553 -0
  242. package/src/store-rows.ts +110 -0
  243. package/src/store-usage.ts +347 -0
  244. package/src/usage-history.ts +219 -0
  245. package/src/usage-scan.ts +81 -0
  246. package/src/usage-service.ts +82 -0
  247. package/src/usage-summary.ts +79 -0
  248. package/src/usage-types.ts +87 -0
  249. package/src/usage-worker.ts +24 -0
  250. package/src/usage.ts +776 -0
package/src/open.ts ADDED
@@ -0,0 +1,449 @@
1
+ import { watch } from "node:fs";
2
+ import { mkdir, readFile } from "node:fs/promises";
3
+ import { homedir, hostname } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import process from "node:process";
6
+ import type { Models, Provider } from "@nyte-ai/ai";
7
+ import { defaultProviderAuthContext } from "@nyte-ai/ai/auth/context";
8
+ import { FileModelsStore, nyteProviders } from "@nyte-ai/ai";
9
+ import { createModels } from "@nyte-ai/ai/models";
10
+ import { isJson } from "@nyte-ai/schema";
11
+ import { serial } from "@nyte-ai/schema/serial";
12
+ import type {
13
+ HostDescription,
14
+ HostView,
15
+ Json,
16
+ PluginReplacementReport,
17
+ ValidationIssue,
18
+ WorkspaceTarget,
19
+ } from "@nyte-ai/schema";
20
+ import { dev, processEnvironment } from "@nyte-ai/core";
21
+ import { composeHost } from "@nyte-ai/core/server";
22
+ import { readModelSelection } from "@nyte-ai/core/store";
23
+ import type { Host, ListenOptions, Listener, OpenOptions, Plugin, Store } from "@nyte-ai/core";
24
+ import approvals from "@nyte-ai/plugin/core/approvals";
25
+ import { acpAgents } from "@nyte-ai/plugin/core/acp";
26
+ import bashDescription from "@nyte-ai/plugin/core/bash-description";
27
+ import browserTools from "@nyte-ai/plugin/core/browser-tools";
28
+ import canvas from "@nyte-ai/plugin/core/canvas";
29
+ import { claudeCodeSdk } from "@nyte-ai/plugin/core/claude-code-sdk";
30
+ import codemode from "@nyte-ai/plugin/core/codemode";
31
+ import openaiAstraContext from "@nyte-ai/plugin/core/openai-astra-context";
32
+ import { openaiCompaction } from "@nyte-ai/plugin/core/openai-compaction";
33
+ import question from "@nyte-ai/plugin/core/question";
34
+ import rename from "@nyte-ai/plugin/core/rename";
35
+ import telemetry from "@nyte-ai/plugin/core/telemetry";
36
+ import webSearch from "@nyte-ai/plugin/core/web-search";
37
+ import manifest from "../package.json" with { type: "json" };
38
+ import { credentialStore } from "./credentials.ts";
39
+ import { identitySigner } from "./identity-key.ts";
40
+ import { readErrorCode } from "./local/tree.ts";
41
+ import { localGitHubAccount } from "./local/github.ts";
42
+ import { localProvider } from "./local/provider.ts";
43
+ import { importLegacyTrust } from "./local/legacy-trust.ts";
44
+ import { migrateLocalWorkspaceIdentities } from "./local/workspace-migration.ts";
45
+ import { listen } from "./listen.ts";
46
+ import { nodeImages } from "./images/node.ts";
47
+ import { createMcpHub } from "./mcp/hub.ts";
48
+ import { loadPluginDirectory, pluginLoader } from "./plugins/loader.ts";
49
+ import { defaultSecrets } from "./secrets.ts";
50
+ import type { SecretsSource } from "./secrets.ts";
51
+ import { sqliteStore, storePath } from "./sqlite.ts";
52
+
53
+ const RELOAD_DEBOUNCE_MS = 250;
54
+
55
+ const STORE_POLL_MS = 500;
56
+
57
+ export type OpenHostOptions = Omit<OpenOptions, "store" | "workspace" | "models" | "plugins"> & {
58
+ readonly store?: Store;
59
+ readonly workspace?: WorkspaceTarget;
60
+ readonly models?: Models;
61
+ readonly plugins?: readonly Plugin<"core">[];
62
+ readonly home?: string;
63
+ readonly shell?: () => string | null;
64
+ readonly label?: string;
65
+ readonly onRevoked?: () => void;
66
+ };
67
+
68
+ export async function openHost(options: OpenHostOptions): Promise<Host> {
69
+ const home = options.home ?? homedir();
70
+ const workspace = options.workspace ?? { kind: "local", path: process.cwd() };
71
+
72
+ if (options.store !== undefined)
73
+ return composeOpen(options, { home, workspace, store: options.store, owned: null });
74
+ const owned = await defaultStore(home);
75
+
76
+ try {
77
+ return await composeOpen(options, { home, workspace, store: owned, owned });
78
+ } catch (cause) {
79
+ await owned.close().catch(() => undefined);
80
+ throw cause;
81
+ }
82
+ }
83
+
84
+ async function defaultStore(home: string): Promise<Store> {
85
+ const path = storePath(home);
86
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
87
+
88
+ return sqliteStore({ path });
89
+ }
90
+
91
+ async function composeOpen(
92
+ options: OpenHostOptions,
93
+ input: {
94
+ readonly home: string;
95
+ readonly workspace: WorkspaceTarget;
96
+ readonly store: Store;
97
+ readonly owned: Store | null;
98
+ },
99
+ ): Promise<Host> {
100
+ const { home, workspace, store, owned } = input;
101
+ const secrets = options.secrets ?? defaultSecrets(home);
102
+ const description = hostDescription();
103
+ const given = options.plugins ?? [];
104
+ const usesLocalProvider = !given.some((plugin) => plugin.environment?.kind === "local");
105
+
106
+ const claudeCode = claudeCodeSdk();
107
+ const acp = acpAgents();
108
+
109
+ const [models, files, settings] = await Promise.all([
110
+ options.models ??
111
+ nyteModels({ store, secrets, home, providers: [claudeCode.provider, acp.provider] }),
112
+ filePlugins(home),
113
+ settingsSeed(home),
114
+ usesLocalProvider
115
+ ? store.open().then(async (connection) => {
116
+ await migrateLocalWorkspaceIdentities(connection);
117
+ await importLegacyTrust(connection, home);
118
+ })
119
+ : undefined,
120
+ ]);
121
+
122
+ const builtins = [
123
+ approvals,
124
+ question,
125
+ rename,
126
+ bashDescription,
127
+ webSearch,
128
+ codemode,
129
+ openaiCompaction(models),
130
+ openaiAstraContext,
131
+ telemetry,
132
+ canvas,
133
+ claudeCode.plugin,
134
+ acp.plugin,
135
+ ];
136
+
137
+ const plugins = [
138
+ ...(usesLocalProvider
139
+ ? [{ id: "local", environment: localProvider({ home, shell: options.shell }) }]
140
+ : []),
141
+ ...builtins.filter((builtin) => !given.some((plugin) => plugin.id === builtin.id)),
142
+ ...given,
143
+ ];
144
+
145
+ let projectDirs: () => Promise<readonly string[]> = async () => [];
146
+
147
+ const host = await composeHost({
148
+ ...options,
149
+ store,
150
+ workspace,
151
+ loadPlugins: async (paths) => pluginLoader([...files.dirs, ...(await projectDirs())])(paths),
152
+ models,
153
+ mcp: options.mcp ?? createMcpHub({ home }),
154
+ github: options.github ?? (usesLocalProvider ? localGitHubAccount({ home }) : undefined),
155
+ images: options.images ?? nodeImages(),
156
+ plugins: [...plugins, ...files.plugins],
157
+ pluginSources: files.sources,
158
+ secrets,
159
+ home,
160
+ description,
161
+ ...(settings.seed === null ? {} : { seed: settings.seed }),
162
+ ...(owned === null ? {} : { poll: STORE_POLL_MS }),
163
+ member: {
164
+ label: options.label ?? "nyte",
165
+ pid: process.pid,
166
+ capabilities: given.some((plugin) => plugin.id === browserTools.id) ? [browserTools.id] : [],
167
+ },
168
+ revoked: options.onRevoked,
169
+ });
170
+
171
+ projectDirs = async () =>
172
+ (await host.workspaces.list()).flatMap((entry) =>
173
+ entry.trusted === true && "path" in entry.target
174
+ ? [join(entry.target.path, ".nyte", "plugins")]
175
+ : [],
176
+ );
177
+
178
+ for (const row of files.failed) host.server.report(row);
179
+
180
+ if (settings.issue !== null)
181
+ host.server.report({
182
+ id: "settings",
183
+ source: "user",
184
+ path: settingsPath(home),
185
+ issues: [settings.issue],
186
+ });
187
+
188
+ dev("open", "host", {
189
+ home,
190
+ seedIds: Object.keys(settings.seed?.settings ?? {}).length,
191
+ pluginFiles: files.plugins.length + files.failed.length,
192
+ failedFiles: files.failed.length,
193
+ });
194
+
195
+ const reported = new Map(
196
+ host.server
197
+ .pluginRows()
198
+ .filter((row) => row.path !== null && row.id === row.path)
199
+ .map((row) => [row.id, row]),
200
+ );
201
+
202
+ const reload = async (): Promise<void> => {
203
+ const next = await filePlugins(home);
204
+
205
+ for (const [plugin, source] of next.sources) files.sources.set(plugin, source);
206
+ const report = await host.plugins.replace([...plugins, ...next.plugins]);
207
+ const failed = [...next.failed, ...refused(next, report)];
208
+
209
+ for (const [id, row] of reported)
210
+ if (!failed.some((row) => row.id === id)) host.server.report({ ...row, issues: [] });
211
+
212
+ for (const row of failed) host.server.report(row);
213
+ reported.clear();
214
+
215
+ for (const row of failed) reported.set(row.id, row);
216
+
217
+ for (const plugin of files.sources.keys())
218
+ if (!host.server.loaded(plugin)) files.sources.delete(plugin);
219
+ dev("open", "plugins reloaded", {
220
+ generation: report.generation,
221
+ pluginFiles: next.plugins.length,
222
+ failedFiles: failed.length,
223
+ });
224
+ };
225
+
226
+ const unwatch = watchPlugins(files.dirs, reload);
227
+ const listeners = new Set<Listener>();
228
+ let identity: ReturnType<typeof identitySigner> | null = null;
229
+
230
+ const attach = async (listenOptions: ListenOptions): Promise<Listener> => {
231
+ const listener = await listen(host, {
232
+ describe: () => description,
233
+ ...listenOptions,
234
+ identity: listenOptions.identity ?? (await (identity ??= identitySigner(host.hostId, home))),
235
+ });
236
+
237
+ listeners.add(listener);
238
+ dev("open", "listen", { address: listener.address });
239
+
240
+ return listener;
241
+ };
242
+
243
+ let closing: Promise<void> | null = null;
244
+
245
+ const close = (): Promise<void> => {
246
+ closing ??= (async () => {
247
+ await unwatch();
248
+
249
+ for (const listener of listeners) await listener.close();
250
+ await host.close();
251
+ await owned?.close();
252
+ })();
253
+
254
+ return closing;
255
+ };
256
+
257
+ return { ...host, listen: attach, close, [Symbol.asyncDispose]: close };
258
+ }
259
+
260
+ async function nyteModels(input: {
261
+ readonly store: Store;
262
+ readonly secrets: SecretsSource;
263
+ readonly home: string;
264
+ readonly providers: readonly Provider[];
265
+ }): Promise<Models> {
266
+ const credentials = credentialStore(input.store, input.secrets);
267
+ const auth = defaultProviderAuthContext();
268
+
269
+ const models = createModels({
270
+ credentials,
271
+ modelsStore: new FileModelsStore(join(input.home, ".nyte", "models-store.json")),
272
+ authContext: {
273
+ ...auth,
274
+ env: async (name) => {
275
+ await processEnvironment();
276
+
277
+ return auth.env(name);
278
+ },
279
+ },
280
+ catalog: { url: process.env.NYTE_MODELS_URL },
281
+ });
282
+
283
+ for (const provider of [...nyteProviders(), ...input.providers]) models.setProvider(provider);
284
+
285
+ const [stored, selected] = await Promise.all([
286
+ credentials.list(),
287
+ input.store.open().then(readModelSelection),
288
+ ]);
289
+
290
+ const restored = await models.refresh({
291
+ providers: [
292
+ ...new Set([
293
+ ...stored.map((row) => row.providerId),
294
+ ...input.providers.map((provider) => provider.id),
295
+ ...(selected === null ? [] : [selected.provider]),
296
+ ]),
297
+ ],
298
+ allowNetwork: false,
299
+ });
300
+
301
+ dev("open", "models restored", {
302
+ providers: stored.length,
303
+ failed: [...restored.errors.keys()],
304
+ });
305
+
306
+ return models;
307
+ }
308
+
309
+ async function filePlugins(home: string): Promise<{
310
+ readonly plugins: readonly Plugin<"core">[];
311
+ readonly sources: Map<
312
+ Plugin<"core">,
313
+ { readonly source: "user" | "project"; readonly path: string }
314
+ >;
315
+ readonly dirs: readonly string[];
316
+ readonly failed: readonly HostView["plugins"][number][];
317
+ }> {
318
+ const scopes = [{ source: "user" as const, dir: join(home, ".nyte", "plugins") }];
319
+
320
+ const plugins: Plugin<"core">[] = [];
321
+ const sources = new Map<Plugin<"core">, { source: "user" | "project"; path: string }>();
322
+ const failed: HostView["plugins"][number][] = [];
323
+
324
+ for (const { source, dir } of scopes) {
325
+ const found = await loadPluginDirectory(dir);
326
+
327
+ for (const { plugin, path } of found.loaded) {
328
+ plugins.push(plugin);
329
+ sources.set(plugin, { source, path });
330
+ }
331
+
332
+ for (const { path, issue } of found.failed)
333
+ failed.push({ id: path, source, path, issues: [issue] });
334
+ }
335
+
336
+ return { plugins, sources, dirs: scopes.map((scope) => scope.dir), failed };
337
+ }
338
+
339
+ function refused(
340
+ files: Awaited<ReturnType<typeof filePlugins>>,
341
+ report: PluginReplacementReport,
342
+ ): readonly HostView["plugins"][number][] {
343
+ return report.plugins.flatMap((outcome) => {
344
+ const plugin = files.plugins.find((candidate) => candidate.id === outcome.id);
345
+ const file = plugin === undefined ? undefined : files.sources.get(plugin);
346
+
347
+ return outcome.kind === "activated" || file === undefined
348
+ ? []
349
+ : [{ id: file.path, source: file.source, path: file.path, issues: outcome.issues }];
350
+ });
351
+ }
352
+
353
+ function watchPlugins(dirs: readonly string[], reload: () => Promise<void>): () => Promise<void> {
354
+ let timer: ReturnType<typeof setTimeout> | null = null;
355
+ const reloads = serial();
356
+
357
+ const run = (): void => {
358
+ timer = null;
359
+ void reloads.run(reload).catch((error: unknown) =>
360
+ dev("open", "plugin reload failed", {
361
+ message: error instanceof Error ? error.message : String(error),
362
+ }),
363
+ );
364
+ };
365
+
366
+ const changed = (): void => {
367
+ if (timer !== null) clearTimeout(timer);
368
+ timer = setTimeout(run, RELOAD_DEBOUNCE_MS);
369
+ timer.unref();
370
+ };
371
+
372
+ const watchers = dirs.flatMap((dir) => {
373
+ try {
374
+ const watcher = watch(dir, { recursive: true, persistent: false }, changed);
375
+
376
+ watcher.on("error", (error) => {
377
+ dev("open", "plugin watch failed", { dir, message: error.message });
378
+ watcher.close();
379
+ });
380
+ dev("open", "plugins watched", { dir });
381
+
382
+ return [watcher];
383
+ } catch (error) {
384
+ dev("open", "plugins unwatched", { dir, code: readErrorCode(error) });
385
+
386
+ return [];
387
+ }
388
+ });
389
+
390
+ return async () => {
391
+ for (const watcher of watchers) watcher.close();
392
+
393
+ if (timer !== null) clearTimeout(timer);
394
+ await reloads.settled();
395
+ };
396
+ }
397
+
398
+ async function settingsSeed(home: string): Promise<{
399
+ readonly seed: {
400
+ readonly settings: Readonly<Record<string, Json>>;
401
+ readonly path: string;
402
+ } | null;
403
+ readonly issue: ValidationIssue | null;
404
+ }> {
405
+ const path = settingsPath(home);
406
+
407
+ const text = await readFile(path, "utf8").catch((error: unknown) =>
408
+ readErrorCode(error) === "ENOENT"
409
+ ? null
410
+ : error instanceof Error
411
+ ? error
412
+ : new Error(String(error)),
413
+ );
414
+
415
+ if (text === null) return { seed: null, issue: null };
416
+
417
+ if (text instanceof Error)
418
+ return {
419
+ seed: null,
420
+ issue: { severity: "warning", path: [], message: `${path} cannot be read: ${text.message}` },
421
+ };
422
+ let parsed: unknown;
423
+
424
+ try {
425
+ parsed = JSON.parse(text);
426
+ } catch {
427
+ parsed = undefined;
428
+ }
429
+
430
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
431
+ return {
432
+ seed: null,
433
+ issue: { severity: "warning", path: [], message: `${path} is not a JSON object.` },
434
+ };
435
+
436
+ const settings: Record<string, Json> = {};
437
+
438
+ for (const [id, value] of Object.entries(parsed)) if (isJson(value)) settings[id] = value;
439
+
440
+ return { seed: { settings, path }, issue: null };
441
+ }
442
+
443
+ function hostDescription(): HostDescription {
444
+ return { name: hostname(), platform: process.platform, version: manifest.version };
445
+ }
446
+
447
+ function settingsPath(home: string): string {
448
+ return join(home, ".nyte", "settings.json");
449
+ }
@@ -0,0 +1,186 @@
1
+ import { readdir, realpath, stat } from "node:fs/promises";
2
+ import { isAbsolute, join, relative } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { SdkError } from "@nyte-ai/schema";
5
+ import type { ValidationIssue } from "@nyte-ai/schema";
6
+ import { dev } from "@nyte-ai/core";
7
+ import type { Plugin } from "@nyte-ai/core";
8
+ import type { ComposeOptions } from "@nyte-ai/core/server";
9
+ import { readErrorCode } from "../local/tree.ts";
10
+
11
+ const EXTENSIONS = [".ts", ".mts", ".js", ".mjs"];
12
+
13
+ /** Returns the core plugins in a plugin directory (`~/.nyte/plugins`, a workspace's `.nyte/plugins`), by file name: each `.ts`, `.mts`, `.js` or `.mjs` file directly in it and each subdirectory's `index` of those, loaded with a dynamic `import()` of its file URL. That `import()` is one of the two dynamic loads the rewrite allows (§41a), host-only, because a plugin file is code the owner placed there and its path is known only at run time; Node strips `.ts` types natively (erasable syntax only). The URL carries the file's modification time and size as a query, so a replaced file loads fresh instead of from the module cache. The default export must be a core plugin (an object with a string `id`, and, where present, `contribute` and `hooks` objects, an `activate` function and an `environment` with a string `kind` and `inspect` and `attach` functions); every file that does not load or export one is reported, together, as `SdkError("invalid")` with an issue per file naming it. A missing directory has none. */
14
+ export async function loadPluginFiles(
15
+ dir: string,
16
+ ): Promise<readonly { readonly plugin: Plugin<"core">; readonly path: string }[]> {
17
+ return loadAll(await pluginFilesIn(dir));
18
+ }
19
+
20
+ /** Returns a plugin directory's plugins as `loadPluginFiles` finds and imports them, but file by file: every file that loads, with its path, and every file that does not, with its path and one `error` issue (its `path` the file, `source.file` the file, the message `loadPluginFiles` would have put in its `invalid`), never rejecting for a bad file; a missing directory has neither. What `openHost` loads `~/.nyte/plugins` and a local workspace's `.nyte/plugins` through, so one bad file is reported on `HostView.plugins` instead of failing the open (§41j); `loadPluginFiles` becomes this, throwing its one `invalid` when anything failed. */
21
+ export async function loadPluginDirectory(dir: string): Promise<{
22
+ readonly loaded: readonly { readonly plugin: Plugin<"core">; readonly path: string }[];
23
+ readonly failed: readonly { readonly path: string; readonly issue: ValidationIssue }[];
24
+ }> {
25
+ return importEach(await pluginFilesIn(dir));
26
+ }
27
+
28
+ /** Returns the host's `loadPlugins` (`composeHost({ loadPlugins })`), which `plugins.replace` and `plugins.check` load their files through (§40b, §47m): each path, resolved to its real path, must lie inside one of `dirs` (else `SdkError("denied")`) and is imported as `loadPluginFiles` imports a file; the plugins come back in path order. */
29
+ export function pluginLoader(dirs: readonly string[]): NonNullable<ComposeOptions["loadPlugins"]> {
30
+ return async (paths) => {
31
+ const roots = (await Promise.all(dirs.map((dir) => realpath(dir).catch(() => null)))).filter(
32
+ (root) => root !== null,
33
+ );
34
+
35
+ const files = await Promise.all(
36
+ paths.map(async (path) => {
37
+ const real = await realpath(path).catch(() => null);
38
+
39
+ if (real === null) throw new SdkError("invalid", `No plugin file is at ${path}.`);
40
+
41
+ if (!roots.some((root) => inside(root, real)))
42
+ throw new SdkError("denied", `${path} is not in a plugin directory this host loads.`);
43
+
44
+ return real;
45
+ }),
46
+ );
47
+
48
+ return (await loadAll(files)).map((loaded) => loaded.plugin);
49
+ };
50
+ }
51
+
52
+ async function loadAll(
53
+ files: readonly string[],
54
+ ): Promise<readonly { readonly plugin: Plugin<"core">; readonly path: string }[]> {
55
+ const { loaded, failed } = await importEach(files);
56
+ const issues = failed.map((entry) => entry.issue);
57
+
58
+ if (issues.length > 0)
59
+ throw new SdkError(
60
+ "invalid",
61
+ `${issues.length === 1 ? "A plugin file" : `${issues.length} plugin files`} did not load: ${failed.map((entry) => `${entry.path}: ${entry.issue.message}`).join("; ")}`,
62
+ { issues },
63
+ );
64
+
65
+ return loaded;
66
+ }
67
+
68
+ async function importEach(files: readonly string[]): Promise<{
69
+ readonly loaded: readonly { readonly plugin: Plugin<"core">; readonly path: string }[];
70
+ readonly failed: readonly { readonly path: string; readonly issue: ValidationIssue }[];
71
+ }> {
72
+ const loaded: { readonly plugin: Plugin<"core">; readonly path: string }[] = [];
73
+ const failed: { readonly path: string; readonly issue: ValidationIssue }[] = [];
74
+
75
+ for (const path of files) {
76
+ const outcome = await importPlugin(path);
77
+ dev("plugins", "file", { path, outcome: "plugin" in outcome ? "loaded" : "failed" });
78
+
79
+ if ("plugin" in outcome) loaded.push({ plugin: outcome.plugin, path });
80
+ else
81
+ failed.push({
82
+ path,
83
+ issue: {
84
+ severity: "error",
85
+ path: [path],
86
+ message: outcome.reason,
87
+ source: { plugin: path, file: path },
88
+ },
89
+ });
90
+ }
91
+
92
+ return { loaded, failed };
93
+ }
94
+
95
+ async function pluginFilesIn(dir: string): Promise<readonly string[]> {
96
+ const entries = await readdir(dir, { withFileTypes: true }).catch((error: unknown) => {
97
+ if (readErrorCode(error) === "ENOENT" || readErrorCode(error) === "ENOTDIR") return [];
98
+
99
+ throw error;
100
+ });
101
+
102
+ return (
103
+ await Promise.all(
104
+ entries
105
+ .sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0))
106
+ .map(async (entry) => {
107
+ const path = join(dir, entry.name);
108
+
109
+ if (entry.isFile())
110
+ return EXTENSIONS.some((extension) => entry.name.endsWith(extension)) ? [path] : [];
111
+
112
+ if (!entry.isDirectory()) return [];
113
+
114
+ for (const extension of EXTENSIONS) {
115
+ const index = join(path, `index${extension}`);
116
+
117
+ if ((await stat(index).catch(() => null))?.isFile() === true) return [index];
118
+ }
119
+
120
+ return [];
121
+ }),
122
+ )
123
+ ).flat();
124
+ }
125
+
126
+ async function importPlugin(
127
+ path: string,
128
+ ): Promise<{ readonly plugin: Plugin<"core"> } | { readonly reason: string }> {
129
+ try {
130
+ const stats = await stat(path);
131
+ const url = pathToFileURL(path);
132
+ url.searchParams.set("nyte", `${stats.mtimeMs}-${stats.size}`);
133
+ const imported: unknown = await import(url.href);
134
+
135
+ const exported =
136
+ typeof imported === "object" && imported !== null && "default" in imported
137
+ ? imported.default
138
+ : undefined;
139
+
140
+ return fitsCorePlugin(exported)
141
+ ? { plugin: exported }
142
+ : { reason: "Its default export is not a core plugin (an object with a string id)." };
143
+ } catch (error) {
144
+ return { reason: error instanceof Error ? error.message : String(error) };
145
+ }
146
+ }
147
+
148
+ function fitsCorePlugin(value: unknown): value is Plugin<"core"> {
149
+ if (
150
+ typeof value !== "object" ||
151
+ value === null ||
152
+ !("id" in value) ||
153
+ typeof value.id !== "string"
154
+ )
155
+ return false;
156
+
157
+ const object = (field: unknown): boolean =>
158
+ field === undefined || (typeof field === "object" && field !== null);
159
+
160
+ if ("contribute" in value && !object(value.contribute)) return false;
161
+
162
+ if ("hooks" in value && !object(value.hooks)) return false;
163
+
164
+ if ("activate" in value && value.activate !== undefined && typeof value.activate !== "function")
165
+ return false;
166
+
167
+ if (!("environment" in value) || value.environment === undefined) return true;
168
+ const environment = value.environment;
169
+
170
+ return (
171
+ typeof environment === "object" &&
172
+ environment !== null &&
173
+ "kind" in environment &&
174
+ typeof environment.kind === "string" &&
175
+ "inspect" in environment &&
176
+ typeof environment.inspect === "function" &&
177
+ "attach" in environment &&
178
+ typeof environment.attach === "function"
179
+ );
180
+ }
181
+
182
+ function inside(root: string, path: string): boolean {
183
+ const within = relative(root, path);
184
+
185
+ return within !== "" && !within.startsWith("..") && !isAbsolute(within);
186
+ }