@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,243 @@
1
+ import { cancel, intro, isCancel, multiselect, note, outro, } from "@clack/prompts";
2
+ import { ensureGatewayConfigShape } from "../shared/config-mutators.js";
3
+ import { requireRawConfig, resolveConfigPath, writeConfigWithValidation } from "../shared/config-io.js";
4
+ import { applyConfigureSection, isConfigureSection, normalizeConfigureSectionOrder, } from "../shared/configure-sections.js";
5
+ import { applyAndValidateContributionSchemas, getContributionSchema, loadContributionSchemaCatalog, listContributionSchemas, } from "../shared/config-schema.js";
6
+ export const CONFIG_SECTION_VALUES = ["providers", "connectors", "routes", "bindings", "sandboxes", "data", "extensions"];
7
+ const EDITABLE_CONFIG_SECTIONS = ["provider", "connector", "route", "binding"];
8
+ /**
9
+ * Validates section identifiers accepted by `config show|list`.
10
+ */
11
+ export function isConfigSection(value) {
12
+ return CONFIG_SECTION_VALUES.includes(value);
13
+ }
14
+ /**
15
+ * Returns a stable scalar/object/array type label for list output.
16
+ */
17
+ function describeValueType(value) {
18
+ if (value === null) {
19
+ return "null";
20
+ }
21
+ if (Array.isArray(value)) {
22
+ return "array";
23
+ }
24
+ return typeof value;
25
+ }
26
+ /**
27
+ * Guards plain object-like values for preview/list summary generation.
28
+ */
29
+ function isRecord(value) {
30
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
31
+ }
32
+ /**
33
+ * Renders a compact preview string for list rows.
34
+ */
35
+ export function previewConfigValue(value, maxLength = 80) {
36
+ let raw;
37
+ if (value === null) {
38
+ raw = "null";
39
+ }
40
+ else if (typeof value === "string") {
41
+ raw = JSON.stringify(value);
42
+ }
43
+ else if (typeof value === "number" || typeof value === "boolean") {
44
+ raw = String(value);
45
+ }
46
+ else if (Array.isArray(value)) {
47
+ const head = value.slice(0, 3).map((item) => previewConfigValue(item, 24)).join(", ");
48
+ raw = `[${head}${value.length > 3 ? ", ..." : ""}]`;
49
+ }
50
+ else if (isRecord(value)) {
51
+ const keys = Object.keys(value);
52
+ const head = keys.slice(0, 3).join(", ");
53
+ raw = `{${head}${keys.length > 3 ? ", ..." : ""}}`;
54
+ }
55
+ else {
56
+ raw = String(value);
57
+ }
58
+ return raw.length > maxLength ? `${raw.slice(0, maxLength - 3)}...` : raw;
59
+ }
60
+ /**
61
+ * Summarizes one object value into list-friendly rows.
62
+ */
63
+ export function buildConfigListEntries(value) {
64
+ if (!isRecord(value)) {
65
+ return [
66
+ {
67
+ key: "(value)",
68
+ type: describeValueType(value),
69
+ preview: previewConfigValue(value),
70
+ },
71
+ ];
72
+ }
73
+ return Object.entries(value)
74
+ .sort((a, b) => a[0].localeCompare(b[0]))
75
+ .map(([key, item]) => ({
76
+ key,
77
+ type: describeValueType(item),
78
+ ...(isRecord(item) ? { children: Object.keys(item).length } : {}),
79
+ ...(Array.isArray(item) ? { children: item.length } : {}),
80
+ preview: previewConfigValue(item),
81
+ }));
82
+ }
83
+ /**
84
+ * Picks and validates a top-level config section.
85
+ */
86
+ function resolveConfigSection(section) {
87
+ if (!section) {
88
+ return undefined;
89
+ }
90
+ if (!isConfigSection(section)) {
91
+ throw new Error(`Unknown section '${section}'. Allowed: ${CONFIG_SECTION_VALUES.join(", ")}`);
92
+ }
93
+ return section;
94
+ }
95
+ /**
96
+ * Pretty prints list rows for human-readable CLI output.
97
+ */
98
+ function printListEntries(entries) {
99
+ if (entries.length === 0) {
100
+ console.log("(empty)");
101
+ return;
102
+ }
103
+ for (const entry of entries) {
104
+ const children = entry.children !== undefined ? `, children=${entry.children}` : "";
105
+ console.log(`${entry.key}: type=${entry.type}${children}, preview=${entry.preview}`);
106
+ }
107
+ }
108
+ /**
109
+ * Loads current config and normalizes missing sections to make read output stable.
110
+ */
111
+ async function loadNormalizedConfig() {
112
+ const configPath = resolveConfigPath();
113
+ const rawConfig = await requireRawConfig(configPath);
114
+ return ensureGatewayConfigShape(structuredClone(rawConfig));
115
+ }
116
+ /**
117
+ * Prints full config or one top-level section.
118
+ */
119
+ export async function runConfigShowCommand(options) {
120
+ const normalized = await loadNormalizedConfig();
121
+ const section = resolveConfigSection(options.section);
122
+ const value = section ? normalized[section] : normalized;
123
+ if (options.json) {
124
+ console.log(JSON.stringify(value ?? null));
125
+ return;
126
+ }
127
+ console.log(JSON.stringify(value ?? null, null, 2));
128
+ }
129
+ /**
130
+ * Prints a typed summary for top-level config sections or one section's children.
131
+ */
132
+ export async function runConfigListCommand(options) {
133
+ const normalized = await loadNormalizedConfig();
134
+ const section = resolveConfigSection(options.section);
135
+ const entries = section
136
+ ? buildConfigListEntries(normalized[section])
137
+ : CONFIG_SECTION_VALUES.map((key) => ({
138
+ key,
139
+ type: describeValueType(normalized[key]),
140
+ ...(isRecord(normalized[key]) ? { children: Object.keys(normalized[key]).length } : {}),
141
+ ...(Array.isArray(normalized[key]) ? { children: normalized[key].length } : {}),
142
+ preview: previewConfigValue(normalized[key]),
143
+ }));
144
+ if (options.json) {
145
+ console.log(JSON.stringify(entries));
146
+ return;
147
+ }
148
+ printListEntries(entries);
149
+ }
150
+ /**
151
+ * Lists loaded contribution schema availability for installed/allow-listed extensions.
152
+ */
153
+ export async function runConfigSchemaListCommand(options) {
154
+ const configPath = resolveConfigPath();
155
+ const rawConfig = await requireRawConfig(configPath);
156
+ const entries = await listContributionSchemas(configPath, rawConfig);
157
+ if (options.json) {
158
+ console.log(JSON.stringify(entries));
159
+ return;
160
+ }
161
+ if (entries.length === 0) {
162
+ console.log("(empty)");
163
+ return;
164
+ }
165
+ for (const entry of entries) {
166
+ console.log(`${entry.contributionId}: kind=${entry.kind}, package=${entry.packageName}, hasSchema=${entry.hasSchema ? "yes" : "no"}`);
167
+ }
168
+ }
169
+ /**
170
+ * Prints one contribution JSON Schema by contributionId.
171
+ */
172
+ export async function runConfigSchemaShowCommand(options) {
173
+ const configPath = resolveConfigPath();
174
+ const rawConfig = await requireRawConfig(configPath);
175
+ const entry = await getContributionSchema(configPath, rawConfig, options.contributionId);
176
+ if (!entry) {
177
+ throw new Error(`Unknown contributionId '${options.contributionId}'. Run 'dobby config schema list' first.`);
178
+ }
179
+ if (!entry.configSchema) {
180
+ throw new Error(`Contribution '${options.contributionId}' does not expose configSchema.`);
181
+ }
182
+ if (options.json) {
183
+ console.log(JSON.stringify(entry.configSchema));
184
+ return;
185
+ }
186
+ console.log(JSON.stringify(entry.configSchema, null, 2));
187
+ }
188
+ /**
189
+ * Resolves interactive `config edit` target sections from flags or prompt.
190
+ */
191
+ async function resolveEditSections(sections) {
192
+ if (sections.length > 0) {
193
+ const normalized = [];
194
+ for (const section of sections) {
195
+ if (!isConfigureSection(section) || !EDITABLE_CONFIG_SECTIONS.includes(section)) {
196
+ throw new Error(`Unknown --section '${section}'. Allowed: ${EDITABLE_CONFIG_SECTIONS.join(", ")}`);
197
+ }
198
+ normalized.push(section);
199
+ }
200
+ return normalized;
201
+ }
202
+ const picked = await multiselect({
203
+ message: "Select sections to edit",
204
+ options: EDITABLE_CONFIG_SECTIONS.map((section) => ({ value: section, label: section })),
205
+ initialValues: ["provider", "connector", "route", "binding"],
206
+ required: true,
207
+ });
208
+ if (isCancel(picked)) {
209
+ cancel("Config edit cancelled.");
210
+ throw new Error("Config edit cancelled.");
211
+ }
212
+ return picked;
213
+ }
214
+ /**
215
+ * Runs interactive high-frequency config editing with one validated atomic write.
216
+ */
217
+ export async function runConfigEditCommand(options) {
218
+ const configPath = resolveConfigPath();
219
+ const rawConfig = await requireRawConfig(configPath);
220
+ const next = ensureGatewayConfigShape(structuredClone(rawConfig));
221
+ intro("dobby config edit");
222
+ const requestedSections = await resolveEditSections(options.sections);
223
+ const sections = normalizeConfigureSectionOrder(requestedSections);
224
+ if (sections.join(",") !== requestedSections.join(",")) {
225
+ await note(`Execution order: ${sections.join(" -> ")}`, "Info");
226
+ }
227
+ const catalog = await loadContributionSchemaCatalog(configPath, next);
228
+ const schemaByContributionId = new Map(catalog
229
+ .filter((item) => item.configSchema)
230
+ .map((item) => [item.contributionId, item.configSchema]));
231
+ const schemaStateByContributionId = new Map(catalog.map((item) => [item.contributionId, item.configSchema ? "with_schema" : "without_schema"]));
232
+ for (const section of sections) {
233
+ await applyConfigureSection(section, next, { schemaByContributionId, schemaStateByContributionId });
234
+ await note(`Section '${section}' prepared`, "Updated");
235
+ }
236
+ const validatedConfig = await applyAndValidateContributionSchemas(configPath, next);
237
+ await writeConfigWithValidation(configPath, validatedConfig, {
238
+ validate: true,
239
+ createBackup: true,
240
+ });
241
+ await note(`Saved to ${configPath}`, "Saved");
242
+ outro("Configuration updated.");
243
+ }
@@ -0,0 +1,61 @@
1
+ import { cancel, intro, isCancel, multiselect, note, outro, } from "@clack/prompts";
2
+ import { ensureGatewayConfigShape } from "../shared/config-mutators.js";
3
+ import { requireRawConfig, resolveConfigPath, writeConfigWithValidation } from "../shared/config-io.js";
4
+ import { applyConfigureSection, CONFIGURE_SECTION_VALUES, isConfigureSection, normalizeConfigureSectionOrder, } from "../shared/configure-sections.js";
5
+ import { applyAndValidateContributionSchemas, loadContributionSchemaCatalog } from "../shared/config-schema.js";
6
+ /**
7
+ * Resolves target sections from CLI flags or interactive section picker.
8
+ */
9
+ async function resolveSections(sections) {
10
+ if (sections.length > 0) {
11
+ const normalized = [];
12
+ for (const section of sections) {
13
+ if (!isConfigureSection(section)) {
14
+ throw new Error(`Unknown --section '${section}'. Allowed: ${CONFIGURE_SECTION_VALUES.join(", ")}`);
15
+ }
16
+ normalized.push(section);
17
+ }
18
+ return normalized;
19
+ }
20
+ const picked = await multiselect({
21
+ message: "Select sections to configure",
22
+ options: CONFIGURE_SECTION_VALUES.map((section) => ({ value: section, label: section })),
23
+ initialValues: ["provider", "connector", "route", "binding"],
24
+ required: true,
25
+ });
26
+ if (isCancel(picked)) {
27
+ cancel("Configure cancelled.");
28
+ throw new Error("Configure cancelled.");
29
+ }
30
+ return picked;
31
+ }
32
+ /**
33
+ * Executes interactive config updates and validates one final atomic save.
34
+ */
35
+ export async function runConfigureCommand(options) {
36
+ const configPath = resolveConfigPath();
37
+ const rawConfig = await requireRawConfig(configPath);
38
+ const next = ensureGatewayConfigShape(structuredClone(rawConfig));
39
+ intro("dobby configure");
40
+ const requestedSections = await resolveSections(options.sections);
41
+ const sections = normalizeConfigureSectionOrder(requestedSections);
42
+ if (sections.join(",") !== requestedSections.join(",")) {
43
+ await note(`Execution order: ${sections.join(" -> ")}`, "Info");
44
+ }
45
+ const catalog = await loadContributionSchemaCatalog(configPath, next);
46
+ const schemaByContributionId = new Map(catalog
47
+ .filter((item) => item.configSchema)
48
+ .map((item) => [item.contributionId, item.configSchema]));
49
+ const schemaStateByContributionId = new Map(catalog.map((item) => [item.contributionId, item.configSchema ? "with_schema" : "without_schema"]));
50
+ for (const section of sections) {
51
+ await applyConfigureSection(section, next, { schemaByContributionId, schemaStateByContributionId });
52
+ await note(`Section '${section}' prepared`, "Updated");
53
+ }
54
+ const validatedConfig = await applyAndValidateContributionSchemas(configPath, next);
55
+ await writeConfigWithValidation(configPath, validatedConfig, {
56
+ validate: true,
57
+ createBackup: true,
58
+ });
59
+ await note(`Saved to ${configPath}`, "Saved");
60
+ outro("Configuration updated.");
61
+ }
@@ -0,0 +1,288 @@
1
+ import { loadGatewayConfig } from "../../core/routing.js";
2
+ import { loadCronConfig } from "../../cron/config.js";
3
+ import { computeInitialNextRunAtMs, describeSchedule } from "../../cron/schedule.js";
4
+ import { CronStore } from "../../cron/store.js";
5
+ import { resolveConfigPath } from "../shared/config-io.js";
6
+ import { createLogger } from "../shared/runtime.js";
7
+ function slugify(value) {
8
+ const normalized = value
9
+ .trim()
10
+ .toLowerCase()
11
+ .replaceAll(/[^a-z0-9]+/g, "-")
12
+ .replaceAll(/^-+|-+$/g, "");
13
+ return normalized.length > 0 ? normalized : "job";
14
+ }
15
+ function parseSchedule(input) {
16
+ const variants = [input.at ? "at" : null, input.everyMs !== undefined ? "every" : null, input.cronExpr ? "cron" : null]
17
+ .filter((item) => item !== null);
18
+ if (variants.length !== 1) {
19
+ throw new Error("Exactly one schedule option is required: --at | --every-ms | --cron");
20
+ }
21
+ if (input.at) {
22
+ return { kind: "at", at: input.at };
23
+ }
24
+ if (input.everyMs !== undefined) {
25
+ if (!Number.isFinite(input.everyMs) || input.everyMs <= 0) {
26
+ throw new Error("--every-ms must be a positive integer");
27
+ }
28
+ return { kind: "every", everyMs: Math.floor(input.everyMs) };
29
+ }
30
+ if (!input.cronExpr) {
31
+ throw new Error("Missing --cron expression");
32
+ }
33
+ return {
34
+ kind: "cron",
35
+ expr: input.cronExpr,
36
+ ...(input.tz ? { tz: input.tz } : {}),
37
+ };
38
+ }
39
+ function parseSessionPolicy(value) {
40
+ if (value === undefined) {
41
+ return undefined;
42
+ }
43
+ if (value === "stateless" || value === "shared-session") {
44
+ return value;
45
+ }
46
+ throw new Error(`Invalid session policy '${value}'. Expected 'stateless' or 'shared-session'.`);
47
+ }
48
+ function assertDeliveryReferences(config, input) {
49
+ if (!config.connectors.items[input.connectorId]) {
50
+ throw new Error(`Unknown connectorId '${input.connectorId}'`);
51
+ }
52
+ if (!config.routes.items[input.routeId]) {
53
+ throw new Error(`Unknown routeId '${input.routeId}'`);
54
+ }
55
+ }
56
+ async function loadCronContext(options) {
57
+ const configPath = resolveConfigPath();
58
+ const gatewayConfig = await loadGatewayConfig(configPath);
59
+ const loadedCronConfig = await loadCronConfig({
60
+ gatewayConfigPath: configPath,
61
+ gatewayConfig,
62
+ ...(options?.cronConfigPath ? { explicitCronConfigPath: options.cronConfigPath } : {}),
63
+ });
64
+ const logger = createLogger();
65
+ const store = new CronStore(loadedCronConfig.config.storeFile, loadedCronConfig.config.runLogFile, logger);
66
+ await store.load();
67
+ return {
68
+ configPath,
69
+ gatewayConfig,
70
+ cronConfigPath: loadedCronConfig.configPath,
71
+ store,
72
+ };
73
+ }
74
+ export async function runCronAddCommand(options) {
75
+ const context = await loadCronContext(options.cronConfigPath ? { cronConfigPath: options.cronConfigPath } : undefined);
76
+ assertDeliveryReferences(context.gatewayConfig, {
77
+ connectorId: options.connectorId,
78
+ routeId: options.routeId,
79
+ });
80
+ const schedule = parseSchedule({
81
+ ...(options.at ? { at: options.at } : {}),
82
+ ...(options.everyMs !== undefined ? { everyMs: options.everyMs } : {}),
83
+ ...(options.cronExpr ? { cronExpr: options.cronExpr } : {}),
84
+ ...(options.tz ? { tz: options.tz } : {}),
85
+ });
86
+ const now = Date.now();
87
+ const nextRunAtMs = computeInitialNextRunAtMs(schedule, now);
88
+ const id = `${slugify(options.name)}-${Math.random().toString(36).slice(2, 8)}`;
89
+ const sessionPolicy = parseSessionPolicy(options.sessionPolicy) ?? "stateless";
90
+ const job = {
91
+ id,
92
+ name: options.name,
93
+ enabled: true,
94
+ schedule,
95
+ sessionPolicy,
96
+ prompt: options.prompt,
97
+ delivery: {
98
+ connectorId: options.connectorId,
99
+ routeId: options.routeId,
100
+ channelId: options.channelId,
101
+ ...(options.threadId ? { threadId: options.threadId } : {}),
102
+ },
103
+ createdAtMs: now,
104
+ updatedAtMs: now,
105
+ state: {
106
+ ...(nextRunAtMs !== undefined ? { nextRunAtMs } : {}),
107
+ consecutiveErrors: 0,
108
+ },
109
+ };
110
+ await context.store.upsertJob(job);
111
+ console.log(`Added cron job ${job.id}`);
112
+ console.log(`- schedule: ${describeSchedule(job.schedule)}`);
113
+ console.log(`- delivery: ${job.delivery.connectorId}/${job.delivery.routeId}/${job.delivery.channelId}`);
114
+ console.log(`- cron config: ${context.cronConfigPath}`);
115
+ }
116
+ export async function runCronListCommand(options) {
117
+ const context = await loadCronContext(options.cronConfigPath ? { cronConfigPath: options.cronConfigPath } : undefined);
118
+ const jobs = context.store.listJobs();
119
+ if (options.json) {
120
+ console.log(JSON.stringify({ jobs, cronConfigPath: context.cronConfigPath }, null, 2));
121
+ return;
122
+ }
123
+ if (jobs.length === 0) {
124
+ console.log(`No cron jobs configured (${context.cronConfigPath})`);
125
+ return;
126
+ }
127
+ console.log(`Cron jobs (${context.cronConfigPath}):`);
128
+ for (const job of jobs) {
129
+ const next = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : "-";
130
+ const last = job.state.lastRunAtMs ? new Date(job.state.lastRunAtMs).toISOString() : "-";
131
+ const schedule = describeSchedule(job.schedule);
132
+ console.log(`- ${job.id} [${job.enabled ? "enabled" : "paused"}] ${job.name}`);
133
+ console.log(` schedule=${schedule}`);
134
+ console.log(` next=${next} last=${last} status=${job.state.lastStatus ?? "-"}`);
135
+ console.log(` delivery=${job.delivery.connectorId}/${job.delivery.routeId}/${job.delivery.channelId}`);
136
+ }
137
+ }
138
+ export async function runCronStatusCommand(options) {
139
+ const context = await loadCronContext(options.cronConfigPath ? { cronConfigPath: options.cronConfigPath } : undefined);
140
+ const target = options.jobId ? context.store.getJob(options.jobId) : null;
141
+ if (options.jobId && !target) {
142
+ throw new Error(`Cron job '${options.jobId}' does not exist`);
143
+ }
144
+ if (options.json) {
145
+ if (target) {
146
+ console.log(JSON.stringify(target, null, 2));
147
+ return;
148
+ }
149
+ console.log(JSON.stringify(context.store.listJobs(), null, 2));
150
+ return;
151
+ }
152
+ if (target) {
153
+ console.log(`Cron status for ${target.id}`);
154
+ console.log(`- name: ${target.name}`);
155
+ console.log(`- enabled: ${target.enabled}`);
156
+ console.log(`- schedule: ${describeSchedule(target.schedule)}`);
157
+ console.log(`- sessionPolicy: ${target.sessionPolicy ?? "stateless"}`);
158
+ console.log(`- nextRun: ${target.state.nextRunAtMs ? new Date(target.state.nextRunAtMs).toISOString() : "-"}`);
159
+ console.log(`- lastRun: ${target.state.lastRunAtMs ? new Date(target.state.lastRunAtMs).toISOString() : "-"}`);
160
+ console.log(`- lastStatus: ${target.state.lastStatus ?? "-"}`);
161
+ if (target.state.lastError) {
162
+ console.log(`- lastError: ${target.state.lastError}`);
163
+ }
164
+ return;
165
+ }
166
+ await runCronListCommand({
167
+ ...(options.cronConfigPath ? { cronConfigPath: options.cronConfigPath } : {}),
168
+ json: false,
169
+ });
170
+ }
171
+ export async function runCronRunCommand(options) {
172
+ const context = await loadCronContext(options.cronConfigPath ? { cronConfigPath: options.cronConfigPath } : undefined);
173
+ const now = Date.now();
174
+ await context.store.updateJob(options.jobId, (current) => ({
175
+ ...current,
176
+ enabled: true,
177
+ updatedAtMs: now,
178
+ state: {
179
+ ...current.state,
180
+ nextRunAtMs: now,
181
+ },
182
+ }));
183
+ console.log(`Scheduled cron job ${options.jobId} to run on next scheduler tick.`);
184
+ console.log("Ensure the gateway process is running for execution.");
185
+ }
186
+ export async function runCronUpdateCommand(options) {
187
+ const context = await loadCronContext(options.cronConfigPath ? { cronConfigPath: options.cronConfigPath } : undefined);
188
+ const now = Date.now();
189
+ const hasScheduleUpdate = options.at !== undefined || options.everyMs !== undefined || options.cronExpr !== undefined;
190
+ const nextSchedule = hasScheduleUpdate
191
+ ? parseSchedule({
192
+ ...(options.at ? { at: options.at } : {}),
193
+ ...(options.everyMs !== undefined ? { everyMs: options.everyMs } : {}),
194
+ ...(options.cronExpr ? { cronExpr: options.cronExpr } : {}),
195
+ ...(options.tz ? { tz: options.tz } : {}),
196
+ })
197
+ : null;
198
+ await context.store.updateJob(options.jobId, (current) => {
199
+ const updatedDelivery = {
200
+ connectorId: options.connectorId ?? current.delivery.connectorId,
201
+ routeId: options.routeId ?? current.delivery.routeId,
202
+ channelId: options.channelId ?? current.delivery.channelId,
203
+ };
204
+ const resolvedThreadId = options.clearThread ? undefined : (options.threadId ?? current.delivery.threadId);
205
+ if (resolvedThreadId !== undefined) {
206
+ updatedDelivery.threadId = resolvedThreadId;
207
+ }
208
+ assertDeliveryReferences(context.gatewayConfig, {
209
+ connectorId: updatedDelivery.connectorId,
210
+ routeId: updatedDelivery.routeId,
211
+ });
212
+ const schedule = nextSchedule ?? current.schedule;
213
+ const nextRunAtMs = nextSchedule
214
+ ? computeInitialNextRunAtMs(schedule, now)
215
+ : current.state.nextRunAtMs;
216
+ const nextState = {
217
+ ...current.state,
218
+ };
219
+ if (nextSchedule) {
220
+ if (nextRunAtMs === undefined) {
221
+ delete nextState.nextRunAtMs;
222
+ }
223
+ else {
224
+ nextState.nextRunAtMs = nextRunAtMs;
225
+ }
226
+ }
227
+ const parsedSessionPolicy = parseSessionPolicy(options.sessionPolicy);
228
+ const nextJob = {
229
+ ...current,
230
+ name: options.name ?? current.name,
231
+ prompt: options.prompt ?? current.prompt,
232
+ schedule,
233
+ delivery: updatedDelivery,
234
+ updatedAtMs: now,
235
+ state: nextState,
236
+ };
237
+ const nextSessionPolicy = parsedSessionPolicy ?? current.sessionPolicy;
238
+ if (nextSessionPolicy !== undefined) {
239
+ nextJob.sessionPolicy = nextSessionPolicy;
240
+ }
241
+ else {
242
+ delete nextJob.sessionPolicy;
243
+ }
244
+ return nextJob;
245
+ });
246
+ console.log(`Updated cron job ${options.jobId}`);
247
+ }
248
+ export async function runCronRemoveCommand(options) {
249
+ const context = await loadCronContext(options.cronConfigPath ? { cronConfigPath: options.cronConfigPath } : undefined);
250
+ const removed = await context.store.removeJob(options.jobId);
251
+ if (!removed) {
252
+ throw new Error(`Cron job '${options.jobId}' does not exist`);
253
+ }
254
+ console.log(`Removed cron job ${options.jobId}`);
255
+ }
256
+ export async function runCronPauseCommand(options) {
257
+ const context = await loadCronContext(options.cronConfigPath ? { cronConfigPath: options.cronConfigPath } : undefined);
258
+ const now = Date.now();
259
+ await context.store.updateJob(options.jobId, (current) => ({
260
+ ...current,
261
+ enabled: false,
262
+ updatedAtMs: now,
263
+ }));
264
+ console.log(`Paused cron job ${options.jobId}`);
265
+ }
266
+ export async function runCronResumeCommand(options) {
267
+ const context = await loadCronContext(options.cronConfigPath ? { cronConfigPath: options.cronConfigPath } : undefined);
268
+ const now = Date.now();
269
+ await context.store.updateJob(options.jobId, (current) => {
270
+ const nextState = {
271
+ ...current.state,
272
+ };
273
+ const nextRunAtMs = computeInitialNextRunAtMs(current.schedule, now);
274
+ if (nextRunAtMs === undefined) {
275
+ delete nextState.nextRunAtMs;
276
+ }
277
+ else {
278
+ nextState.nextRunAtMs = nextRunAtMs;
279
+ }
280
+ return {
281
+ ...current,
282
+ enabled: true,
283
+ updatedAtMs: now,
284
+ state: nextState,
285
+ };
286
+ });
287
+ console.log(`Resumed cron job ${options.jobId}`);
288
+ }