@dobby.ai/dobby 0.1.0

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 (174) hide show
  1. package/.env.example +9 -0
  2. package/AGENTS.md +267 -0
  3. package/README.md +382 -0
  4. package/ROADMAP.md +34 -0
  5. package/config/cron.example.json +9 -0
  6. package/config/gateway.example.json +128 -0
  7. package/config/models.custom.example.json +27 -0
  8. package/dist/src/agent/event-forwarder.js +341 -0
  9. package/dist/src/agent/tests/event-forwarder.test.js +113 -0
  10. package/dist/src/cli/commands/config.js +243 -0
  11. package/dist/src/cli/commands/configure.js +61 -0
  12. package/dist/src/cli/commands/cron.js +288 -0
  13. package/dist/src/cli/commands/doctor.js +189 -0
  14. package/dist/src/cli/commands/extension.js +151 -0
  15. package/dist/src/cli/commands/init.js +286 -0
  16. package/dist/src/cli/commands/start.js +177 -0
  17. package/dist/src/cli/commands/topology.js +254 -0
  18. package/dist/src/cli/index.js +8 -0
  19. package/dist/src/cli/program.js +386 -0
  20. package/dist/src/cli/shared/config-io.js +223 -0
  21. package/dist/src/cli/shared/config-mutators.js +345 -0
  22. package/dist/src/cli/shared/config-path.js +207 -0
  23. package/dist/src/cli/shared/config-schema.js +159 -0
  24. package/dist/src/cli/shared/config-types.js +1 -0
  25. package/dist/src/cli/shared/configure-sections.js +429 -0
  26. package/dist/src/cli/shared/discord-config.js +12 -0
  27. package/dist/src/cli/shared/init-catalog.js +115 -0
  28. package/dist/src/cli/shared/init-models-file.js +65 -0
  29. package/dist/src/cli/shared/presets.js +86 -0
  30. package/dist/src/cli/shared/runtime.js +29 -0
  31. package/dist/src/cli/shared/schema-prompts.js +325 -0
  32. package/dist/src/cli/tests/config-command.test.js +42 -0
  33. package/dist/src/cli/tests/config-io.test.js +64 -0
  34. package/dist/src/cli/tests/config-mutators.test.js +47 -0
  35. package/dist/src/cli/tests/config-path.test.js +21 -0
  36. package/dist/src/cli/tests/discord-config.test.js +23 -0
  37. package/dist/src/cli/tests/doctor.test.js +107 -0
  38. package/dist/src/cli/tests/init-catalog.test.js +87 -0
  39. package/dist/src/cli/tests/presets.test.js +41 -0
  40. package/dist/src/cli/tests/program-options.test.js +92 -0
  41. package/dist/src/cli/tests/routing-config.test.js +199 -0
  42. package/dist/src/cli/tests/routing-legacy.test.js +191 -0
  43. package/dist/src/core/control-command.js +12 -0
  44. package/dist/src/core/dedup-store.js +92 -0
  45. package/dist/src/core/gateway.js +432 -0
  46. package/dist/src/core/routing.js +306 -0
  47. package/dist/src/core/runtime-registry.js +119 -0
  48. package/dist/src/core/tests/control-command.test.js +17 -0
  49. package/dist/src/core/tests/gateway-update-strategy.test.js +167 -0
  50. package/dist/src/core/tests/runtime-registry.test.js +116 -0
  51. package/dist/src/core/tests/typing-controller.test.js +103 -0
  52. package/dist/src/core/types.js +1 -0
  53. package/dist/src/core/typing-controller.js +88 -0
  54. package/dist/src/cron/config.js +114 -0
  55. package/dist/src/cron/schedule.js +49 -0
  56. package/dist/src/cron/service.js +196 -0
  57. package/dist/src/cron/store.js +142 -0
  58. package/dist/src/cron/types.js +1 -0
  59. package/dist/src/extension/loader.js +97 -0
  60. package/dist/src/extension/manager.js +269 -0
  61. package/dist/src/extension/manifest.js +21 -0
  62. package/dist/src/extension/registry.js +137 -0
  63. package/dist/src/main.js +6 -0
  64. package/dist/src/sandbox/executor.js +1 -0
  65. package/dist/src/sandbox/host-executor.js +111 -0
  66. package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +175 -0
  67. package/docs/CRON_SCHEDULER_DESIGN.md +374 -0
  68. package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +77 -0
  69. package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +119 -0
  70. package/docs/MVP.md +135 -0
  71. package/docs/RUNBOOK.md +242 -0
  72. package/docs/TEAMWORK_HANDOFF_DESIGN.md +440 -0
  73. package/package.json +43 -0
  74. package/plugins/connector-discord/dobby.manifest.json +18 -0
  75. package/plugins/connector-discord/index.js +1 -0
  76. package/plugins/connector-discord/package-lock.json +360 -0
  77. package/plugins/connector-discord/package.json +38 -0
  78. package/plugins/connector-discord/src/connector.ts +350 -0
  79. package/plugins/connector-discord/src/contribution.ts +21 -0
  80. package/plugins/connector-discord/src/mapper.ts +102 -0
  81. package/plugins/connector-discord/tsconfig.json +19 -0
  82. package/plugins/connector-feishu/dobby.manifest.json +18 -0
  83. package/plugins/connector-feishu/index.js +1 -0
  84. package/plugins/connector-feishu/package-lock.json +618 -0
  85. package/plugins/connector-feishu/package.json +38 -0
  86. package/plugins/connector-feishu/src/connector.ts +343 -0
  87. package/plugins/connector-feishu/src/contribution.ts +26 -0
  88. package/plugins/connector-feishu/src/mapper.ts +401 -0
  89. package/plugins/connector-feishu/tsconfig.json +19 -0
  90. package/plugins/plugin-sdk/index.d.ts +261 -0
  91. package/plugins/plugin-sdk/index.js +1 -0
  92. package/plugins/plugin-sdk/package-lock.json +12 -0
  93. package/plugins/plugin-sdk/package.json +22 -0
  94. package/plugins/provider-claude/dobby.manifest.json +17 -0
  95. package/plugins/provider-claude/index.js +1 -0
  96. package/plugins/provider-claude/package-lock.json +3398 -0
  97. package/plugins/provider-claude/package.json +39 -0
  98. package/plugins/provider-claude/src/contribution.ts +1018 -0
  99. package/plugins/provider-claude/tsconfig.json +19 -0
  100. package/plugins/provider-claude-cli/dobby.manifest.json +17 -0
  101. package/plugins/provider-claude-cli/index.js +1 -0
  102. package/plugins/provider-claude-cli/package-lock.json +2898 -0
  103. package/plugins/provider-claude-cli/package.json +38 -0
  104. package/plugins/provider-claude-cli/src/contribution.ts +1673 -0
  105. package/plugins/provider-claude-cli/tsconfig.json +19 -0
  106. package/plugins/provider-pi/dobby.manifest.json +17 -0
  107. package/plugins/provider-pi/index.js +1 -0
  108. package/plugins/provider-pi/package-lock.json +3877 -0
  109. package/plugins/provider-pi/package.json +40 -0
  110. package/plugins/provider-pi/src/contribution.ts +476 -0
  111. package/plugins/provider-pi/tsconfig.json +19 -0
  112. package/plugins/sandbox-core/boxlite.js +1 -0
  113. package/plugins/sandbox-core/dobby.manifest.json +17 -0
  114. package/plugins/sandbox-core/docker.js +1 -0
  115. package/plugins/sandbox-core/package-lock.json +136 -0
  116. package/plugins/sandbox-core/package.json +39 -0
  117. package/plugins/sandbox-core/src/boxlite-context.ts +2 -0
  118. package/plugins/sandbox-core/src/boxlite-contribution.ts +53 -0
  119. package/plugins/sandbox-core/src/boxlite-executor.ts +911 -0
  120. package/plugins/sandbox-core/src/docker-contribution.ts +43 -0
  121. package/plugins/sandbox-core/src/docker-executor.ts +217 -0
  122. package/plugins/sandbox-core/tsconfig.json +19 -0
  123. package/scripts/local-extensions.mjs +168 -0
  124. package/src/agent/event-forwarder.ts +414 -0
  125. package/src/cli/commands/config.ts +328 -0
  126. package/src/cli/commands/configure.ts +92 -0
  127. package/src/cli/commands/cron.ts +410 -0
  128. package/src/cli/commands/doctor.ts +230 -0
  129. package/src/cli/commands/extension.ts +205 -0
  130. package/src/cli/commands/init.ts +396 -0
  131. package/src/cli/commands/start.ts +223 -0
  132. package/src/cli/commands/topology.ts +383 -0
  133. package/src/cli/index.ts +9 -0
  134. package/src/cli/program.ts +465 -0
  135. package/src/cli/shared/config-io.ts +277 -0
  136. package/src/cli/shared/config-mutators.ts +440 -0
  137. package/src/cli/shared/config-schema.ts +228 -0
  138. package/src/cli/shared/config-types.ts +121 -0
  139. package/src/cli/shared/configure-sections.ts +551 -0
  140. package/src/cli/shared/discord-config.ts +14 -0
  141. package/src/cli/shared/init-catalog.ts +189 -0
  142. package/src/cli/shared/init-models-file.ts +77 -0
  143. package/src/cli/shared/runtime.ts +33 -0
  144. package/src/cli/shared/schema-prompts.ts +414 -0
  145. package/src/cli/tests/config-command.test.ts +56 -0
  146. package/src/cli/tests/config-io.test.ts +92 -0
  147. package/src/cli/tests/config-mutators.test.ts +59 -0
  148. package/src/cli/tests/doctor.test.ts +120 -0
  149. package/src/cli/tests/init-catalog.test.ts +96 -0
  150. package/src/cli/tests/program-options.test.ts +113 -0
  151. package/src/cli/tests/routing-config.test.ts +209 -0
  152. package/src/core/control-command.ts +12 -0
  153. package/src/core/dedup-store.ts +103 -0
  154. package/src/core/gateway.ts +607 -0
  155. package/src/core/routing.ts +379 -0
  156. package/src/core/runtime-registry.ts +141 -0
  157. package/src/core/tests/control-command.test.ts +20 -0
  158. package/src/core/tests/runtime-registry.test.ts +140 -0
  159. package/src/core/tests/typing-controller.test.ts +129 -0
  160. package/src/core/types.ts +318 -0
  161. package/src/core/typing-controller.ts +119 -0
  162. package/src/cron/config.ts +154 -0
  163. package/src/cron/schedule.ts +61 -0
  164. package/src/cron/service.ts +249 -0
  165. package/src/cron/store.ts +155 -0
  166. package/src/cron/types.ts +60 -0
  167. package/src/extension/loader.ts +145 -0
  168. package/src/extension/manager.ts +355 -0
  169. package/src/extension/manifest.ts +26 -0
  170. package/src/extension/registry.ts +229 -0
  171. package/src/main.ts +8 -0
  172. package/src/sandbox/executor.ts +44 -0
  173. package/src/sandbox/host-executor.ts +118 -0
  174. package/tsconfig.json +18 -0
@@ -0,0 +1,386 @@
1
+ import { Command } from "commander";
2
+ import { runConfigEditCommand, runConfigListCommand, runConfigSchemaListCommand, runConfigSchemaShowCommand, runConfigShowCommand, } from "./commands/config.js";
3
+ import { runConfigureCommand } from "./commands/configure.js";
4
+ import { runCronAddCommand, runCronListCommand, runCronPauseCommand, runCronRemoveCommand, runCronResumeCommand, runCronRunCommand, runCronStatusCommand, runCronUpdateCommand, } from "./commands/cron.js";
5
+ import { runDoctorCommand } from "./commands/doctor.js";
6
+ import { runExtensionInstallCommand, runExtensionListCommand, runExtensionUninstallCommand, } from "./commands/extension.js";
7
+ import { runInitCommand } from "./commands/init.js";
8
+ import { runStartCommand } from "./commands/start.js";
9
+ import { runBindingListCommand, runBindingRemoveCommand, runBindingSetCommand, runBotListCommand, runBotSetCommand, runRouteListCommand, runRouteRemoveCommand, runRouteSetCommand, } from "./commands/topology.js";
10
+ /**
11
+ * Builds the top-level dobby CLI program and registers all subcommands.
12
+ */
13
+ export function buildProgram() {
14
+ const program = new Command();
15
+ program
16
+ .name("dobby")
17
+ .description("Discord-first local agent gateway")
18
+ .showHelpAfterError()
19
+ .action(async () => {
20
+ await runStartCommand();
21
+ });
22
+ program
23
+ .command("start")
24
+ .description("Start the gateway")
25
+ .action(async () => {
26
+ await runStartCommand();
27
+ });
28
+ program
29
+ .command("init")
30
+ .description("Initialize minimal runnable gateway config")
31
+ .action(async () => {
32
+ await runInitCommand();
33
+ });
34
+ program
35
+ .command("configure")
36
+ .description("Interactive configuration wizard")
37
+ .option("--section <section>", "Config section (repeatable): provider|connector|route|binding|sandbox|data", (value, previous) => [...previous, value], [])
38
+ .action(async (opts) => {
39
+ await runConfigureCommand({
40
+ sections: opts.section,
41
+ });
42
+ });
43
+ const botCommand = program.command("bot").description("Manage bot connector settings");
44
+ botCommand
45
+ .command("list")
46
+ .description("List configured bot connectors")
47
+ .option("--json", "Output JSON", false)
48
+ .action(async (opts) => {
49
+ await runBotListCommand({
50
+ json: Boolean(opts.json),
51
+ });
52
+ });
53
+ botCommand
54
+ .command("set")
55
+ .description("Update one bot connector")
56
+ .argument("<connectorId>", "Connector instance ID")
57
+ .option("--name <name>", "Discord botName")
58
+ .option("--token <token>", "Discord botToken")
59
+ .action(async (connectorId, opts) => {
60
+ await runBotSetCommand({
61
+ connectorId,
62
+ ...(typeof opts.name === "string" ? { name: opts.name } : {}),
63
+ ...(typeof opts.token === "string" ? { token: opts.token } : {}),
64
+ });
65
+ });
66
+ const bindingCommand = program.command("binding").description("Manage connector source-route bindings");
67
+ bindingCommand
68
+ .command("list")
69
+ .description("List bindings")
70
+ .option("--connector <id>", "Filter by connector instance ID")
71
+ .option("--json", "Output JSON", false)
72
+ .action(async (opts) => {
73
+ await runBindingListCommand({
74
+ ...(typeof opts.connector === "string" ? { connectorId: opts.connector } : {}),
75
+ json: Boolean(opts.json),
76
+ });
77
+ });
78
+ bindingCommand
79
+ .command("set")
80
+ .description("Create or update one binding")
81
+ .argument("<bindingId>", "Binding ID")
82
+ .requiredOption("--connector <id>", "Connector instance ID")
83
+ .requiredOption("--source-type <type>", "Source type: channel|chat")
84
+ .requiredOption("--source-id <id>", "Source ID")
85
+ .requiredOption("--route <id>", "Route ID")
86
+ .action(async (bindingId, opts) => {
87
+ if (opts.sourceType !== "channel" && opts.sourceType !== "chat") {
88
+ throw new Error("--source-type must be channel or chat");
89
+ }
90
+ await runBindingSetCommand({
91
+ bindingId,
92
+ connectorId: opts.connector,
93
+ sourceType: opts.sourceType,
94
+ sourceId: opts.sourceId,
95
+ routeId: opts.route,
96
+ });
97
+ });
98
+ bindingCommand
99
+ .command("remove")
100
+ .description("Remove one binding")
101
+ .argument("<bindingId>", "Binding ID")
102
+ .action(async (bindingId) => {
103
+ await runBindingRemoveCommand({
104
+ bindingId,
105
+ });
106
+ });
107
+ const routeCommand = program.command("route").description("Manage route profiles");
108
+ routeCommand
109
+ .command("list")
110
+ .description("List route profiles")
111
+ .option("--json", "Output JSON", false)
112
+ .action(async (opts) => {
113
+ await runRouteListCommand({
114
+ json: Boolean(opts.json),
115
+ });
116
+ });
117
+ routeCommand
118
+ .command("set")
119
+ .description("Create or update one route")
120
+ .argument("<routeId>", "Route ID")
121
+ .option("--project-root <path>", "Route project root")
122
+ .option("--tools <profile>", "Route tools profile: full|readonly")
123
+ .option("--provider <id>", "Provider instance ID")
124
+ .option("--sandbox <id>", "Sandbox instance ID")
125
+ .option("--mentions <policy>", "Mention policy: required|optional")
126
+ .action(async (routeId, opts) => {
127
+ if (typeof opts.mentions === "string"
128
+ && opts.mentions !== "required"
129
+ && opts.mentions !== "optional") {
130
+ throw new Error("--mentions must be required or optional");
131
+ }
132
+ await runRouteSetCommand({
133
+ routeId,
134
+ ...(typeof opts.projectRoot === "string" ? { projectRoot: opts.projectRoot } : {}),
135
+ ...(typeof opts.tools === "string" ? { tools: opts.tools } : {}),
136
+ ...(typeof opts.provider === "string" ? { providerId: opts.provider } : {}),
137
+ ...(typeof opts.sandbox === "string" ? { sandboxId: opts.sandbox } : {}),
138
+ ...(typeof opts.mentions === "string" ? { mentions: opts.mentions } : {}),
139
+ });
140
+ });
141
+ routeCommand
142
+ .command("remove")
143
+ .description("Remove one route")
144
+ .argument("<routeId>", "Route ID")
145
+ .option("--cascade-bindings", "Remove bindings that reference this route", false)
146
+ .action(async (routeId, opts) => {
147
+ await runRouteRemoveCommand({
148
+ routeId,
149
+ cascadeBindings: Boolean(opts.cascadeBindings),
150
+ });
151
+ });
152
+ const configCommand = program.command("config").description("Inspect and edit config");
153
+ configCommand
154
+ .command("show")
155
+ .description("Show full config or one section")
156
+ .argument("[section]", "Section: providers|connectors|routes|bindings|sandboxes|data|extensions")
157
+ .option("--json", "Output JSON", false)
158
+ .action(async (section, opts) => {
159
+ await runConfigShowCommand({
160
+ ...(typeof section === "string" ? { section } : {}),
161
+ json: Boolean(opts.json),
162
+ });
163
+ });
164
+ configCommand
165
+ .command("list")
166
+ .description("List config keys with type and preview")
167
+ .argument("[section]", "Section: providers|connectors|routes|bindings|sandboxes|data|extensions")
168
+ .option("--json", "Output JSON", false)
169
+ .action(async (section, opts) => {
170
+ await runConfigListCommand({
171
+ ...(typeof section === "string" ? { section } : {}),
172
+ json: Boolean(opts.json),
173
+ });
174
+ });
175
+ configCommand
176
+ .command("edit")
177
+ .description("Interactive edit for high-frequency sections")
178
+ .option("--section <section>", "Edit section (repeatable): provider|connector|route|binding", (value, previous) => [...previous, value], [])
179
+ .action(async (opts) => {
180
+ await runConfigEditCommand({
181
+ sections: opts.section,
182
+ });
183
+ });
184
+ const configSchemaCommand = configCommand.command("schema").description("Inspect extension config schemas");
185
+ configSchemaCommand
186
+ .command("list")
187
+ .description("List loaded contributions and schema availability")
188
+ .option("--json", "Output JSON", false)
189
+ .action(async (opts) => {
190
+ await runConfigSchemaListCommand({
191
+ json: Boolean(opts.json),
192
+ });
193
+ });
194
+ configSchemaCommand
195
+ .command("show")
196
+ .description("Show one contribution config schema")
197
+ .argument("<contributionId>", "Contribution ID")
198
+ .option("--json", "Output JSON", false)
199
+ .action(async (contributionId, opts) => {
200
+ await runConfigSchemaShowCommand({
201
+ contributionId,
202
+ json: Boolean(opts.json),
203
+ });
204
+ });
205
+ const extensionCommand = program.command("extension").description("Manage extensions");
206
+ extensionCommand
207
+ .command("install")
208
+ .description("Install extension package")
209
+ .argument("<packageSpec>", "npm package spec")
210
+ .option("--enable", "Enable extension in config after install", false)
211
+ .option("--json", "Output JSON", false)
212
+ .action(async (packageSpec, opts) => {
213
+ await runExtensionInstallCommand({
214
+ spec: packageSpec,
215
+ enable: Boolean(opts.enable),
216
+ json: Boolean(opts.json),
217
+ });
218
+ });
219
+ extensionCommand
220
+ .command("uninstall")
221
+ .description("Uninstall extension package")
222
+ .argument("<packageName>", "Package name")
223
+ .action(async (packageName) => {
224
+ await runExtensionUninstallCommand({
225
+ packageName,
226
+ });
227
+ });
228
+ extensionCommand
229
+ .command("list")
230
+ .description("List installed extension packages")
231
+ .option("--json", "Output JSON", false)
232
+ .action(async (opts) => {
233
+ await runExtensionListCommand({
234
+ json: Boolean(opts.json),
235
+ });
236
+ });
237
+ program
238
+ .command("doctor")
239
+ .description("Validate configuration and common runtime risks")
240
+ .option("--fix", "Apply conservative fixes", false)
241
+ .action(async (opts) => {
242
+ await runDoctorCommand({
243
+ fix: Boolean(opts.fix),
244
+ });
245
+ });
246
+ const cronCommand = program.command("cron").description("Manage scheduled cron jobs");
247
+ cronCommand
248
+ .command("add")
249
+ .description("Create one cron job")
250
+ .argument("<name>", "Job name")
251
+ .requiredOption("--prompt <text>", "Prompt text for each run")
252
+ .requiredOption("--connector <id>", "Connector instance ID")
253
+ .requiredOption("--route <id>", "Route ID")
254
+ .requiredOption("--channel <id>", "Delivery channel/chat ID")
255
+ .option("--thread <id>", "Delivery thread ID")
256
+ .option("--session-policy <policy>", "Session policy: stateless|shared-session", "stateless")
257
+ .option("--at <iso>", "Run once at ISO timestamp")
258
+ .option("--every-ms <ms>", "Run at fixed interval in milliseconds")
259
+ .option("--cron <expr>", "Cron expression")
260
+ .option("--tz <tz>", "Timezone for cron expression")
261
+ .option("--cron-config <path>", "Override cron config path")
262
+ .action(async (name, opts) => {
263
+ const parsedEveryMs = typeof opts.everyMs === "string" ? Number(opts.everyMs) : null;
264
+ await runCronAddCommand({
265
+ name,
266
+ prompt: opts.prompt,
267
+ connectorId: opts.connector,
268
+ routeId: opts.route,
269
+ channelId: opts.channel,
270
+ ...(typeof opts.thread === "string" ? { threadId: opts.thread } : {}),
271
+ sessionPolicy: opts.sessionPolicy,
272
+ ...(typeof opts.at === "string" ? { at: opts.at } : {}),
273
+ ...(parsedEveryMs !== null && Number.isFinite(parsedEveryMs) ? { everyMs: parsedEveryMs } : {}),
274
+ ...(typeof opts.cron === "string" ? { cronExpr: opts.cron } : {}),
275
+ ...(typeof opts.tz === "string" ? { tz: opts.tz } : {}),
276
+ ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig } : {}),
277
+ });
278
+ });
279
+ cronCommand
280
+ .command("list")
281
+ .description("List cron jobs")
282
+ .option("--json", "Output JSON", false)
283
+ .option("--cron-config <path>", "Override cron config path")
284
+ .action(async (opts) => {
285
+ await runCronListCommand({
286
+ json: Boolean(opts.json),
287
+ ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig } : {}),
288
+ });
289
+ });
290
+ cronCommand
291
+ .command("status")
292
+ .description("Show status for all jobs or one job")
293
+ .argument("[jobId]", "Cron job ID")
294
+ .option("--json", "Output JSON", false)
295
+ .option("--cron-config <path>", "Override cron config path")
296
+ .action(async (jobId, opts) => {
297
+ await runCronStatusCommand({
298
+ ...(typeof jobId === "string" ? { jobId } : {}),
299
+ json: Boolean(opts.json),
300
+ ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig } : {}),
301
+ });
302
+ });
303
+ cronCommand
304
+ .command("run")
305
+ .description("Queue one cron job for immediate execution")
306
+ .argument("<jobId>", "Cron job ID")
307
+ .option("--cron-config <path>", "Override cron config path")
308
+ .action(async (jobId, opts) => {
309
+ await runCronRunCommand({
310
+ jobId,
311
+ ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig } : {}),
312
+ });
313
+ });
314
+ cronCommand
315
+ .command("update")
316
+ .description("Update one cron job")
317
+ .argument("<jobId>", "Cron job ID")
318
+ .option("--name <name>", "Job name")
319
+ .option("--prompt <text>", "Job prompt")
320
+ .option("--connector <id>", "Connector instance ID")
321
+ .option("--route <id>", "Route ID")
322
+ .option("--channel <id>", "Delivery channel/chat ID")
323
+ .option("--thread <id>", "Delivery thread ID")
324
+ .option("--clear-thread", "Unset delivery thread", false)
325
+ .option("--session-policy <policy>", "Session policy: stateless|shared-session")
326
+ .option("--at <iso>", "Run once at ISO timestamp")
327
+ .option("--every-ms <ms>", "Run at fixed interval in milliseconds")
328
+ .option("--cron <expr>", "Cron expression")
329
+ .option("--tz <tz>", "Timezone for cron expression")
330
+ .option("--cron-config <path>", "Override cron config path")
331
+ .action(async (jobId, opts) => {
332
+ const parsedEveryMs = typeof opts.everyMs === "string" ? Number(opts.everyMs) : null;
333
+ await runCronUpdateCommand({
334
+ jobId,
335
+ ...(typeof opts.name === "string" ? { name: opts.name } : {}),
336
+ ...(typeof opts.prompt === "string" ? { prompt: opts.prompt } : {}),
337
+ ...(typeof opts.connector === "string" ? { connectorId: opts.connector } : {}),
338
+ ...(typeof opts.route === "string" ? { routeId: opts.route } : {}),
339
+ ...(typeof opts.channel === "string" ? { channelId: opts.channel } : {}),
340
+ ...(typeof opts.thread === "string" ? { threadId: opts.thread } : {}),
341
+ clearThread: Boolean(opts.clearThread),
342
+ ...(typeof opts.sessionPolicy === "string"
343
+ ? { sessionPolicy: opts.sessionPolicy }
344
+ : {}),
345
+ ...(typeof opts.at === "string" ? { at: opts.at } : {}),
346
+ ...(parsedEveryMs !== null && Number.isFinite(parsedEveryMs) ? { everyMs: parsedEveryMs } : {}),
347
+ ...(typeof opts.cron === "string" ? { cronExpr: opts.cron } : {}),
348
+ ...(typeof opts.tz === "string" ? { tz: opts.tz } : {}),
349
+ ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig } : {}),
350
+ });
351
+ });
352
+ cronCommand
353
+ .command("remove")
354
+ .description("Remove one cron job")
355
+ .argument("<jobId>", "Cron job ID")
356
+ .option("--cron-config <path>", "Override cron config path")
357
+ .action(async (jobId, opts) => {
358
+ await runCronRemoveCommand({
359
+ jobId,
360
+ ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig } : {}),
361
+ });
362
+ });
363
+ cronCommand
364
+ .command("pause")
365
+ .description("Pause one cron job")
366
+ .argument("<jobId>", "Cron job ID")
367
+ .option("--cron-config <path>", "Override cron config path")
368
+ .action(async (jobId, opts) => {
369
+ await runCronPauseCommand({
370
+ jobId,
371
+ ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig } : {}),
372
+ });
373
+ });
374
+ cronCommand
375
+ .command("resume")
376
+ .description("Resume one cron job")
377
+ .argument("<jobId>", "Cron job ID")
378
+ .option("--cron-config <path>", "Override cron config path")
379
+ .action(async (jobId, opts) => {
380
+ await runCronResumeCommand({
381
+ jobId,
382
+ ...(typeof opts.cronConfig === "string" ? { cronConfigPath: opts.cronConfig } : {}),
383
+ });
384
+ });
385
+ return program;
386
+ }
@@ -0,0 +1,223 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { access, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { loadGatewayConfig } from "../../core/routing.js";
6
+ /**
7
+ * Default config file path used by all CLI commands.
8
+ */
9
+ export const DEFAULT_CONFIG_PATH = resolve(homedir(), ".dobby", "gateway.json");
10
+ /**
11
+ * Expands "~" prefixed paths to the current user's home directory.
12
+ */
13
+ function expandHome(value) {
14
+ if (value === "~") {
15
+ return homedir();
16
+ }
17
+ if (value.startsWith("~/") || value.startsWith("~\\")) {
18
+ return resolve(homedir(), value.slice(2));
19
+ }
20
+ return value;
21
+ }
22
+ /**
23
+ * Returns true when a directory looks like the dobby repository root.
24
+ */
25
+ function isDobbyRepoRoot(candidateDir) {
26
+ const packageJsonPath = resolve(candidateDir, "package.json");
27
+ const repoConfigPath = resolve(candidateDir, "config", "gateway.json");
28
+ const localExtensionsScriptPath = resolve(candidateDir, "scripts", "local-extensions.mjs");
29
+ if (!existsSync(packageJsonPath) || !existsSync(repoConfigPath) || !existsSync(localExtensionsScriptPath)) {
30
+ return false;
31
+ }
32
+ try {
33
+ const packageJsonRaw = readFileSync(packageJsonPath, "utf-8");
34
+ const parsed = JSON.parse(packageJsonRaw);
35
+ return parsed.name === "dobby";
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ function resolveConfigBaseDir(configPath) {
42
+ const absoluteConfigPath = resolve(configPath);
43
+ const configDir = dirname(absoluteConfigPath);
44
+ const repoRoot = dirname(configDir);
45
+ if (absoluteConfigPath === resolve(repoRoot, "config", "gateway.json") && isDobbyRepoRoot(repoRoot)) {
46
+ return repoRoot;
47
+ }
48
+ return configDir;
49
+ }
50
+ /**
51
+ * Scans current directory and ancestors to find a local dobby repo config path.
52
+ */
53
+ function findDobbyRepoConfigPath(startDir) {
54
+ let currentDir = resolve(startDir);
55
+ while (true) {
56
+ if (isDobbyRepoRoot(currentDir)) {
57
+ return resolve(currentDir, "config", "gateway.json");
58
+ }
59
+ const parentDir = dirname(currentDir);
60
+ if (parentDir === currentDir) {
61
+ return null;
62
+ }
63
+ currentDir = parentDir;
64
+ }
65
+ }
66
+ /**
67
+ * Resolves config path source by priority: env override -> local repo -> default home path.
68
+ */
69
+ function resolveConfigPathInfo(input) {
70
+ const env = input?.env ?? process.env;
71
+ const cwd = input?.cwd ?? process.cwd();
72
+ const rawOverride = env.DOBBY_CONFIG_PATH;
73
+ const override = typeof rawOverride === "string" ? rawOverride.trim() : "";
74
+ if (override.length > 0) {
75
+ const expanded = expandHome(override);
76
+ return {
77
+ path: isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded),
78
+ source: "env",
79
+ };
80
+ }
81
+ const localRepoConfigPath = findDobbyRepoConfigPath(cwd);
82
+ if (localRepoConfigPath) {
83
+ return {
84
+ path: localRepoConfigPath,
85
+ source: "repo",
86
+ };
87
+ }
88
+ return {
89
+ path: DEFAULT_CONFIG_PATH,
90
+ source: "default",
91
+ };
92
+ }
93
+ /**
94
+ * Resolves config path with dev-friendly detection and env override support.
95
+ */
96
+ export function resolveConfigPath(input) {
97
+ return resolveConfigPathInfo(input).path;
98
+ }
99
+ /**
100
+ * Formats a user-facing init command hint for missing-config errors.
101
+ */
102
+ function initCommandHint() {
103
+ return "dobby init";
104
+ }
105
+ /**
106
+ * Resolves data.rootDir into an absolute path using config-file-relative semantics.
107
+ */
108
+ export function resolveDataRootDir(configPath, rawConfig) {
109
+ const absoluteConfigPath = resolve(configPath);
110
+ const baseDir = resolveConfigBaseDir(absoluteConfigPath);
111
+ const rawRootDir = typeof rawConfig.data?.rootDir === "string" && rawConfig.data.rootDir.trim().length > 0
112
+ ? rawConfig.data.rootDir
113
+ : "./data";
114
+ const expanded = expandHome(rawRootDir);
115
+ if (isAbsolute(expanded)) {
116
+ return resolve(expanded);
117
+ }
118
+ return resolve(baseDir, expanded);
119
+ }
120
+ /**
121
+ * Checks whether a file exists without throwing for missing paths.
122
+ */
123
+ async function fileExists(path) {
124
+ try {
125
+ await access(path);
126
+ return true;
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }
132
+ /**
133
+ * Reads and parses the raw JSON config file.
134
+ */
135
+ export async function readRawConfig(configPath) {
136
+ const absolutePath = resolve(configPath);
137
+ if (!(await fileExists(absolutePath))) {
138
+ return null;
139
+ }
140
+ const raw = await readFile(absolutePath, "utf-8");
141
+ const parsed = JSON.parse(raw);
142
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
143
+ throw new Error(`Config at '${absolutePath}' must be a JSON object`);
144
+ }
145
+ return parsed;
146
+ }
147
+ /**
148
+ * Writes config content atomically via temp file + rename.
149
+ */
150
+ async function writeAtomic(configPath, content) {
151
+ const absolutePath = resolve(configPath);
152
+ const configDir = dirname(absolutePath);
153
+ await mkdir(configDir, { recursive: true });
154
+ const tempPath = `${absolutePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
155
+ await writeFile(tempPath, content, "utf-8");
156
+ await rename(tempPath, absolutePath);
157
+ }
158
+ /**
159
+ * Serializes and writes the config file with normalized JSON formatting.
160
+ */
161
+ export async function writeRawConfig(configPath, rawConfig) {
162
+ const payload = `${JSON.stringify(rawConfig, null, 2)}\n`;
163
+ await writeAtomic(configPath, payload);
164
+ }
165
+ /**
166
+ * Creates a timestamped backup beside the target config file.
167
+ */
168
+ export async function backupConfig(configPath) {
169
+ const absolutePath = resolve(configPath);
170
+ if (!(await fileExists(absolutePath))) {
171
+ return null;
172
+ }
173
+ const content = await readFile(absolutePath, "utf-8");
174
+ const backupPath = `${absolutePath}.bak-${Date.now()}`;
175
+ await writeFile(backupPath, content, "utf-8");
176
+ return backupPath;
177
+ }
178
+ /**
179
+ * Writes config, validates with loadGatewayConfig, and rolls back on any failure.
180
+ */
181
+ export async function writeConfigWithValidation(configPath, rawConfig, options) {
182
+ const absolutePath = resolve(configPath);
183
+ const validate = options?.validate !== false;
184
+ const previousExists = await fileExists(absolutePath);
185
+ const previousContent = previousExists ? await readFile(absolutePath, "utf-8") : null;
186
+ const backupPath = options?.createBackup ? await backupConfig(absolutePath) : null;
187
+ try {
188
+ await writeRawConfig(absolutePath, rawConfig);
189
+ if (!validate) {
190
+ return { backupPath };
191
+ }
192
+ await loadGatewayConfig(absolutePath);
193
+ return { backupPath };
194
+ }
195
+ catch (error) {
196
+ if (previousContent === null) {
197
+ await rm(absolutePath, { force: true });
198
+ }
199
+ else {
200
+ await writeAtomic(absolutePath, previousContent);
201
+ }
202
+ throw error;
203
+ }
204
+ }
205
+ /**
206
+ * Reads config and throws a user-oriented error when the file is missing.
207
+ */
208
+ export async function requireRawConfig(configPath) {
209
+ const raw = await readRawConfig(configPath);
210
+ if (!raw) {
211
+ const resolvedConfigPath = resolve(configPath);
212
+ const currentResolution = resolveConfigPathInfo();
213
+ let sourceHint = "";
214
+ if (resolvedConfigPath === currentResolution.path && currentResolution.source === "env") {
215
+ sourceHint = ` Source: DOBBY_CONFIG_PATH='${process.env.DOBBY_CONFIG_PATH ?? ""}'.`;
216
+ }
217
+ else if (resolvedConfigPath === currentResolution.path && currentResolution.source === "repo") {
218
+ sourceHint = " Source: detected dobby repo, using ./config/gateway.json.";
219
+ }
220
+ throw new Error(`Config '${resolvedConfigPath}' does not exist.${sourceHint} Run '${initCommandHint()}' first.`);
221
+ }
222
+ return raw;
223
+ }