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,232 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { getAllToolDefinitions } from '../../core/tool-executor.js';
4
+ import { buildSystemPrompt } from './system-prompts.js';
5
+ import { initializeSkillRegistry } from '../../skills/index.js';
6
+ import { runAgentLoop } from '../../core/agent-loop.js';
7
+ import { generateId, now } from '../../core/message-convert.js';
8
+ import { createHookExecutor } from '../../core/hooks.js';
9
+ import { buildSkillCatalog } from '../../core/skill-catalog.js';
10
+ import { DEFAULT_MODELS } from '../../models-catalog.js';
11
+ import { persistSession } from '../../core/session-store.js';
12
+ export class Agent {
13
+ provider;
14
+ messages;
15
+ model;
16
+ config;
17
+ autoConfirm;
18
+ skillRegistry = null;
19
+ skillCatalog = '';
20
+ abortController = null;
21
+ _middleware = [];
22
+ systemPrompt;
23
+ providerType;
24
+ persistence;
25
+ sessionId;
26
+ constructor(provider, model = DEFAULT_MODELS['openai-compatible'], config = {}, systemPrompt, persistence = null, providerType) {
27
+ this.provider = provider;
28
+ this.model = model;
29
+ this.config = config;
30
+ this.autoConfirm = !!config?.autoConfirm;
31
+ // Default to the headless/Docker prompt; the caller (repl.ts) selects the
32
+ // interactive prompt when launching in a TTY. Kept mode-agnostic here so
33
+ // Core's runAgentLoop never needs to know about launch mode.
34
+ this.systemPrompt = systemPrompt ?? buildSystemPrompt();
35
+ this.providerType = providerType;
36
+ this.persistence = persistence;
37
+ this.sessionId = generateId();
38
+ this.messages = [{
39
+ id: generateId(),
40
+ role: "system",
41
+ content: this.systemPrompt,
42
+ timestamp: now(),
43
+ }];
44
+ }
45
+ async initializeSkills() {
46
+ try {
47
+ this.skillRegistry = await initializeSkillRegistry(process.cwd());
48
+ const metadata = this.skillRegistry.getMetadata();
49
+ if (metadata.length > 0) {
50
+ // Build and store skill catalog — will be injected by runAgentLoop
51
+ this.skillCatalog = buildSkillCatalog(metadata);
52
+ console.log(chalk.green(`Loaded ${metadata.length} skill(s):`));
53
+ for (const s of metadata) {
54
+ console.log(chalk.dim(` - ${s.name}`));
55
+ }
56
+ }
57
+ }
58
+ catch (error) {
59
+ console.warn(chalk.yellow(`Warning: Skills initialization failed: ${error.message}`));
60
+ }
61
+ }
62
+ getSkillRegistry() {
63
+ return this.skillRegistry;
64
+ }
65
+ /** Set middleware pipeline (e.g., gateway semantic injection). */
66
+ setMiddleware(middleware) {
67
+ this._middleware = middleware;
68
+ }
69
+ async chat(userInput, signal, approveTool, permissionLevel, onStep) {
70
+ // @path references are resolved by the caller (repl.ts / use-agent.ts),
71
+ // not here — one resolution site per caller (T022).
72
+ this.messages.push({ id: generateId(), role: "user", content: userInput, timestamp: now() });
73
+ // When a custom onStep is supplied (TUI mode), the caller owns rendering:
74
+ // skip the ora spinner and chalk finish messages, and return the loop
75
+ // result so the caller renders terminal states. The readline path passes
76
+ // no onStep and stays byte-identical.
77
+ const customSteps = !!onStep;
78
+ const spinner = customSteps ? null : ora('Thinking...').start();
79
+ let wrappedApproveTool = approveTool;
80
+ if (!customSteps && approveTool && spinner) {
81
+ wrappedApproveTool = async (call) => {
82
+ spinner.stop();
83
+ try {
84
+ return await approveTool(call);
85
+ }
86
+ finally {
87
+ spinner.start();
88
+ }
89
+ };
90
+ }
91
+ const defaultOnStep = (step) => {
92
+ if (!spinner)
93
+ return;
94
+ if (step.type === "text" && step.content) {
95
+ spinner.stop();
96
+ console.log(chalk.blue("Zoe: ") + step.content);
97
+ spinner.start();
98
+ }
99
+ else if (step.type === "tool_call" && step.toolCall) {
100
+ spinner.stop();
101
+ console.log(chalk.gray(`Executing tool: ${step.toolCall.name}...`));
102
+ spinner.start();
103
+ }
104
+ };
105
+ try {
106
+ const result = await runAgentLoop({
107
+ provider: this.provider,
108
+ model: this.model,
109
+ messages: this.messages,
110
+ toolDefs: getAllToolDefinitions(),
111
+ skillCatalog: this.skillCatalog || undefined,
112
+ maxSteps: 30,
113
+ hooks: createHookExecutor(),
114
+ config: { ...this.config, agentName: 'cli' },
115
+ signal,
116
+ approveTool: wrappedApproveTool,
117
+ permissionLevel,
118
+ autoConfirm: this.autoConfirm,
119
+ middleware: this._middleware.length > 0 ? this._middleware : undefined,
120
+ onStep: onStep ?? defaultOnStep,
121
+ // Stream only when the caller supplies its own onStep (TUI mode) — the
122
+ // readline default handler prints complete 'text' steps, not deltas.
123
+ stream: customSteps,
124
+ });
125
+ spinner?.stop();
126
+ if (!customSteps) {
127
+ if (result.finishReason === "aborted") {
128
+ console.log(chalk.yellow("\n(Interrupted)"));
129
+ }
130
+ else if (result.finishReason === "max_steps") {
131
+ console.log(chalk.yellow("\n(Max steps reached — the agent needed more iterations to complete. Try increasing maxSteps or asking a more specific question.)"));
132
+ }
133
+ else if (result.error) {
134
+ console.error(chalk.red(`Error: ${result.error.message}`));
135
+ }
136
+ }
137
+ return { finishReason: result.finishReason, error: result.error?.message, usage: result.usage };
138
+ }
139
+ catch (error) {
140
+ spinner?.stop();
141
+ if (error.name === 'AbortError' || signal?.aborted) {
142
+ if (!customSteps)
143
+ console.log(chalk.yellow("\n(Interrupted)"));
144
+ return { finishReason: 'aborted' };
145
+ }
146
+ if (!customSteps)
147
+ console.error(chalk.red(error.message));
148
+ return { finishReason: 'error', error: error.message };
149
+ }
150
+ finally {
151
+ // Persist after every turn (success, abort, or error) so partial output
152
+ // survives a restart. Save is best-effort: a persistence failure must
153
+ // never crash the chat path. (Mirrors SDK agent.ts error handling.)
154
+ if (this.persistence) {
155
+ try {
156
+ await persistSession(this.persistence, this.sessionId, this.messages, {
157
+ provider: this.providerType,
158
+ model: this.model,
159
+ });
160
+ }
161
+ catch { /* persistence is best-effort */ }
162
+ }
163
+ }
164
+ }
165
+ /**
166
+ * Load a previously persisted session by id, replacing the in-memory history.
167
+ * Re-seeds the system message if the loaded set has none. No-op when no
168
+ * backend is configured.
169
+ */
170
+ async loadSession(sessionId) {
171
+ if (!this.persistence)
172
+ return false;
173
+ const data = await this.persistence.load(sessionId);
174
+ if (!data)
175
+ return false;
176
+ this.sessionId = sessionId;
177
+ const hasSystem = data.messages.some(m => m.role === 'system');
178
+ this.messages = hasSystem
179
+ ? data.messages
180
+ : [{ id: generateId(), role: 'system', content: this.systemPrompt, timestamp: now() }, ...data.messages];
181
+ return true;
182
+ }
183
+ /** Active session id (rotated by `clearConversation` when persistence is on). */
184
+ getSessionId() {
185
+ return this.sessionId;
186
+ }
187
+ /** Configured persistence backend, or null when persistence is disabled. */
188
+ getPersistence() {
189
+ return this.persistence;
190
+ }
191
+ clearConversation() {
192
+ const systemPrompt = this.messages.find(m => m.role === 'system');
193
+ this.messages = systemPrompt
194
+ ? [systemPrompt]
195
+ : [{ id: generateId(), role: 'system', content: this.systemPrompt, timestamp: now() }];
196
+ // Rotate the session id so the next save writes a new file instead of
197
+ // overwriting the prior (now-superseded) session — it survives for resume.
198
+ if (this.persistence) {
199
+ this.sessionId = generateId();
200
+ }
201
+ }
202
+ /** Public accessor for the current message history. */
203
+ getMessages() {
204
+ return this.messages;
205
+ }
206
+ /** Replace the message history (e.g., after compaction). */
207
+ setMessages(messages) {
208
+ this.messages = messages;
209
+ }
210
+ /** Public accessor for the active LLM provider. */
211
+ getProvider() {
212
+ return this.provider;
213
+ }
214
+ /** Public accessor for the active model name. */
215
+ getModel() {
216
+ return this.model;
217
+ }
218
+ switchProvider(provider, model) {
219
+ this.provider = provider;
220
+ this.model = model;
221
+ }
222
+ abort() {
223
+ this.abortController?.abort();
224
+ }
225
+ createAbortSignal() {
226
+ this.abortController = new AbortController();
227
+ return this.abortController.signal;
228
+ }
229
+ clearAbortController() {
230
+ this.abortController = null;
231
+ }
232
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Zoe CLI — Shared session bootstrap
3
+ *
4
+ * The setup phase that both dispatch paths need: config load + merge,
5
+ * provider resolution (+ interactive setup wizard), permission level,
6
+ * Agent construction, skills init, gateway init, and the documents dir.
7
+ *
8
+ * Extracted verbatim from `runChat()` so the readline fallback and the
9
+ * Ink TUI share one setup path — no duplicated ~175 lines, and
10
+ * `zoe -n` stays byte-identical (the setup prints only the same
11
+ * interactive-gated status messages as before). UI chrome (welcome
12
+ * banner, "agent initialized", the readline loop) stays in the caller.
13
+ */
14
+ import { Agent } from './agent.js';
15
+ import type { PermissionLevel, PersistenceBackend } from '../../core/types.js';
16
+ export interface CliSessionContext {
17
+ agent: Agent;
18
+ fullConfig: any;
19
+ activeProviderType: string;
20
+ providerConfig: any;
21
+ permissionLevel: PermissionLevel | undefined;
22
+ gatewayInstance: any;
23
+ persistence: PersistenceBackend;
24
+ }
25
+ export declare function bootstrapCliSession(options: any): Promise<CliSessionContext>;
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Zoe CLI — Shared session bootstrap
3
+ *
4
+ * The setup phase that both dispatch paths need: config load + merge,
5
+ * provider resolution (+ interactive setup wizard), permission level,
6
+ * Agent construction, skills init, gateway init, and the documents dir.
7
+ *
8
+ * Extracted verbatim from `runChat()` so the readline fallback and the
9
+ * Ink TUI share one setup path — no duplicated ~175 lines, and
10
+ * `zoe -n` stays byte-identical (the setup prints only the same
11
+ * interactive-gated status messages as before). UI chrome (welcome
12
+ * banner, "agent initialized", the readline loop) stays in the caller.
13
+ */
14
+ import chalk from 'chalk';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+ import * as os from 'os';
18
+ import { Agent } from './agent.js';
19
+ import { resolveLaunchMode, selectSystemPrompt } from './system-prompts.js';
20
+ import { configureProviders, loadProviderConfig, getProvider, } from '../../core/provider-resolver.js';
21
+ import { loadJsonConfig, applyEnvOverrides, migrateLegacyFormat, getConfigPaths, } from './config-loader.js';
22
+ import { runSetup } from './setup.js';
23
+ import { isNonInteractive } from './docker-utils.js';
24
+ import { createPersistenceBackend } from '../../core/session-store.js';
25
+ import { resolvePermissionLevel } from '../../core/permission.js';
26
+ import { SettingsManager } from '../../core/settings-manager.js';
27
+ import { loadMergedConfig } from './config-loader.js';
28
+ export async function bootstrapCliSession(options) {
29
+ const { global: GLOBAL_CONFIG_FILE, local: LOCAL_CONFIG_FILE } = getConfigPaths();
30
+ // 1. Load and merge configs (local > global)
31
+ const globalConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
32
+ const localConfig = loadJsonConfig(LOCAL_CONFIG_FILE);
33
+ if (Object.keys(localConfig).length > 0 && options.interactive) {
34
+ console.log(chalk.dim(`Loaded project config from ${LOCAL_CONFIG_FILE}`));
35
+ }
36
+ let fullConfig = { ...globalConfig, ...localConfig };
37
+ // 2. Inject runtime flags
38
+ fullConfig.autoConfirm = options.yes || options.headless || options.docker || false;
39
+ // 2b. Resolve permission level from CLI flags, env var, and config
40
+ let permissionLevel;
41
+ const headless = options.headless || options.yes || options.docker;
42
+ if (!headless) {
43
+ const flagLevel = options.yolo ? "permissive"
44
+ : options.strict ? "strict"
45
+ : options.moderate ? "moderate"
46
+ : undefined;
47
+ permissionLevel = resolvePermissionLevel(flagLevel, process.env.ZOE_PERMISSION, fullConfig.permissionLevel);
48
+ }
49
+ // Warn about conflicting flags
50
+ if (headless && (options.strict || options.moderate || options.yolo)) {
51
+ const flag = options.strict ? '--strict' : options.moderate ? '--moderate' : '--yolo';
52
+ console.warn(`Warning: --headless overrides ${flag}. All tools will be auto-approved.`);
53
+ }
54
+ // 3. Auto-migrate legacy config format (top-level apiKey/baseUrl/model)
55
+ // Must run BEFORE applyEnvOverrides, which initializes models={} and would
56
+ // block the !config.models guard in migrateLegacyFormat.
57
+ fullConfig = migrateLegacyFormat(fullConfig, { model: options.model });
58
+ // 4. Apply env var overrides for tool settings
59
+ fullConfig = applyEnvOverrides(fullConfig);
60
+ // 5. Load provider config via unified resolution
61
+ const cliProvider = options.provider;
62
+ let multiConfig = loadProviderConfig(fullConfig, cliProvider);
63
+ if (!multiConfig) {
64
+ console.log(chalk.yellow("No provider configuration found."));
65
+ if (isNonInteractive()) {
66
+ console.error(chalk.red("No provider configured. Set API key env vars (OPENAI_API_KEY / ANTHROPIC_API_KEY / GLM_API_KEY) or provide a config file."));
67
+ process.exit(1);
68
+ }
69
+ else {
70
+ const inquirer = await import('inquirer');
71
+ const { doSetup } = await inquirer.default.prompt([
72
+ {
73
+ type: 'confirm',
74
+ name: 'doSetup',
75
+ message: 'Would you like to run the setup wizard now?',
76
+ default: true
77
+ }
78
+ ]);
79
+ if (doSetup) {
80
+ await runSetup();
81
+ const newConfig = loadJsonConfig(GLOBAL_CONFIG_FILE);
82
+ Object.assign(fullConfig, newConfig);
83
+ multiConfig = loadProviderConfig(fullConfig, cliProvider);
84
+ }
85
+ else {
86
+ console.error(chalk.red("Provider configuration is required to proceed."));
87
+ process.exit(1);
88
+ }
89
+ }
90
+ }
91
+ if (!multiConfig) {
92
+ console.error(chalk.red("Provider configuration is still missing. Exiting."));
93
+ process.exit(1);
94
+ }
95
+ configureProviders(multiConfig);
96
+ // Active provider: CLI --provider flag → multiConfig.default
97
+ const activeProviderType = cliProvider ?? multiConfig.default;
98
+ // getProvider handles model override so it's baked into the provider instance
99
+ const { provider, model } = await getProvider(activeProviderType, options.model);
100
+ const providerConfig = { type: activeProviderType, model };
101
+ // Select system prompt by launch mode: interactive (TUI/readline in a TTY)
102
+ // gets the interactive coding-agent prompt; headless/docker/piped keep
103
+ // the Docker-native prompt unchanged.
104
+ const launchMode = resolveLaunchMode(options);
105
+ const systemPrompt = selectSystemPrompt(launchMode);
106
+ // Session persistence — single file backend shared by the REPL, TUI, and the
107
+ // session selector overlay. Default path is ~/.zoe/sessions (see Core's
108
+ // defaultSessionPath()). Disabled backends can be added via registerBackend().
109
+ const persistence = createPersistenceBackend({ type: 'file' });
110
+ const agent = new Agent(provider, model, fullConfig, systemPrompt, persistence, activeProviderType);
111
+ // Initialize skills system
112
+ await agent.initializeSkills();
113
+ // Initialize gateway (if enabled)
114
+ let gatewayInstance = null;
115
+ try {
116
+ const settingsManager = new SettingsManager({
117
+ config: applyEnvOverrides(loadMergedConfig()),
118
+ projectConfigPath: LOCAL_CONFIG_FILE,
119
+ globalConfigPath: GLOBAL_CONFIG_FILE,
120
+ });
121
+ const gwEnabled = settingsManager.get('gateway.enabled').value;
122
+ if (gwEnabled) {
123
+ const gatewayConfig = {
124
+ enabled: true,
125
+ semanticTopK: settingsManager.get('gateway.semanticTopK').value,
126
+ defaultRateLimitPerMin: settingsManager.get('gateway.defaultRateLimitPerMin').value,
127
+ maxAuditLogsInMemory: settingsManager.get('gateway.maxAuditLogs').value,
128
+ };
129
+ const { GatewaySettingsAdapter } = await import('../../gateway/settings-adapter.js');
130
+ const gwStorageDir = process.env.ZOE_GATEWAY_DIR ?? path.join(os.homedir(), '.zoe');
131
+ const gwSettingsAdapter = new GatewaySettingsAdapter(gwStorageDir);
132
+ await gwSettingsAdapter.initialize();
133
+ const { createGateway } = await import('../../gateway/index.js');
134
+ gatewayInstance = await createGateway(gatewayConfig, gwSettingsAdapter);
135
+ if (gatewayInstance) {
136
+ const { semanticToolInjectionMiddleware } = await import('../../core/middleware/semantic-tools.js');
137
+ agent.setMiddleware([semanticToolInjectionMiddleware(gatewayInstance, gatewayConfig.semanticTopK)]);
138
+ if (options.interactive) {
139
+ console.log(chalk.green('Gateway initialized'));
140
+ }
141
+ }
142
+ }
143
+ }
144
+ catch (e) {
145
+ console.warn(chalk.yellow(`Gateway initialization skipped: ${e instanceof Error ? e.message : String(e)}`));
146
+ }
147
+ // Ensure ~/zoe_documents exists
148
+ const docsDir = path.join(os.homedir(), 'zoe_documents');
149
+ if (!fs.existsSync(docsDir)) {
150
+ fs.mkdirSync(docsDir, { recursive: true });
151
+ for (const sub of ['notes', 'templates', 'output', 'knowledge']) {
152
+ fs.mkdirSync(path.join(docsDir, sub), { recursive: true });
153
+ }
154
+ }
155
+ // Session TTL cleanup — sweep expired sessions on startup (once, no timer).
156
+ // Runs before --resume so an expired target is gone before we try to load it.
157
+ try {
158
+ const settingsManager = new SettingsManager({
159
+ config: applyEnvOverrides(loadMergedConfig()),
160
+ projectConfigPath: LOCAL_CONFIG_FILE,
161
+ globalConfigPath: GLOBAL_CONFIG_FILE,
162
+ });
163
+ const maxAgeDays = settingsManager.get('sessions.maxAgeDays').value;
164
+ if (maxAgeDays && maxAgeDays > 0) {
165
+ const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000;
166
+ const cutoff = Date.now() - maxAgeMs;
167
+ const ids = await persistence.list();
168
+ await Promise.all(ids.map(async (id) => {
169
+ const data = await persistence.load(id);
170
+ if (data && data.updatedAt < cutoff)
171
+ await persistence.delete(id);
172
+ }));
173
+ }
174
+ }
175
+ catch { /* best-effort — never block startup on cleanup */ }
176
+ // --resume <id|last> — load a session before the REPL/TUI starts.
177
+ if (options.resume) {
178
+ let resumeId = options.resume;
179
+ if (resumeId === 'last') {
180
+ const ids = await persistence.list();
181
+ if (ids.length === 0) {
182
+ console.error(chalk.red('No saved sessions to resume.'));
183
+ process.exit(1);
184
+ }
185
+ const loaded = await Promise.all(ids.map((id) => persistence.load(id)));
186
+ const mostRecent = loaded
187
+ .filter((s) => s != null)
188
+ .sort((a, b) => b.updatedAt - a.updatedAt)[0];
189
+ if (!mostRecent) {
190
+ console.error(chalk.red('No saved sessions to resume.'));
191
+ process.exit(1);
192
+ }
193
+ resumeId = mostRecent.id;
194
+ }
195
+ const ok = await agent.loadSession(resumeId);
196
+ if (!ok) {
197
+ console.error(chalk.red(`Session "${resumeId}" not found. Use /sessions in the TUI to list available sessions.`));
198
+ process.exit(1);
199
+ }
200
+ if (options.interactive !== false)
201
+ console.log(chalk.dim(`Resumed session ${resumeId.slice(0, 8)}.`));
202
+ }
203
+ return { agent, fullConfig, activeProviderType, providerConfig, permissionLevel, gatewayInstance, persistence };
204
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Build the shared slash-command registry.
3
+ *
4
+ * Single owner of which commands exist and how they're wired — used by both
5
+ * the readline REPL (`repl.ts`) and the Ink TUI (`tui/`). Handlers follow one
6
+ * contract: they return `{ output, exit }` and never write to stdout directly.
7
+ *
8
+ * `interactive: true` marks handlers that own stdin/stdout (inquirer wizards,
9
+ * ora spinners, the setup wizard). They run in the readline REPL but the TUI
10
+ * defers them (Ink owns stdin there).
11
+ */
12
+ import type { Agent } from '../agent.js';
13
+ import { CommandRegistry } from './registry.js';
14
+ export declare function buildCommandRegistry(agent: Agent, config: any, activeProviderType: string, gatewayInstance?: any): CommandRegistry;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Build the shared slash-command registry.
3
+ *
4
+ * Single owner of which commands exist and how they're wired — used by both
5
+ * the readline REPL (`repl.ts`) and the Ink TUI (`tui/`). Handlers follow one
6
+ * contract: they return `{ output, exit }` and never write to stdout directly.
7
+ *
8
+ * `interactive: true` marks handlers that own stdin/stdout (inquirer wizards,
9
+ * ora spinners, the setup wizard). They run in the readline REPL but the TUI
10
+ * defers them (Ink owns stdin there).
11
+ */
12
+ import { CommandRegistry } from './registry.js';
13
+ import { createHelpHandler } from './help.js';
14
+ import { clearHandler } from './clear.js';
15
+ import { exitHandler } from './exit.js';
16
+ import { compactHandler } from './compact.js';
17
+ import { skillsHandler } from './skills.js';
18
+ import { modelsHandler } from './models.js';
19
+ import { settingsHandler } from './settings.js';
20
+ import { runSetup } from '../setup.js';
21
+ export function buildCommandRegistry(agent, config, activeProviderType, gatewayInstance) {
22
+ const registry = new CommandRegistry();
23
+ const skillRegistry = agent.getSkillRegistry();
24
+ // Tier 1 — Session Control
25
+ registry.register('help', createHelpHandler(registry, skillRegistry), {
26
+ description: 'Show available commands',
27
+ aliases: ['?'],
28
+ });
29
+ registry.register('clear', clearHandler, {
30
+ description: 'Clear conversation history',
31
+ aliases: ['reset', 'new'],
32
+ });
33
+ registry.register('exit', exitHandler, {
34
+ description: 'End the session',
35
+ aliases: ['quit'],
36
+ });
37
+ registry.register('compact', compactHandler, {
38
+ description: 'Compress conversation to a summary',
39
+ aliases: ['compress'],
40
+ interactive: true, // ora spinner
41
+ });
42
+ // Tier 2 — Configuration & Discovery
43
+ registry.register('skills', skillsHandler, {
44
+ description: 'List loaded skills',
45
+ });
46
+ registry.register('models', modelsHandler(agent, config, activeProviderType), {
47
+ description: 'Switch providers and models',
48
+ aliases: ['model'],
49
+ interactive: true, // inquirer wizard
50
+ });
51
+ registry.register('sessions', async () => ({
52
+ output: 'Session selector is available in the TUI: run /sessions or Ctrl+P → sessions.',
53
+ }), {
54
+ description: 'Resume, switch, or delete a session',
55
+ aliases: ['session'],
56
+ interactive: true, // TUI overlay (intercepted by handleUserInput before dispatch)
57
+ });
58
+ registry.register('steer', async () => ({
59
+ output: '/steer <message> interrupts the current run and sends a new message. Available in the TUI.',
60
+ }), {
61
+ description: 'Interrupt the run and send a new message',
62
+ interactive: true, // TUI-intercepted (works during a run — its purpose)
63
+ });
64
+ registry.register('settings', settingsHandler(), {
65
+ description: 'View and edit configuration',
66
+ aliases: ['config', 'setting'],
67
+ // Not flagged interactive: read subcommands (list/get/reset/export/help)
68
+ // return output and work in the TUI. The wizard (no args) + `set` use
69
+ // inquirer and are intercepted by the TUI's handleUserInput.
70
+ });
71
+ registry.register('setup', async () => {
72
+ await runSetup();
73
+ return {};
74
+ }, {
75
+ description: 'Run the setup wizard',
76
+ interactive: true,
77
+ });
78
+ // Gateway management — handler returns a string; adapt to CommandResult.
79
+ registry.register('gateway', async (ctx) => {
80
+ const { createGatewayCommandHandler } = await import('./gateway.js');
81
+ const handler = createGatewayCommandHandler(gatewayInstance);
82
+ return { output: await handler(ctx.args) };
83
+ }, {
84
+ description: 'Gateway management (targets, routes, credentials)',
85
+ aliases: ['gw'],
86
+ });
87
+ return registry;
88
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * /clear command handler for Zoe CLI.
3
+ *
4
+ * Aliases: /reset, /new
5
+ */
6
+ import type { CommandHandler } from './registry.js';
7
+ export declare const clearHandler: CommandHandler;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * /clear command handler for Zoe CLI.
3
+ *
4
+ * Aliases: /reset, /new
5
+ */
6
+ import chalk from 'chalk';
7
+ export const clearHandler = async (ctx) => {
8
+ ctx.agent.clearConversation();
9
+ return { output: chalk.cyan('Conversation cleared. Starting fresh.') };
10
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * /compact command handler for Zoe CLI.
3
+ *
4
+ * Summarizes the conversation to reduce token usage while preserving
5
+ * key context needed to continue the session.
6
+ *
7
+ * Aliases: /compress
8
+ *
9
+ * Uses an `ora` spinner (stdout), so it is marked `interactive` and the TUI
10
+ * defers it — run it in the readline REPL.
11
+ */
12
+ import type { CommandHandler } from './registry.js';
13
+ export declare const compactHandler: CommandHandler;