@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.
- package/.env.example +9 -0
- package/AGENTS.md +267 -0
- package/README.md +382 -0
- package/ROADMAP.md +34 -0
- package/config/cron.example.json +9 -0
- package/config/gateway.example.json +128 -0
- package/config/models.custom.example.json +27 -0
- package/dist/src/agent/event-forwarder.js +341 -0
- package/dist/src/agent/tests/event-forwarder.test.js +113 -0
- package/dist/src/cli/commands/config.js +243 -0
- package/dist/src/cli/commands/configure.js +61 -0
- package/dist/src/cli/commands/cron.js +288 -0
- package/dist/src/cli/commands/doctor.js +189 -0
- package/dist/src/cli/commands/extension.js +151 -0
- package/dist/src/cli/commands/init.js +286 -0
- package/dist/src/cli/commands/start.js +177 -0
- package/dist/src/cli/commands/topology.js +254 -0
- package/dist/src/cli/index.js +8 -0
- package/dist/src/cli/program.js +386 -0
- package/dist/src/cli/shared/config-io.js +223 -0
- package/dist/src/cli/shared/config-mutators.js +345 -0
- package/dist/src/cli/shared/config-path.js +207 -0
- package/dist/src/cli/shared/config-schema.js +159 -0
- package/dist/src/cli/shared/config-types.js +1 -0
- package/dist/src/cli/shared/configure-sections.js +429 -0
- package/dist/src/cli/shared/discord-config.js +12 -0
- package/dist/src/cli/shared/init-catalog.js +115 -0
- package/dist/src/cli/shared/init-models-file.js +65 -0
- package/dist/src/cli/shared/presets.js +86 -0
- package/dist/src/cli/shared/runtime.js +29 -0
- package/dist/src/cli/shared/schema-prompts.js +325 -0
- package/dist/src/cli/tests/config-command.test.js +42 -0
- package/dist/src/cli/tests/config-io.test.js +64 -0
- package/dist/src/cli/tests/config-mutators.test.js +47 -0
- package/dist/src/cli/tests/config-path.test.js +21 -0
- package/dist/src/cli/tests/discord-config.test.js +23 -0
- package/dist/src/cli/tests/doctor.test.js +107 -0
- package/dist/src/cli/tests/init-catalog.test.js +87 -0
- package/dist/src/cli/tests/presets.test.js +41 -0
- package/dist/src/cli/tests/program-options.test.js +92 -0
- package/dist/src/cli/tests/routing-config.test.js +199 -0
- package/dist/src/cli/tests/routing-legacy.test.js +191 -0
- package/dist/src/core/control-command.js +12 -0
- package/dist/src/core/dedup-store.js +92 -0
- package/dist/src/core/gateway.js +432 -0
- package/dist/src/core/routing.js +306 -0
- package/dist/src/core/runtime-registry.js +119 -0
- package/dist/src/core/tests/control-command.test.js +17 -0
- package/dist/src/core/tests/gateway-update-strategy.test.js +167 -0
- package/dist/src/core/tests/runtime-registry.test.js +116 -0
- package/dist/src/core/tests/typing-controller.test.js +103 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/core/typing-controller.js +88 -0
- package/dist/src/cron/config.js +114 -0
- package/dist/src/cron/schedule.js +49 -0
- package/dist/src/cron/service.js +196 -0
- package/dist/src/cron/store.js +142 -0
- package/dist/src/cron/types.js +1 -0
- package/dist/src/extension/loader.js +97 -0
- package/dist/src/extension/manager.js +269 -0
- package/dist/src/extension/manifest.js +21 -0
- package/dist/src/extension/registry.js +137 -0
- package/dist/src/main.js +6 -0
- package/dist/src/sandbox/executor.js +1 -0
- package/dist/src/sandbox/host-executor.js +111 -0
- package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +175 -0
- package/docs/CRON_SCHEDULER_DESIGN.md +374 -0
- package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +77 -0
- package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +119 -0
- package/docs/MVP.md +135 -0
- package/docs/RUNBOOK.md +242 -0
- package/docs/TEAMWORK_HANDOFF_DESIGN.md +440 -0
- package/package.json +43 -0
- package/plugins/connector-discord/dobby.manifest.json +18 -0
- package/plugins/connector-discord/index.js +1 -0
- package/plugins/connector-discord/package-lock.json +360 -0
- package/plugins/connector-discord/package.json +38 -0
- package/plugins/connector-discord/src/connector.ts +350 -0
- package/plugins/connector-discord/src/contribution.ts +21 -0
- package/plugins/connector-discord/src/mapper.ts +102 -0
- package/plugins/connector-discord/tsconfig.json +19 -0
- package/plugins/connector-feishu/dobby.manifest.json +18 -0
- package/plugins/connector-feishu/index.js +1 -0
- package/plugins/connector-feishu/package-lock.json +618 -0
- package/plugins/connector-feishu/package.json +38 -0
- package/plugins/connector-feishu/src/connector.ts +343 -0
- package/plugins/connector-feishu/src/contribution.ts +26 -0
- package/plugins/connector-feishu/src/mapper.ts +401 -0
- package/plugins/connector-feishu/tsconfig.json +19 -0
- package/plugins/plugin-sdk/index.d.ts +261 -0
- package/plugins/plugin-sdk/index.js +1 -0
- package/plugins/plugin-sdk/package-lock.json +12 -0
- package/plugins/plugin-sdk/package.json +22 -0
- package/plugins/provider-claude/dobby.manifest.json +17 -0
- package/plugins/provider-claude/index.js +1 -0
- package/plugins/provider-claude/package-lock.json +3398 -0
- package/plugins/provider-claude/package.json +39 -0
- package/plugins/provider-claude/src/contribution.ts +1018 -0
- package/plugins/provider-claude/tsconfig.json +19 -0
- package/plugins/provider-claude-cli/dobby.manifest.json +17 -0
- package/plugins/provider-claude-cli/index.js +1 -0
- package/plugins/provider-claude-cli/package-lock.json +2898 -0
- package/plugins/provider-claude-cli/package.json +38 -0
- package/plugins/provider-claude-cli/src/contribution.ts +1673 -0
- package/plugins/provider-claude-cli/tsconfig.json +19 -0
- package/plugins/provider-pi/dobby.manifest.json +17 -0
- package/plugins/provider-pi/index.js +1 -0
- package/plugins/provider-pi/package-lock.json +3877 -0
- package/plugins/provider-pi/package.json +40 -0
- package/plugins/provider-pi/src/contribution.ts +476 -0
- package/plugins/provider-pi/tsconfig.json +19 -0
- package/plugins/sandbox-core/boxlite.js +1 -0
- package/plugins/sandbox-core/dobby.manifest.json +17 -0
- package/plugins/sandbox-core/docker.js +1 -0
- package/plugins/sandbox-core/package-lock.json +136 -0
- package/plugins/sandbox-core/package.json +39 -0
- package/plugins/sandbox-core/src/boxlite-context.ts +2 -0
- package/plugins/sandbox-core/src/boxlite-contribution.ts +53 -0
- package/plugins/sandbox-core/src/boxlite-executor.ts +911 -0
- package/plugins/sandbox-core/src/docker-contribution.ts +43 -0
- package/plugins/sandbox-core/src/docker-executor.ts +217 -0
- package/plugins/sandbox-core/tsconfig.json +19 -0
- package/scripts/local-extensions.mjs +168 -0
- package/src/agent/event-forwarder.ts +414 -0
- package/src/cli/commands/config.ts +328 -0
- package/src/cli/commands/configure.ts +92 -0
- package/src/cli/commands/cron.ts +410 -0
- package/src/cli/commands/doctor.ts +230 -0
- package/src/cli/commands/extension.ts +205 -0
- package/src/cli/commands/init.ts +396 -0
- package/src/cli/commands/start.ts +223 -0
- package/src/cli/commands/topology.ts +383 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/program.ts +465 -0
- package/src/cli/shared/config-io.ts +277 -0
- package/src/cli/shared/config-mutators.ts +440 -0
- package/src/cli/shared/config-schema.ts +228 -0
- package/src/cli/shared/config-types.ts +121 -0
- package/src/cli/shared/configure-sections.ts +551 -0
- package/src/cli/shared/discord-config.ts +14 -0
- package/src/cli/shared/init-catalog.ts +189 -0
- package/src/cli/shared/init-models-file.ts +77 -0
- package/src/cli/shared/runtime.ts +33 -0
- package/src/cli/shared/schema-prompts.ts +414 -0
- package/src/cli/tests/config-command.test.ts +56 -0
- package/src/cli/tests/config-io.test.ts +92 -0
- package/src/cli/tests/config-mutators.test.ts +59 -0
- package/src/cli/tests/doctor.test.ts +120 -0
- package/src/cli/tests/init-catalog.test.ts +96 -0
- package/src/cli/tests/program-options.test.ts +113 -0
- package/src/cli/tests/routing-config.test.ts +209 -0
- package/src/core/control-command.ts +12 -0
- package/src/core/dedup-store.ts +103 -0
- package/src/core/gateway.ts +607 -0
- package/src/core/routing.ts +379 -0
- package/src/core/runtime-registry.ts +141 -0
- package/src/core/tests/control-command.test.ts +20 -0
- package/src/core/tests/runtime-registry.test.ts +140 -0
- package/src/core/tests/typing-controller.test.ts +129 -0
- package/src/core/types.ts +318 -0
- package/src/core/typing-controller.ts +119 -0
- package/src/cron/config.ts +154 -0
- package/src/cron/schedule.ts +61 -0
- package/src/cron/service.ts +249 -0
- package/src/cron/store.ts +155 -0
- package/src/cron/types.ts +60 -0
- package/src/extension/loader.ts +145 -0
- package/src/extension/manager.ts +355 -0
- package/src/extension/manifest.ts +26 -0
- package/src/extension/registry.ts +229 -0
- package/src/main.ts +8 -0
- package/src/sandbox/executor.ts +44 -0
- package/src/sandbox/host-executor.ts +118 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cancel,
|
|
3
|
+
confirm,
|
|
4
|
+
isCancel,
|
|
5
|
+
note,
|
|
6
|
+
password,
|
|
7
|
+
select,
|
|
8
|
+
text,
|
|
9
|
+
} from "@clack/prompts";
|
|
10
|
+
import JSON5 from "json5";
|
|
11
|
+
import {
|
|
12
|
+
ensureGatewayConfigShape,
|
|
13
|
+
setDefaultProviderIfMissingOrInvalid,
|
|
14
|
+
upsertBinding,
|
|
15
|
+
upsertConnectorInstance,
|
|
16
|
+
upsertProviderInstance,
|
|
17
|
+
upsertRoute,
|
|
18
|
+
} from "./config-mutators.js";
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_DISCORD_BOT_NAME,
|
|
21
|
+
DISCORD_CONNECTOR_CONTRIBUTION_ID,
|
|
22
|
+
} from "./discord-config.js";
|
|
23
|
+
import type { RawBindingConfig, RawGatewayConfig } from "./config-types.js";
|
|
24
|
+
import { promptConfigFromSchema } from "./schema-prompts.js";
|
|
25
|
+
|
|
26
|
+
export type ConfigureSection = "provider" | "connector" | "route" | "binding" | "sandbox" | "data";
|
|
27
|
+
|
|
28
|
+
export const CONFIGURE_SECTION_VALUES: ConfigureSection[] = ["provider", "connector", "route", "binding", "sandbox", "data"];
|
|
29
|
+
|
|
30
|
+
export interface ConfigureSectionContext {
|
|
31
|
+
schemaByContributionId?: ReadonlyMap<string, Record<string, unknown>>;
|
|
32
|
+
schemaStateByContributionId?: ReadonlyMap<string, "with_schema" | "without_schema">;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type SchemaFallbackState = "without_schema" | "not_loaded";
|
|
36
|
+
|
|
37
|
+
const SECTION_ORDER: Record<ConfigureSection, number> = {
|
|
38
|
+
provider: 1,
|
|
39
|
+
connector: 2,
|
|
40
|
+
sandbox: 3,
|
|
41
|
+
route: 4,
|
|
42
|
+
binding: 5,
|
|
43
|
+
data: 6,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function isConfigureSection(value: string): value is ConfigureSection {
|
|
47
|
+
return CONFIGURE_SECTION_VALUES.includes(value as ConfigureSection);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function normalizeConfigureSectionOrder(sections: ConfigureSection[]): ConfigureSection[] {
|
|
51
|
+
const unique: ConfigureSection[] = [];
|
|
52
|
+
for (const section of sections) {
|
|
53
|
+
if (!unique.includes(section)) {
|
|
54
|
+
unique.push(section);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return unique.sort((a, b) => SECTION_ORDER[a] - SECTION_ORDER[b]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function requiredText(message: string, initialValue?: string, placeholder?: string): Promise<string> {
|
|
62
|
+
while (true) {
|
|
63
|
+
const result = await text({
|
|
64
|
+
message,
|
|
65
|
+
...(initialValue !== undefined ? { initialValue } : {}),
|
|
66
|
+
...(placeholder !== undefined ? { placeholder } : {}),
|
|
67
|
+
});
|
|
68
|
+
if (isCancel(result)) {
|
|
69
|
+
cancel("Configure cancelled.");
|
|
70
|
+
throw new Error("Configure cancelled.");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const trimmed = String(result ?? "").trim();
|
|
74
|
+
if (trimmed.length > 0) {
|
|
75
|
+
return trimmed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await note("This field is required.", "Validation");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function optionalText(message: string, initialValue?: string, placeholder?: string): Promise<string | undefined> {
|
|
83
|
+
const result = await text({
|
|
84
|
+
message,
|
|
85
|
+
...(initialValue !== undefined ? { initialValue } : {}),
|
|
86
|
+
...(placeholder !== undefined ? { placeholder } : {}),
|
|
87
|
+
});
|
|
88
|
+
if (isCancel(result)) {
|
|
89
|
+
cancel("Configure cancelled.");
|
|
90
|
+
throw new Error("Configure cancelled.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const trimmed = String(result ?? "").trim();
|
|
94
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function noteSchemaFallback(
|
|
98
|
+
contributionId: string,
|
|
99
|
+
context?: ConfigureSectionContext,
|
|
100
|
+
): Promise<SchemaFallbackState> {
|
|
101
|
+
const state = context?.schemaStateByContributionId?.get(contributionId);
|
|
102
|
+
if (state === "without_schema") {
|
|
103
|
+
await note(
|
|
104
|
+
`Contribution '${contributionId}' is loaded but does not expose configSchema. Falling back to JSON input.`,
|
|
105
|
+
"Schema",
|
|
106
|
+
);
|
|
107
|
+
return "without_schema";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await note(
|
|
111
|
+
`No loaded schema for contribution '${contributionId}'. The extension may be disabled or not installed. Falling back to JSON input.`,
|
|
112
|
+
"Schema",
|
|
113
|
+
);
|
|
114
|
+
return "not_loaded";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function confirmUnloadedSchemaFallback(contributionId: string, state: SchemaFallbackState): Promise<void> {
|
|
118
|
+
if (state !== "not_loaded") {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const proceed = await confirm({
|
|
123
|
+
message: `Continue with raw JSON for '${contributionId}'? Defaults will not be auto-applied.`,
|
|
124
|
+
initialValue: false,
|
|
125
|
+
});
|
|
126
|
+
if (isCancel(proceed)) {
|
|
127
|
+
cancel("Configure cancelled.");
|
|
128
|
+
throw new Error("Configure cancelled.");
|
|
129
|
+
}
|
|
130
|
+
if (!proceed) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Cannot continue without schema for contribution '${contributionId}'. ` +
|
|
133
|
+
`Enable/install the extension first (check 'extensions.allowList'), then retry.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function promptJsonObject(
|
|
139
|
+
message: string,
|
|
140
|
+
initialValue: Record<string, unknown>,
|
|
141
|
+
): Promise<Record<string, unknown>> {
|
|
142
|
+
const result = await text({
|
|
143
|
+
message,
|
|
144
|
+
initialValue: JSON.stringify(initialValue, null, 2),
|
|
145
|
+
});
|
|
146
|
+
if (isCancel(result)) {
|
|
147
|
+
cancel("Configure cancelled.");
|
|
148
|
+
throw new Error("Configure cancelled.");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const rawText = String(result ?? "{}").trim();
|
|
152
|
+
if (rawText.length === 0) {
|
|
153
|
+
return {};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const parsed = JSON5.parse(rawText);
|
|
157
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
158
|
+
throw new Error("Config must be a JSON object");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return parsed as Record<string, unknown>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function configureProviderSection(config: RawGatewayConfig, context?: ConfigureSectionContext): Promise<void> {
|
|
165
|
+
const next = ensureGatewayConfigShape(config);
|
|
166
|
+
const providerItems = next.providers.items;
|
|
167
|
+
|
|
168
|
+
const providerChoices = Object.keys(providerItems).sort((a, b) => a.localeCompare(b));
|
|
169
|
+
const targetProvider = providerChoices.length === 0
|
|
170
|
+
? "__new"
|
|
171
|
+
: await select({
|
|
172
|
+
message: "Select provider instance",
|
|
173
|
+
options: [
|
|
174
|
+
...providerChoices.map((id) => ({ value: id, label: id })),
|
|
175
|
+
{ value: "__new", label: "Create new provider instance" },
|
|
176
|
+
],
|
|
177
|
+
initialValue: providerChoices[0],
|
|
178
|
+
});
|
|
179
|
+
if (isCancel(targetProvider)) {
|
|
180
|
+
cancel("Configure cancelled.");
|
|
181
|
+
throw new Error("Configure cancelled.");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const instanceId = String(targetProvider) === "__new"
|
|
185
|
+
? await requiredText("New provider instance ID", "pi.main")
|
|
186
|
+
: String(targetProvider);
|
|
187
|
+
const existing = providerItems[instanceId];
|
|
188
|
+
const contributionId = await requiredText(
|
|
189
|
+
"Provider contribution ID",
|
|
190
|
+
existing?.type ?? "provider.pi",
|
|
191
|
+
"provider.pi",
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
let providerConfig: Record<string, unknown> = {};
|
|
195
|
+
const existingConfig = existing ? Object.fromEntries(Object.entries(existing).filter(([key]) => key !== "type")) : {};
|
|
196
|
+
if (context?.schemaByContributionId?.has(contributionId)) {
|
|
197
|
+
providerConfig = await promptConfigFromSchema(
|
|
198
|
+
context.schemaByContributionId.get(contributionId)!,
|
|
199
|
+
existingConfig,
|
|
200
|
+
{ title: `Provider '${instanceId}' (${contributionId})` },
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
const fallbackState = await noteSchemaFallback(contributionId, context);
|
|
204
|
+
await confirmUnloadedSchemaFallback(contributionId, fallbackState);
|
|
205
|
+
providerConfig = await promptJsonObject("Provider config JSON", existingConfig);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
upsertProviderInstance(next, instanceId, contributionId, providerConfig);
|
|
209
|
+
setDefaultProviderIfMissingOrInvalid(next);
|
|
210
|
+
|
|
211
|
+
const providerIds = Object.keys(next.providers.items).sort((a, b) => a.localeCompare(b));
|
|
212
|
+
if (providerIds.length > 0) {
|
|
213
|
+
const defaultProvider = await select({
|
|
214
|
+
message: "Default provider",
|
|
215
|
+
options: providerIds.map((id) => ({ value: id, label: id })),
|
|
216
|
+
initialValue: next.providers.default && providerIds.includes(next.providers.default) ? next.providers.default : providerIds[0],
|
|
217
|
+
});
|
|
218
|
+
if (isCancel(defaultProvider)) {
|
|
219
|
+
cancel("Configure cancelled.");
|
|
220
|
+
throw new Error("Configure cancelled.");
|
|
221
|
+
}
|
|
222
|
+
next.providers.default = String(defaultProvider);
|
|
223
|
+
next.routes.defaults.provider = String(defaultProvider);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
Object.assign(config, next);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function configureConnectorSection(config: RawGatewayConfig, context?: ConfigureSectionContext): Promise<void> {
|
|
230
|
+
const next = ensureGatewayConfigShape(config);
|
|
231
|
+
const connectorItems = next.connectors.items;
|
|
232
|
+
|
|
233
|
+
const connectorChoices = Object.keys(connectorItems).sort((a, b) => a.localeCompare(b));
|
|
234
|
+
const targetConnector = connectorChoices.length === 0
|
|
235
|
+
? "__new"
|
|
236
|
+
: await select({
|
|
237
|
+
message: "Select connector instance",
|
|
238
|
+
options: [
|
|
239
|
+
...connectorChoices.map((id) => ({ value: id, label: id })),
|
|
240
|
+
{ value: "__new", label: "Create new connector instance" },
|
|
241
|
+
],
|
|
242
|
+
initialValue: connectorChoices[0],
|
|
243
|
+
});
|
|
244
|
+
if (isCancel(targetConnector)) {
|
|
245
|
+
cancel("Configure cancelled.");
|
|
246
|
+
throw new Error("Configure cancelled.");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const instanceId = String(targetConnector) === "__new"
|
|
250
|
+
? await requiredText("New connector instance ID", "discord.main")
|
|
251
|
+
: String(targetConnector);
|
|
252
|
+
const existing = connectorItems[instanceId];
|
|
253
|
+
const contributionId = await requiredText(
|
|
254
|
+
"Connector contribution ID",
|
|
255
|
+
existing?.type ?? DISCORD_CONNECTOR_CONTRIBUTION_ID,
|
|
256
|
+
DISCORD_CONNECTOR_CONTRIBUTION_ID,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
let connectorConfig: Record<string, unknown> = {};
|
|
260
|
+
const existingConfig = existing ? Object.fromEntries(Object.entries(existing).filter(([key]) => key !== "type")) : {};
|
|
261
|
+
if (contributionId === DISCORD_CONNECTOR_CONTRIBUTION_ID) {
|
|
262
|
+
const botName = await requiredText(
|
|
263
|
+
"Discord botName",
|
|
264
|
+
typeof existing?.botName === "string" ? existing.botName : DEFAULT_DISCORD_BOT_NAME,
|
|
265
|
+
);
|
|
266
|
+
const botTokenResult = await password({
|
|
267
|
+
message: "Discord botToken (leave blank to keep current value)",
|
|
268
|
+
mask: "*",
|
|
269
|
+
});
|
|
270
|
+
if (isCancel(botTokenResult)) {
|
|
271
|
+
cancel("Configure cancelled.");
|
|
272
|
+
throw new Error("Configure cancelled.");
|
|
273
|
+
}
|
|
274
|
+
const providedBotToken = String(botTokenResult ?? "").trim();
|
|
275
|
+
const existingBotToken = typeof existing?.botToken === "string" ? existing.botToken : "";
|
|
276
|
+
const botToken = providedBotToken.length > 0 ? providedBotToken : existingBotToken;
|
|
277
|
+
if (botToken.length === 0) {
|
|
278
|
+
throw new Error("Discord botToken is required");
|
|
279
|
+
}
|
|
280
|
+
connectorConfig = {
|
|
281
|
+
...existingConfig,
|
|
282
|
+
botName,
|
|
283
|
+
botToken,
|
|
284
|
+
};
|
|
285
|
+
} else if (context?.schemaByContributionId?.has(contributionId)) {
|
|
286
|
+
connectorConfig = await promptConfigFromSchema(
|
|
287
|
+
context.schemaByContributionId.get(contributionId)!,
|
|
288
|
+
existingConfig,
|
|
289
|
+
{ title: `Connector '${instanceId}' (${contributionId})` },
|
|
290
|
+
);
|
|
291
|
+
} else {
|
|
292
|
+
const fallbackState = await noteSchemaFallback(contributionId, context);
|
|
293
|
+
await confirmUnloadedSchemaFallback(contributionId, fallbackState);
|
|
294
|
+
connectorConfig = await promptJsonObject("Connector config JSON", existingConfig);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
upsertConnectorInstance(next, instanceId, contributionId, connectorConfig);
|
|
298
|
+
Object.assign(config, next);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function configureRouteSection(config: RawGatewayConfig): Promise<void> {
|
|
302
|
+
const next = ensureGatewayConfigShape(config);
|
|
303
|
+
const routeItems = next.routes.items;
|
|
304
|
+
const routeChoices = Object.keys(routeItems).sort((a, b) => a.localeCompare(b));
|
|
305
|
+
|
|
306
|
+
const targetRoute = routeChoices.length === 0
|
|
307
|
+
? "__new"
|
|
308
|
+
: await select({
|
|
309
|
+
message: "Select route",
|
|
310
|
+
options: [
|
|
311
|
+
...routeChoices.map((id) => ({ value: id, label: id })),
|
|
312
|
+
{ value: "__new", label: "Create new route" },
|
|
313
|
+
],
|
|
314
|
+
initialValue: routeChoices[0],
|
|
315
|
+
});
|
|
316
|
+
if (isCancel(targetRoute)) {
|
|
317
|
+
cancel("Configure cancelled.");
|
|
318
|
+
throw new Error("Configure cancelled.");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const routeId = String(targetRoute) === "__new" ? await requiredText("New route ID", "main") : String(targetRoute);
|
|
322
|
+
const existing = routeItems[routeId];
|
|
323
|
+
|
|
324
|
+
const projectRoot = await requiredText("projectRoot", existing?.projectRoot ?? process.cwd());
|
|
325
|
+
const tools = await select({
|
|
326
|
+
message: "tools",
|
|
327
|
+
options: [
|
|
328
|
+
{ value: "__default", label: `Use route default (${next.routes.defaults.tools ?? "full"})` },
|
|
329
|
+
{ value: "full", label: "full" },
|
|
330
|
+
{ value: "readonly", label: "readonly" },
|
|
331
|
+
],
|
|
332
|
+
initialValue: existing?.tools ?? "__default",
|
|
333
|
+
});
|
|
334
|
+
if (isCancel(tools)) {
|
|
335
|
+
cancel("Configure cancelled.");
|
|
336
|
+
throw new Error("Configure cancelled.");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const mentions = await select({
|
|
340
|
+
message: "mentions",
|
|
341
|
+
options: [
|
|
342
|
+
{ value: "__default", label: `Use route default (${next.routes.defaults.mentions ?? "required"})` },
|
|
343
|
+
{ value: "required", label: "required" },
|
|
344
|
+
{ value: "optional", label: "optional" },
|
|
345
|
+
],
|
|
346
|
+
initialValue: existing?.mentions ?? "__default",
|
|
347
|
+
});
|
|
348
|
+
if (isCancel(mentions)) {
|
|
349
|
+
cancel("Configure cancelled.");
|
|
350
|
+
throw new Error("Configure cancelled.");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const providerIds = Object.keys(next.providers.items).sort((a, b) => a.localeCompare(b));
|
|
354
|
+
const providerValue = providerIds.length > 0
|
|
355
|
+
? await select({
|
|
356
|
+
message: "provider",
|
|
357
|
+
options: [
|
|
358
|
+
{ value: "__default", label: `Use route default (${(next.routes.defaults.provider ?? next.providers.default) || "(unset)"})` },
|
|
359
|
+
...providerIds.map((id) => ({ value: id, label: id })),
|
|
360
|
+
],
|
|
361
|
+
initialValue: existing?.provider ?? "__default",
|
|
362
|
+
})
|
|
363
|
+
: "__default";
|
|
364
|
+
if (isCancel(providerValue)) {
|
|
365
|
+
cancel("Configure cancelled.");
|
|
366
|
+
throw new Error("Configure cancelled.");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const sandboxIds = ["host.builtin", ...Object.keys(next.sandboxes.items).sort((a, b) => a.localeCompare(b))];
|
|
370
|
+
const sandboxValue = await select({
|
|
371
|
+
message: "sandbox",
|
|
372
|
+
options: [
|
|
373
|
+
{ value: "__default", label: `Use route default (${next.routes.defaults.sandbox ?? next.sandboxes.default})` },
|
|
374
|
+
...sandboxIds.map((id) => ({ value: id, label: id })),
|
|
375
|
+
],
|
|
376
|
+
initialValue: existing?.sandbox ?? "__default",
|
|
377
|
+
});
|
|
378
|
+
if (isCancel(sandboxValue)) {
|
|
379
|
+
cancel("Configure cancelled.");
|
|
380
|
+
throw new Error("Configure cancelled.");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const systemPromptFile = await optionalText("systemPromptFile (optional)", existing?.systemPromptFile ?? "");
|
|
384
|
+
|
|
385
|
+
upsertRoute(next, routeId, {
|
|
386
|
+
projectRoot,
|
|
387
|
+
...(tools !== "__default" ? { tools: String(tools) as "full" | "readonly" } : {}),
|
|
388
|
+
...(mentions !== "__default" ? { mentions: String(mentions) as "required" | "optional" } : {}),
|
|
389
|
+
...(providerValue !== "__default" ? { provider: String(providerValue) } : {}),
|
|
390
|
+
...(sandboxValue !== "__default" ? { sandbox: String(sandboxValue) } : {}),
|
|
391
|
+
...(systemPromptFile ? { systemPromptFile } : {}),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
Object.assign(config, next);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async function configureBindingSection(config: RawGatewayConfig): Promise<void> {
|
|
398
|
+
const next = ensureGatewayConfigShape(config);
|
|
399
|
+
const bindingItems = next.bindings.items;
|
|
400
|
+
const bindingChoices = Object.keys(bindingItems).sort((a, b) => a.localeCompare(b));
|
|
401
|
+
|
|
402
|
+
if (Object.keys(next.connectors.items).length === 0) {
|
|
403
|
+
throw new Error("No connectors found. Configure connectors first.");
|
|
404
|
+
}
|
|
405
|
+
if (Object.keys(next.routes.items).length === 0) {
|
|
406
|
+
throw new Error("No routes found. Configure routes first.");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const targetBinding = bindingChoices.length === 0
|
|
410
|
+
? "__new"
|
|
411
|
+
: await select({
|
|
412
|
+
message: "Select binding",
|
|
413
|
+
options: [
|
|
414
|
+
...bindingChoices.map((id) => ({ value: id, label: id })),
|
|
415
|
+
{ value: "__new", label: "Create new binding" },
|
|
416
|
+
],
|
|
417
|
+
initialValue: bindingChoices[0],
|
|
418
|
+
});
|
|
419
|
+
if (isCancel(targetBinding)) {
|
|
420
|
+
cancel("Configure cancelled.");
|
|
421
|
+
throw new Error("Configure cancelled.");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const bindingId = String(targetBinding) === "__new"
|
|
425
|
+
? await requiredText("New binding ID", "discord.main.main")
|
|
426
|
+
: String(targetBinding);
|
|
427
|
+
const existing = bindingItems[bindingId];
|
|
428
|
+
|
|
429
|
+
const connectorIds = Object.keys(next.connectors.items).sort((a, b) => a.localeCompare(b));
|
|
430
|
+
const connectorId = await select({
|
|
431
|
+
message: "connector",
|
|
432
|
+
options: connectorIds.map((id) => ({ value: id, label: id })),
|
|
433
|
+
initialValue: existing?.connector && connectorIds.includes(existing.connector) ? existing.connector : connectorIds[0],
|
|
434
|
+
});
|
|
435
|
+
if (isCancel(connectorId)) {
|
|
436
|
+
cancel("Configure cancelled.");
|
|
437
|
+
throw new Error("Configure cancelled.");
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const sourceType = await select({
|
|
441
|
+
message: "source.type",
|
|
442
|
+
options: [
|
|
443
|
+
{ value: "channel", label: "channel" },
|
|
444
|
+
{ value: "chat", label: "chat" },
|
|
445
|
+
],
|
|
446
|
+
initialValue: existing?.source.type ?? "channel",
|
|
447
|
+
});
|
|
448
|
+
if (isCancel(sourceType)) {
|
|
449
|
+
cancel("Configure cancelled.");
|
|
450
|
+
throw new Error("Configure cancelled.");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const sourceId = await requiredText("source.id", existing?.source.id);
|
|
454
|
+
const routeIds = Object.keys(next.routes.items).sort((a, b) => a.localeCompare(b));
|
|
455
|
+
const routeId = await select({
|
|
456
|
+
message: "route",
|
|
457
|
+
options: routeIds.map((id) => ({ value: id, label: id })),
|
|
458
|
+
initialValue: existing?.route && routeIds.includes(existing.route) ? existing.route : routeIds[0],
|
|
459
|
+
});
|
|
460
|
+
if (isCancel(routeId)) {
|
|
461
|
+
cancel("Configure cancelled.");
|
|
462
|
+
throw new Error("Configure cancelled.");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const duplicate = Object.entries(next.bindings.items).find(([existingBindingId, binding]) =>
|
|
466
|
+
existingBindingId !== bindingId
|
|
467
|
+
&& binding.connector === connectorId
|
|
468
|
+
&& binding.source.type === sourceType
|
|
469
|
+
&& binding.source.id === sourceId
|
|
470
|
+
);
|
|
471
|
+
if (duplicate) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`Binding source '${String(connectorId)}/${String(sourceType)}:${sourceId}' is already used by '${duplicate[0]}'`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
upsertBinding(next, bindingId, {
|
|
478
|
+
connector: String(connectorId),
|
|
479
|
+
source: {
|
|
480
|
+
type: String(sourceType) as "channel" | "chat",
|
|
481
|
+
id: sourceId,
|
|
482
|
+
},
|
|
483
|
+
route: String(routeId),
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
Object.assign(config, next);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function configureSandboxSection(config: RawGatewayConfig): Promise<void> {
|
|
490
|
+
const next = ensureGatewayConfigShape(config);
|
|
491
|
+
const sandboxIds = ["host.builtin", ...Object.keys(next.sandboxes.items).sort((a, b) => a.localeCompare(b))];
|
|
492
|
+
|
|
493
|
+
const defaultSandbox = await select({
|
|
494
|
+
message: "Default sandbox",
|
|
495
|
+
options: sandboxIds.map((id) => ({ value: id, label: id })),
|
|
496
|
+
initialValue: next.sandboxes.default && sandboxIds.includes(next.sandboxes.default) ? next.sandboxes.default : "host.builtin",
|
|
497
|
+
});
|
|
498
|
+
if (isCancel(defaultSandbox)) {
|
|
499
|
+
cancel("Configure cancelled.");
|
|
500
|
+
throw new Error("Configure cancelled.");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
next.sandboxes.default = String(defaultSandbox);
|
|
504
|
+
next.routes.defaults.sandbox = String(defaultSandbox);
|
|
505
|
+
Object.assign(config, next);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function configureDataSection(config: RawGatewayConfig): Promise<void> {
|
|
509
|
+
const next = ensureGatewayConfigShape(config);
|
|
510
|
+
const rootDir = await requiredText("data.rootDir", next.data.rootDir);
|
|
511
|
+
const dedupTtlMsRaw = await requiredText("data.dedupTtlMs", String(next.data.dedupTtlMs));
|
|
512
|
+
const dedupTtlMs = Number.parseInt(dedupTtlMsRaw, 10);
|
|
513
|
+
if (!Number.isFinite(dedupTtlMs) || dedupTtlMs <= 0) {
|
|
514
|
+
throw new Error("data.dedupTtlMs must be a positive integer");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
next.data = {
|
|
518
|
+
...next.data,
|
|
519
|
+
rootDir,
|
|
520
|
+
dedupTtlMs,
|
|
521
|
+
};
|
|
522
|
+
Object.assign(config, next);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export async function applyConfigureSection(
|
|
526
|
+
section: ConfigureSection,
|
|
527
|
+
config: RawGatewayConfig,
|
|
528
|
+
context?: ConfigureSectionContext,
|
|
529
|
+
): Promise<void> {
|
|
530
|
+
if (section === "provider") {
|
|
531
|
+
await configureProviderSection(config, context);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (section === "connector") {
|
|
535
|
+
await configureConnectorSection(config, context);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (section === "route") {
|
|
539
|
+
await configureRouteSection(config);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (section === "binding") {
|
|
543
|
+
await configureBindingSection(config);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (section === "sandbox") {
|
|
547
|
+
await configureSandboxSection(config);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
await configureDataSection(config);
|
|
551
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contribution id used by the built-in Discord connector plugin.
|
|
3
|
+
*/
|
|
4
|
+
export const DISCORD_CONNECTOR_CONTRIBUTION_ID = "connector.discord";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default connector instance id used by starter init flows.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID = "discord.main";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default human-readable bot name for starter setups.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_DISCORD_BOT_NAME = "dobby-main";
|