zoe-agent 0.3.1

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 (267) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/LICENSE +96 -0
  3. package/README.md +568 -0
  4. package/dist/adapters/cli/agent.d.ts +59 -0
  5. package/dist/adapters/cli/agent.js +232 -0
  6. package/dist/adapters/cli/bootstrap.d.ts +25 -0
  7. package/dist/adapters/cli/bootstrap.js +204 -0
  8. package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
  9. package/dist/adapters/cli/commands/build-registry.js +88 -0
  10. package/dist/adapters/cli/commands/clear.d.ts +7 -0
  11. package/dist/adapters/cli/commands/clear.js +10 -0
  12. package/dist/adapters/cli/commands/compact.d.ts +13 -0
  13. package/dist/adapters/cli/commands/compact.js +96 -0
  14. package/dist/adapters/cli/commands/exit.d.ts +7 -0
  15. package/dist/adapters/cli/commands/exit.js +9 -0
  16. package/dist/adapters/cli/commands/gateway.d.ts +7 -0
  17. package/dist/adapters/cli/commands/gateway.js +152 -0
  18. package/dist/adapters/cli/commands/help.d.ts +9 -0
  19. package/dist/adapters/cli/commands/help.js +12 -0
  20. package/dist/adapters/cli/commands/models.d.ts +10 -0
  21. package/dist/adapters/cli/commands/models.js +32 -0
  22. package/dist/adapters/cli/commands/registry.d.ts +70 -0
  23. package/dist/adapters/cli/commands/registry.js +111 -0
  24. package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
  25. package/dist/adapters/cli/commands/settings-utils.js +182 -0
  26. package/dist/adapters/cli/commands/settings.d.ts +9 -0
  27. package/dist/adapters/cli/commands/settings.js +395 -0
  28. package/dist/adapters/cli/commands/skills.d.ts +7 -0
  29. package/dist/adapters/cli/commands/skills.js +21 -0
  30. package/dist/adapters/cli/config-loader.d.ts +27 -0
  31. package/dist/adapters/cli/config-loader.js +48 -0
  32. package/dist/adapters/cli/docker-utils.d.ts +37 -0
  33. package/dist/adapters/cli/docker-utils.js +90 -0
  34. package/dist/adapters/cli/index.d.ts +2 -0
  35. package/dist/adapters/cli/index.js +88 -0
  36. package/dist/adapters/cli/repl.d.ts +22 -0
  37. package/dist/adapters/cli/repl.js +256 -0
  38. package/dist/adapters/cli/setup.d.ts +19 -0
  39. package/dist/adapters/cli/setup.js +613 -0
  40. package/dist/adapters/cli/system-prompts.d.ts +56 -0
  41. package/dist/adapters/cli/system-prompts.js +131 -0
  42. package/dist/adapters/cli/tui/app.d.ts +58 -0
  43. package/dist/adapters/cli/tui/app.js +314 -0
  44. package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
  45. package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
  46. package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
  47. package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
  48. package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
  49. package/dist/adapters/cli/tui/components/command-palette.js +50 -0
  50. package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
  51. package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
  52. package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
  53. package/dist/adapters/cli/tui/components/error-message.js +8 -0
  54. package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
  55. package/dist/adapters/cli/tui/components/footer.js +19 -0
  56. package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
  57. package/dist/adapters/cli/tui/components/goal-status.js +22 -0
  58. package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
  59. package/dist/adapters/cli/tui/components/info-message.js +8 -0
  60. package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
  61. package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
  62. package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
  63. package/dist/adapters/cli/tui/components/markdown.js +92 -0
  64. package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
  65. package/dist/adapters/cli/tui/components/message-area.js +55 -0
  66. package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
  67. package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
  68. package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
  69. package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
  70. package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
  71. package/dist/adapters/cli/tui/components/text-input.js +142 -0
  72. package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
  73. package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
  74. package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
  75. package/dist/adapters/cli/tui/components/user-message.js +8 -0
  76. package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
  77. package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
  78. package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
  79. package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
  80. package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
  81. package/dist/adapters/cli/tui/feed-serializer.js +70 -0
  82. package/dist/adapters/cli/tui/file-index.d.ts +8 -0
  83. package/dist/adapters/cli/tui/file-index.js +41 -0
  84. package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
  85. package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
  86. package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
  87. package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
  88. package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
  89. package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
  90. package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
  91. package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
  92. package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
  93. package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
  94. package/dist/adapters/cli/tui/index.d.ts +19 -0
  95. package/dist/adapters/cli/tui/index.js +206 -0
  96. package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
  97. package/dist/adapters/cli/tui/ink-reset.js +57 -0
  98. package/dist/adapters/cli/tui/layout.d.ts +15 -0
  99. package/dist/adapters/cli/tui/layout.js +15 -0
  100. package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
  101. package/dist/adapters/cli/tui/logo/gradient.js +31 -0
  102. package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
  103. package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
  104. package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
  105. package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
  106. package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
  107. package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
  108. package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
  109. package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
  110. package/dist/adapters/cli/tui/session-export.d.ts +21 -0
  111. package/dist/adapters/cli/tui/session-export.js +63 -0
  112. package/dist/adapters/cli/tui/theme.d.ts +23 -0
  113. package/dist/adapters/cli/tui/theme.js +22 -0
  114. package/dist/adapters/cli/tui/types.d.ts +52 -0
  115. package/dist/adapters/cli/tui/types.js +12 -0
  116. package/dist/adapters/sdk/agent.d.ts +20 -0
  117. package/dist/adapters/sdk/agent.js +356 -0
  118. package/dist/adapters/sdk/http.d.ts +43 -0
  119. package/dist/adapters/sdk/http.js +61 -0
  120. package/dist/adapters/sdk/index.d.ts +58 -0
  121. package/dist/adapters/sdk/index.js +209 -0
  122. package/dist/adapters/sdk/settings.d.ts +18 -0
  123. package/dist/adapters/sdk/settings.js +57 -0
  124. package/dist/adapters/sdk/tools.d.ts +7 -0
  125. package/dist/adapters/sdk/tools.js +13 -0
  126. package/dist/adapters/server/auth.d.ts +53 -0
  127. package/dist/adapters/server/auth.js +168 -0
  128. package/dist/adapters/server/index.d.ts +40 -0
  129. package/dist/adapters/server/index.js +255 -0
  130. package/dist/adapters/server/rest-gateway.d.ts +13 -0
  131. package/dist/adapters/server/rest-gateway.js +218 -0
  132. package/dist/adapters/server/rest.d.ts +37 -0
  133. package/dist/adapters/server/rest.js +341 -0
  134. package/dist/adapters/server/server-core.d.ts +55 -0
  135. package/dist/adapters/server/server-core.js +121 -0
  136. package/dist/adapters/server/session-store.d.ts +81 -0
  137. package/dist/adapters/server/session-store.js +272 -0
  138. package/dist/adapters/server/settings-handlers.d.ts +24 -0
  139. package/dist/adapters/server/settings-handlers.js +360 -0
  140. package/dist/adapters/server/standalone.d.ts +19 -0
  141. package/dist/adapters/server/standalone.js +113 -0
  142. package/dist/adapters/server/websocket.d.ts +26 -0
  143. package/dist/adapters/server/websocket.js +68 -0
  144. package/dist/adapters/server/ws-handlers.d.ts +32 -0
  145. package/dist/adapters/server/ws-handlers.js +523 -0
  146. package/dist/adapters/server/ws-types.d.ts +304 -0
  147. package/dist/adapters/server/ws-types.js +7 -0
  148. package/dist/core/agent-loop.d.ts +68 -0
  149. package/dist/core/agent-loop.js +423 -0
  150. package/dist/core/config.d.ts +115 -0
  151. package/dist/core/config.js +189 -0
  152. package/dist/core/errors.d.ts +58 -0
  153. package/dist/core/errors.js +88 -0
  154. package/dist/core/hooks.d.ts +35 -0
  155. package/dist/core/hooks.js +49 -0
  156. package/dist/core/index.d.ts +23 -0
  157. package/dist/core/index.js +29 -0
  158. package/dist/core/message-convert.d.ts +41 -0
  159. package/dist/core/message-convert.js +94 -0
  160. package/dist/core/middleware/auth.d.ts +24 -0
  161. package/dist/core/middleware/auth.js +28 -0
  162. package/dist/core/middleware/logging.d.ts +23 -0
  163. package/dist/core/middleware/logging.js +28 -0
  164. package/dist/core/middleware/rate-limit.d.ts +27 -0
  165. package/dist/core/middleware/rate-limit.js +38 -0
  166. package/dist/core/middleware/semantic-tools.d.ts +10 -0
  167. package/dist/core/middleware/semantic-tools.js +43 -0
  168. package/dist/core/middleware.d.ts +48 -0
  169. package/dist/core/middleware.js +38 -0
  170. package/dist/core/permission.d.ts +25 -0
  171. package/dist/core/permission.js +50 -0
  172. package/dist/core/provider-config.d.ts +129 -0
  173. package/dist/core/provider-config.js +273 -0
  174. package/dist/core/provider-env.d.ts +39 -0
  175. package/dist/core/provider-env.js +142 -0
  176. package/dist/core/provider-resolver.d.ts +12 -0
  177. package/dist/core/provider-resolver.js +12 -0
  178. package/dist/core/session-store.d.ts +75 -0
  179. package/dist/core/session-store.js +245 -0
  180. package/dist/core/settings-manager.d.ts +57 -0
  181. package/dist/core/settings-manager.js +359 -0
  182. package/dist/core/settings-schema.d.ts +38 -0
  183. package/dist/core/settings-schema.js +171 -0
  184. package/dist/core/skill-catalog.d.ts +6 -0
  185. package/dist/core/skill-catalog.js +17 -0
  186. package/dist/core/skill-invoker.d.ts +127 -0
  187. package/dist/core/skill-invoker.js +182 -0
  188. package/dist/core/stream-accumulator.d.ts +21 -0
  189. package/dist/core/stream-accumulator.js +51 -0
  190. package/dist/core/stream-manager.d.ts +58 -0
  191. package/dist/core/stream-manager.js +212 -0
  192. package/dist/core/tool-executor.d.ts +84 -0
  193. package/dist/core/tool-executor.js +256 -0
  194. package/dist/core/types.d.ts +259 -0
  195. package/dist/core/types.js +11 -0
  196. package/dist/gateway/gateway.d.ts +52 -0
  197. package/dist/gateway/gateway.js +537 -0
  198. package/dist/gateway/index.d.ts +21 -0
  199. package/dist/gateway/index.js +31 -0
  200. package/dist/gateway/openapi-importer.d.ts +15 -0
  201. package/dist/gateway/openapi-importer.js +66 -0
  202. package/dist/gateway/semantic-scorer.d.ts +7 -0
  203. package/dist/gateway/semantic-scorer.js +24 -0
  204. package/dist/gateway/settings-adapter.d.ts +49 -0
  205. package/dist/gateway/settings-adapter.js +137 -0
  206. package/dist/gateway/tool-factory.d.ts +9 -0
  207. package/dist/gateway/tool-factory.js +414 -0
  208. package/dist/gateway/types.d.ts +68 -0
  209. package/dist/gateway/types.js +7 -0
  210. package/dist/models-catalog.js +46 -0
  211. package/dist/providers/anthropic.d.ts +22 -0
  212. package/dist/providers/anthropic.js +148 -0
  213. package/dist/providers/factory.d.ts +10 -0
  214. package/dist/providers/factory.js +25 -0
  215. package/dist/providers/openai.d.ts +15 -0
  216. package/dist/providers/openai.js +71 -0
  217. package/dist/providers/types.d.ts +48 -0
  218. package/dist/providers/types.js +1 -0
  219. package/dist/skills/args.d.ts +37 -0
  220. package/dist/skills/args.js +99 -0
  221. package/dist/skills/index.d.ts +11 -0
  222. package/dist/skills/index.js +23 -0
  223. package/dist/skills/loader.d.ts +3 -0
  224. package/dist/skills/loader.js +59 -0
  225. package/dist/skills/parser.d.ts +7 -0
  226. package/dist/skills/parser.js +152 -0
  227. package/dist/skills/registry.d.ts +13 -0
  228. package/dist/skills/registry.js +74 -0
  229. package/dist/skills/resolver.d.ts +19 -0
  230. package/dist/skills/resolver.js +116 -0
  231. package/dist/skills/types.d.ts +74 -0
  232. package/dist/skills/types.js +50 -0
  233. package/dist/tools/browser.d.ts +2 -0
  234. package/dist/tools/browser.js +68 -0
  235. package/dist/tools/core.d.ts +20 -0
  236. package/dist/tools/core.js +244 -0
  237. package/dist/tools/email.d.ts +2 -0
  238. package/dist/tools/email.js +61 -0
  239. package/dist/tools/image.d.ts +2 -0
  240. package/dist/tools/image.js +257 -0
  241. package/dist/tools/index.d.ts +2 -0
  242. package/dist/tools/index.js +88 -0
  243. package/dist/tools/interface.d.ts +22 -0
  244. package/dist/tools/interface.js +1 -0
  245. package/dist/tools/notify.d.ts +2 -0
  246. package/dist/tools/notify.js +100 -0
  247. package/dist/tools/prompt-optimizer.d.ts +2 -0
  248. package/dist/tools/prompt-optimizer.js +65 -0
  249. package/dist/tools/screenshot.d.ts +2 -0
  250. package/dist/tools/screenshot.js +184 -0
  251. package/dist/tools/search.d.ts +2 -0
  252. package/dist/tools/search.js +78 -0
  253. package/dist/tools/todos.d.ts +10 -0
  254. package/dist/tools/todos.js +50 -0
  255. package/package.json +119 -0
  256. package/skills/docker-ops/SKILL.md +329 -0
  257. package/skills/k8s-deploy/SKILL.md +397 -0
  258. package/skills/log-analyzer/SKILL.md +331 -0
  259. package/skills/speckit-analyze/SKILL.md +260 -0
  260. package/skills/speckit-checklist/SKILL.md +374 -0
  261. package/skills/speckit-clarify/SKILL.md +286 -0
  262. package/skills/speckit-constitution/SKILL.md +157 -0
  263. package/skills/speckit-implement/SKILL.md +224 -0
  264. package/skills/speckit-plan/SKILL.md +171 -0
  265. package/skills/speckit-specify/SKILL.md +346 -0
  266. package/skills/speckit-tasks/SKILL.md +215 -0
  267. package/skills/speckit-taskstoissues/SKILL.md +107 -0
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Zoe SDK — Session persistence
3
+ *
4
+ * Provides composable persistence backends for storing conversation history.
5
+ * Built-in "file" and "memory" backends are registered by default. Custom
6
+ * backends (Redis, SQLite, etc.) can be registered via `registerBackend()`.
7
+ *
8
+ * Legacy `SessionStore`-based API is preserved for backward compatibility.
9
+ */
10
+ import { promises as fs } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ // ── Session ID validation ───────────────────────────────────────────────
14
+ const SESSION_ID_RE = /^[a-zA-Z0-9-]+$/;
15
+ function validateSessionId(sessionId) {
16
+ if (!SESSION_ID_RE.test(sessionId)) {
17
+ throw new Error(`Invalid session ID "${sessionId}". Only alphanumeric characters and dashes are allowed.`);
18
+ }
19
+ }
20
+ // ── Default path ────────────────────────────────────────────────────────
21
+ function defaultSessionPath() {
22
+ return join(homedir(), ".zoe", "sessions");
23
+ }
24
+ // ── File-based PersistenceBackend ───────────────────────────────────────
25
+ /**
26
+ * File-backed persistence backend. Each session is stored as a JSON file
27
+ * at `{basePath}/{sessionId}.json`.
28
+ */
29
+ export class FilePersistenceBackend {
30
+ __persistenceBackend = true;
31
+ basePath;
32
+ constructor(basePath) {
33
+ this.basePath = basePath;
34
+ }
35
+ filePath(id) {
36
+ return join(this.basePath, `${id}.json`);
37
+ }
38
+ async ensureDir() {
39
+ await fs.mkdir(this.basePath, { recursive: true });
40
+ }
41
+ async save(id, data) {
42
+ validateSessionId(id);
43
+ await this.ensureDir();
44
+ const existing = await this.loadFromDisk(id);
45
+ const now = Date.now();
46
+ const full = existing
47
+ ? {
48
+ id,
49
+ messages: data.messages,
50
+ createdAt: existing.createdAt,
51
+ updatedAt: now,
52
+ provider: data.provider ?? existing.provider,
53
+ model: data.model ?? existing.model,
54
+ metadata: data.metadata ?? existing.metadata,
55
+ }
56
+ : {
57
+ id,
58
+ messages: data.messages,
59
+ createdAt: now,
60
+ updatedAt: now,
61
+ provider: data.provider,
62
+ model: data.model,
63
+ metadata: data.metadata,
64
+ };
65
+ const filePath = this.filePath(id);
66
+ const tmpPath = filePath + ".tmp." + Date.now();
67
+ try {
68
+ await fs.writeFile(tmpPath, JSON.stringify(full, null, 2), "utf-8");
69
+ await fs.rename(tmpPath, filePath);
70
+ }
71
+ catch (err) {
72
+ // Clean up orphaned temp file on rename failure (e.g. cross-device move)
73
+ try {
74
+ await fs.unlink(tmpPath);
75
+ }
76
+ catch { /* best effort */ }
77
+ throw err;
78
+ }
79
+ }
80
+ async load(id) {
81
+ return this.loadFromDisk(id);
82
+ }
83
+ async delete(id) {
84
+ validateSessionId(id);
85
+ try {
86
+ await fs.unlink(this.filePath(id));
87
+ }
88
+ catch {
89
+ // File doesn't exist — nothing to delete
90
+ }
91
+ }
92
+ async list() {
93
+ await this.ensureDir();
94
+ const entries = await fs.readdir(this.basePath);
95
+ return entries
96
+ .filter((name) => name.endsWith(".json"))
97
+ .map((name) => name.slice(0, -".json".length));
98
+ }
99
+ async loadFromDisk(id) {
100
+ try {
101
+ const raw = await fs.readFile(this.filePath(id), "utf-8");
102
+ return JSON.parse(raw);
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ }
109
+ // ── In-memory PersistenceBackend ────────────────────────────────────────
110
+ /**
111
+ * In-memory persistence backend backed by a Map. Useful for testing.
112
+ */
113
+ export class MemoryPersistenceBackend {
114
+ __persistenceBackend = true;
115
+ store = new Map();
116
+ async save(id, data) {
117
+ validateSessionId(id);
118
+ const existing = this.store.get(id);
119
+ const now = Date.now();
120
+ this.store.set(id, {
121
+ id,
122
+ messages: data.messages,
123
+ createdAt: existing?.createdAt ?? now,
124
+ updatedAt: now,
125
+ provider: data.provider ?? existing?.provider,
126
+ model: data.model ?? existing?.model,
127
+ metadata: data.metadata ?? existing?.metadata,
128
+ });
129
+ }
130
+ async load(id) {
131
+ return this.store.get(id) ?? null;
132
+ }
133
+ async delete(id) {
134
+ this.store.delete(id);
135
+ }
136
+ async list() {
137
+ return Array.from(this.store.keys());
138
+ }
139
+ }
140
+ const registry = new Map();
141
+ // Register built-in backends
142
+ registry.set("file", (config) => new FilePersistenceBackend(config.path ?? defaultSessionPath()));
143
+ registry.set("memory", () => new MemoryPersistenceBackend());
144
+ /**
145
+ * Register a custom persistence backend factory.
146
+ *
147
+ * @param type Unique backend identifier (e.g., "redis", "sqlite")
148
+ * @param factory Factory function that creates a `PersistenceBackend` from config
149
+ */
150
+ export function registerBackend(type, factory) {
151
+ registry.set(type, factory);
152
+ }
153
+ /**
154
+ * Create a persistence backend from a config object.
155
+ * Uses the `type` field to look up the registered factory.
156
+ *
157
+ * @throws Error if `type` is not registered
158
+ */
159
+ export function createPersistenceBackend(config) {
160
+ const factory = registry.get(config.type);
161
+ if (!factory) {
162
+ throw new Error(`Unknown persistence backend type "${config.type}". Registered types: ${Array.from(registry.keys()).join(", ")}`);
163
+ }
164
+ return factory(config);
165
+ }
166
+ // ── Save orchestration ──────────────────────────────────────────────────
167
+ /**
168
+ * Persist a session's messages to the backend.
169
+ *
170
+ * Single source of truth for the save step shared by all adapters (SDK, CLI,
171
+ * Server). The backend owns `createdAt` (assigns it on first save, preserves
172
+ * it on overwrite — see FilePersistenceBackend / MemoryPersistenceBackend) and
173
+ * merges optional `provider`/`model`/`metadata` fields, so callers only pass
174
+ * what they know. Adapters that don't track provider/model (the SDK) omit them
175
+ * and the persisted values are left untouched.
176
+ */
177
+ export async function persistSession(backend, sessionId, messages, opts) {
178
+ await backend.save(sessionId, {
179
+ id: sessionId,
180
+ messages,
181
+ // createdAt is required by the SessionData type but ignored by the
182
+ // backends — they assign it on first save and preserve it on overwrite.
183
+ updatedAt: Date.now(),
184
+ provider: opts?.provider,
185
+ model: opts?.model,
186
+ metadata: opts?.metadata,
187
+ });
188
+ }
189
+ // ── Deprecated legacy API ───────────────────────────────────────────────
190
+ /**
191
+ * @deprecated Use `FilePersistenceBackend` or `createPersistenceBackend({ type: "file", path })` instead.
192
+ */
193
+ class FileSessionStore {
194
+ backend;
195
+ constructor(basePath) {
196
+ this.backend = new FilePersistenceBackend(basePath);
197
+ }
198
+ async save(sessionId, messages) {
199
+ await this.backend.save(sessionId, { id: sessionId, messages, createdAt: Date.now(), updatedAt: Date.now() });
200
+ }
201
+ async load(sessionId) {
202
+ const data = await this.backend.load(sessionId);
203
+ return data?.messages ?? null;
204
+ }
205
+ async delete(sessionId) {
206
+ await this.backend.delete(sessionId);
207
+ }
208
+ async list() {
209
+ return this.backend.list();
210
+ }
211
+ }
212
+ /**
213
+ * @deprecated Use `MemoryPersistenceBackend` or `createPersistenceBackend({ type: "memory" })` instead.
214
+ */
215
+ class MemorySessionStore {
216
+ backend;
217
+ constructor() {
218
+ this.backend = new MemoryPersistenceBackend();
219
+ }
220
+ async save(sessionId, messages) {
221
+ await this.backend.save(sessionId, { id: sessionId, messages, createdAt: Date.now(), updatedAt: Date.now() });
222
+ }
223
+ async load(sessionId) {
224
+ const data = await this.backend.load(sessionId);
225
+ return data?.messages ?? null;
226
+ }
227
+ async delete(sessionId) {
228
+ await this.backend.delete(sessionId);
229
+ }
230
+ async list() {
231
+ return this.backend.list();
232
+ }
233
+ }
234
+ /**
235
+ * @deprecated Use `createPersistenceBackend({ type: "file", path })` instead.
236
+ */
237
+ export function createSessionStore(path) {
238
+ return new FileSessionStore(path ?? defaultSessionPath());
239
+ }
240
+ /**
241
+ * @deprecated Use `createPersistenceBackend({ type: "memory" })` instead.
242
+ */
243
+ export function createMemoryStore() {
244
+ return new MemorySessionStore();
245
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Zoe Core — Settings Manager
3
+ *
4
+ * Single source of truth for reading, writing, validating, and persisting
5
+ * settings. Adapters (CLI, SDK, Server) delegate to this class.
6
+ */
7
+ import { ZoeError } from './errors.js';
8
+ import { SettingsCategory } from './settings-schema.js';
9
+ export declare class SettingsError extends ZoeError {
10
+ constructor(message: string, code?: string);
11
+ }
12
+ export interface SettingValue {
13
+ value: unknown;
14
+ origin: string;
15
+ masked: boolean;
16
+ }
17
+ export interface SettingEntry extends SettingValue {
18
+ dotKey: string;
19
+ category: SettingsCategory;
20
+ restartRequired: boolean;
21
+ label: string;
22
+ }
23
+ export interface SettingsManagerOptions {
24
+ config: Record<string, any>;
25
+ projectConfigPath?: string;
26
+ globalConfigPath?: string;
27
+ projectConfig?: Record<string, any>;
28
+ globalConfig?: Record<string, any>;
29
+ }
30
+ export declare class SettingsManager {
31
+ private config;
32
+ private projectConfigPath?;
33
+ private globalConfigPath?;
34
+ private projectConfig;
35
+ private globalConfig;
36
+ private listeners;
37
+ constructor(options: SettingsManagerOptions);
38
+ get(dotKey: string): SettingValue;
39
+ list(): SettingEntry[];
40
+ listByCategory(): Record<string, SettingEntry[]>;
41
+ set(dotKey: string, rawValue: string): Promise<void>;
42
+ reset(dotKey: string): Promise<void>;
43
+ resetAll(): Promise<void>;
44
+ onChange(callback: (changedKeys: string[]) => void): () => void;
45
+ private emitChange;
46
+ private validateValue;
47
+ private resolveConfigOrigin;
48
+ private parseEnvValue;
49
+ private resolveWriteTarget;
50
+ private readConfigFile;
51
+ private persist;
52
+ private getValueByPath;
53
+ private applyValueToConfig;
54
+ private removeValueFromConfig;
55
+ private hasPath;
56
+ private maskValue;
57
+ }
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Zoe Core — Settings Manager
3
+ *
4
+ * Single source of truth for reading, writing, validating, and persisting
5
+ * settings. Adapters (CLI, SDK, Server) delegate to this class.
6
+ */
7
+ import * as fs from 'fs/promises';
8
+ import * as path from 'path';
9
+ import { ZoeError } from './errors.js';
10
+ import { SETTINGS_MAP, SETTINGS_SCHEMA, ENV_VAR_MAP, isSecretField, } from './settings-schema.js';
11
+ // ── Settings Error ────────────────────────────────────────────────────────
12
+ export class SettingsError extends ZoeError {
13
+ constructor(message, code = 'SETTINGS_ERROR') {
14
+ super(message, code, false);
15
+ this.name = 'SettingsError';
16
+ }
17
+ }
18
+ // ── SettingsManager ───────────────────────────────────────────────────────
19
+ export class SettingsManager {
20
+ config;
21
+ projectConfigPath;
22
+ globalConfigPath;
23
+ projectConfig;
24
+ globalConfig;
25
+ listeners = [];
26
+ constructor(options) {
27
+ this.config = { ...options.config };
28
+ this.projectConfigPath = options.projectConfigPath;
29
+ this.globalConfigPath = options.globalConfigPath;
30
+ this.projectConfig = options.projectConfig ?? {};
31
+ this.globalConfig = options.globalConfig ?? {};
32
+ }
33
+ // ── Read ───────────────────────────────────────────────────────────────
34
+ get(dotKey) {
35
+ const entry = SETTINGS_MAP.get(dotKey);
36
+ if (!entry) {
37
+ throw new SettingsError(`Unknown setting: ${dotKey}. Use /settings list to see available keys.`, 'SETTINGS_INVALID_KEY');
38
+ }
39
+ const schema = SETTINGS_SCHEMA.get(dotKey);
40
+ const secret = isSecretField(dotKey);
41
+ // Check env var first (empty string = not explicitly set → fall through to default)
42
+ const envVar = ENV_VAR_MAP.get(dotKey);
43
+ if (envVar && process.env[envVar]) {
44
+ const raw = this.parseEnvValue(process.env[envVar], schema);
45
+ const value = secret && raw != null ? this.maskValue(String(raw)) : raw;
46
+ return { value, origin: `env: ${envVar}`, masked: secret };
47
+ }
48
+ // Check config, falling back to schema default
49
+ const raw = this.getValueByPath(this.config, entry.configPath);
50
+ const effectiveValue = raw ?? schema?.default;
51
+ const value = secret && effectiveValue != null ? this.maskValue(String(effectiveValue)) : effectiveValue;
52
+ const origin = raw !== undefined && raw !== null
53
+ ? this.resolveConfigOrigin(entry.configPath)
54
+ : schema?.default !== undefined ? 'default' : 'default';
55
+ return { value, origin, masked: secret };
56
+ }
57
+ list() {
58
+ const results = [];
59
+ for (const [dotKey, mapEntry] of SETTINGS_MAP) {
60
+ const schema = SETTINGS_SCHEMA.get(dotKey);
61
+ const { value, origin, masked } = this.get(dotKey);
62
+ results.push({
63
+ dotKey,
64
+ value,
65
+ origin,
66
+ masked,
67
+ category: mapEntry.category,
68
+ restartRequired: schema?.restartRequired ?? false,
69
+ label: mapEntry.label,
70
+ });
71
+ }
72
+ return results;
73
+ }
74
+ listByCategory() {
75
+ const result = {};
76
+ for (const entry of this.list()) {
77
+ if (!result[entry.category])
78
+ result[entry.category] = [];
79
+ result[entry.category].push(entry);
80
+ }
81
+ return result;
82
+ }
83
+ // ── Write ──────────────────────────────────────────────────────────────
84
+ async set(dotKey, rawValue) {
85
+ const mapEntry = SETTINGS_MAP.get(dotKey);
86
+ if (!mapEntry) {
87
+ throw new SettingsError(`Unknown setting: ${dotKey}. Use /settings list to see available keys.`, 'SETTINGS_INVALID_KEY');
88
+ }
89
+ const schema = SETTINGS_SCHEMA.get(dotKey);
90
+ const value = this.validateValue(dotKey, rawValue, schema);
91
+ // Determine write target
92
+ const writePath = this.resolveWriteTarget(dotKey);
93
+ if (!writePath) {
94
+ throw new SettingsError('No config file path available for writing.', 'SETTINGS_WRITE_FAILED');
95
+ }
96
+ // Check env var override
97
+ const envVar = ENV_VAR_MAP.get(dotKey);
98
+ if (envVar && process.env[envVar]) {
99
+ console.warn(`Note: This key is overridden by env var ${envVar}. Saving to config. The env var takes precedence until unset.`);
100
+ }
101
+ // Read current file, apply change, write
102
+ const fileConfig = await this.readConfigFile(writePath);
103
+ this.applyValueToConfig(fileConfig, mapEntry.configPath, value);
104
+ await this.persist(writePath, fileConfig);
105
+ // Update in-memory
106
+ this.applyValueToConfig(this.config, mapEntry.configPath, value);
107
+ this.emitChange([dotKey]);
108
+ }
109
+ async reset(dotKey) {
110
+ const mapEntry = SETTINGS_MAP.get(dotKey);
111
+ if (!mapEntry) {
112
+ throw new SettingsError(`Unknown setting: ${dotKey}. Use /settings list to see available keys.`, 'SETTINGS_INVALID_KEY');
113
+ }
114
+ const envVar = ENV_VAR_MAP.get(dotKey);
115
+ if (envVar && process.env[envVar]) {
116
+ throw new SettingsError(`Cannot reset: this value is set by env var ${envVar}. Unset the environment variable to use the default.`, 'SETTINGS_WRITE_FAILED');
117
+ }
118
+ const writePath = this.resolveWriteTarget(dotKey);
119
+ if (!writePath) {
120
+ throw new SettingsError('No config file path available for writing.', 'SETTINGS_WRITE_FAILED');
121
+ }
122
+ const fileConfig = await this.readConfigFile(writePath);
123
+ this.removeValueFromConfig(fileConfig, mapEntry.configPath);
124
+ await this.persist(writePath, fileConfig);
125
+ // Reset in-memory to default
126
+ const schema = SETTINGS_SCHEMA.get(dotKey);
127
+ if (schema?.default !== undefined) {
128
+ this.applyValueToConfig(this.config, mapEntry.configPath, schema.default);
129
+ }
130
+ else {
131
+ this.removeValueFromConfig(this.config, mapEntry.configPath);
132
+ }
133
+ this.emitChange([dotKey]);
134
+ }
135
+ async resetAll() {
136
+ const writePath = this.projectConfigPath ?? this.globalConfigPath;
137
+ if (!writePath)
138
+ return;
139
+ await this.persist(writePath, {});
140
+ // Rebuild config from env vars (preserving env overrides)
141
+ const rebuilt = {};
142
+ for (const [dotKey, envVar] of ENV_VAR_MAP) {
143
+ const val = process.env[envVar];
144
+ if (val != null) {
145
+ const entry = SETTINGS_MAP.get(dotKey);
146
+ if (entry)
147
+ this.applyValueToConfig(rebuilt, entry.configPath, val);
148
+ }
149
+ }
150
+ this.config = rebuilt;
151
+ this.emitChange(['*']);
152
+ }
153
+ // ── Events ─────────────────────────────────────────────────────────────
154
+ onChange(callback) {
155
+ this.listeners.push(callback);
156
+ return () => {
157
+ this.listeners = this.listeners.filter(l => l !== callback);
158
+ };
159
+ }
160
+ // ── Internals ──────────────────────────────────────────────────────────
161
+ emitChange(changedKeys) {
162
+ for (const listener of this.listeners) {
163
+ try {
164
+ listener(changedKeys);
165
+ }
166
+ catch { /* non-fatal */ }
167
+ }
168
+ }
169
+ validateValue(dotKey, raw, schema) {
170
+ if (!schema)
171
+ return raw;
172
+ switch (schema.type) {
173
+ case 'number': {
174
+ const num = Number(raw);
175
+ if (isNaN(num)) {
176
+ throw new SettingsError(`${dotKey} must be a number. Got: "${raw}"`, 'SETTINGS_VALIDATION_FAILED');
177
+ }
178
+ if (schema.min !== undefined && num < schema.min) {
179
+ throw new SettingsError(`${dotKey} must be >= ${schema.min}. Got: ${num}`, 'SETTINGS_VALIDATION_FAILED');
180
+ }
181
+ if (schema.max !== undefined && num > schema.max) {
182
+ throw new SettingsError(`${dotKey} must be <= ${schema.max}. Got: ${num}`, 'SETTINGS_VALIDATION_FAILED');
183
+ }
184
+ return num;
185
+ }
186
+ case 'boolean': {
187
+ const lower = raw.toLowerCase();
188
+ if (lower === 'true' || lower === '1')
189
+ return true;
190
+ if (lower === 'false' || lower === '0')
191
+ return false;
192
+ throw new SettingsError(`${dotKey} must be true or false. Got: "${raw}"`, 'SETTINGS_VALIDATION_FAILED');
193
+ }
194
+ case 'enum': {
195
+ if (!schema.enumValues?.includes(raw)) {
196
+ throw new SettingsError(`${dotKey} must be one of: ${schema.enumValues?.join(', ')}. Got: "${raw}"`, 'SETTINGS_VALIDATION_FAILED');
197
+ }
198
+ return raw;
199
+ }
200
+ case 'string': {
201
+ // URL validation for baseUrl/webhook fields
202
+ if (dotKey.includes('baseUrl') || dotKey.includes('.webhook')) {
203
+ try {
204
+ const parsed = new URL(raw);
205
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
206
+ throw new SettingsError(`${dotKey} must be a valid URL starting with http:// or https://.`, 'SETTINGS_VALIDATION_FAILED');
207
+ }
208
+ if (parsed.username || parsed.password) {
209
+ throw new SettingsError(`${dotKey} must not contain embedded credentials.`, 'SETTINGS_VALIDATION_FAILED');
210
+ }
211
+ }
212
+ catch (e) {
213
+ if (e instanceof SettingsError)
214
+ throw e;
215
+ throw new SettingsError(`${dotKey} must be a valid URL starting with http:// or https://.`, 'SETTINGS_VALIDATION_FAILED');
216
+ }
217
+ }
218
+ // Hostname validation
219
+ if (dotKey === 'smtp.host') {
220
+ if (!/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?$/.test(raw)) {
221
+ throw new SettingsError(`${dotKey} must be a valid hostname.`, 'SETTINGS_VALIDATION_FAILED');
222
+ }
223
+ }
224
+ return raw;
225
+ }
226
+ default:
227
+ return raw;
228
+ }
229
+ }
230
+ resolveConfigOrigin(configPath) {
231
+ if (this.hasPath(this.projectConfig, configPath))
232
+ return 'project config';
233
+ if (this.hasPath(this.globalConfig, configPath))
234
+ return 'global config';
235
+ return 'default';
236
+ }
237
+ parseEnvValue(raw, schema) {
238
+ const val = raw ?? '';
239
+ if (!schema)
240
+ return val;
241
+ switch (schema.type) {
242
+ case 'boolean':
243
+ return val.toLowerCase() === 'true' || val === '1';
244
+ case 'number':
245
+ return Number(val);
246
+ default:
247
+ return val;
248
+ }
249
+ }
250
+ resolveWriteTarget(dotKey) {
251
+ const envVar = ENV_VAR_MAP.get(dotKey);
252
+ if (envVar && process.env[envVar]) {
253
+ // Write to project config when env var overrides
254
+ return this.projectConfigPath ?? this.globalConfigPath;
255
+ }
256
+ // Write to whichever config file currently owns the key
257
+ const entry = SETTINGS_MAP.get(dotKey);
258
+ if (!entry)
259
+ return this.projectConfigPath ?? this.globalConfigPath;
260
+ if (this.hasPath(this.projectConfig, entry.configPath) && this.projectConfigPath) {
261
+ return this.projectConfigPath;
262
+ }
263
+ if (this.hasPath(this.globalConfig, entry.configPath) && this.globalConfigPath) {
264
+ return this.globalConfigPath;
265
+ }
266
+ // New key — prefer project config if it exists
267
+ return this.projectConfigPath ?? this.globalConfigPath;
268
+ }
269
+ async readConfigFile(filePath) {
270
+ try {
271
+ const content = await fs.readFile(filePath, 'utf-8');
272
+ return JSON.parse(content);
273
+ }
274
+ catch {
275
+ return {};
276
+ }
277
+ }
278
+ async persist(configPath, data) {
279
+ try {
280
+ const dir = path.dirname(configPath);
281
+ await fs.mkdir(dir, { recursive: true });
282
+ // Backup existing file
283
+ try {
284
+ await fs.rename(configPath, configPath + '.bak');
285
+ }
286
+ catch { /* no existing file to back up */ }
287
+ // Atomic write via temp file
288
+ const tmpPath = configPath + `.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
289
+ await fs.writeFile(tmpPath, JSON.stringify(data, null, 2), { mode: 0o600 });
290
+ await fs.rename(tmpPath, configPath);
291
+ }
292
+ catch (e) {
293
+ console.warn(`Warning: could not save to ${configPath}: ${e.message}. Change applied in-memory only.`);
294
+ }
295
+ }
296
+ getValueByPath(obj, pathParts) {
297
+ let current = obj;
298
+ for (const part of pathParts) {
299
+ if (current == null || typeof current !== 'object')
300
+ return undefined;
301
+ current = current[part];
302
+ }
303
+ return current;
304
+ }
305
+ applyValueToConfig(obj, pathParts, value) {
306
+ if (pathParts.length === 0)
307
+ return;
308
+ // Deep merge for models map
309
+ if (pathParts[0] === 'models' && pathParts.length === 3) {
310
+ if (!obj.models)
311
+ obj.models = {};
312
+ const provider = pathParts[1];
313
+ if (!obj.models[provider])
314
+ obj.models[provider] = {};
315
+ obj.models[provider][pathParts[2]] = value;
316
+ return;
317
+ }
318
+ // Standard path
319
+ let current = obj;
320
+ for (let i = 0; i < pathParts.length - 1; i++) {
321
+ if (current[pathParts[i]] == null || typeof current[pathParts[i]] !== 'object') {
322
+ current[pathParts[i]] = {};
323
+ }
324
+ current = current[pathParts[i]];
325
+ }
326
+ current[pathParts[pathParts.length - 1]] = value;
327
+ }
328
+ removeValueFromConfig(obj, pathParts) {
329
+ if (pathParts.length === 0)
330
+ return;
331
+ if (pathParts.length === 1) {
332
+ delete obj[pathParts[0]];
333
+ return;
334
+ }
335
+ let current = obj;
336
+ for (let i = 0; i < pathParts.length - 1; i++) {
337
+ if (current[pathParts[i]] == null)
338
+ return;
339
+ current = current[pathParts[i]];
340
+ }
341
+ delete current[pathParts[pathParts.length - 1]];
342
+ }
343
+ hasPath(obj, pathParts) {
344
+ let current = obj;
345
+ for (const part of pathParts) {
346
+ if (current == null || typeof current !== 'object')
347
+ return false;
348
+ if (!(part in current))
349
+ return false;
350
+ current = current[part];
351
+ }
352
+ return true;
353
+ }
354
+ maskValue(value) {
355
+ if (!value || value.length < 8)
356
+ return '******';
357
+ return `${value.slice(0, 3)}...${value.slice(-4)}`;
358
+ }
359
+ }