@dobby.ai/dobby 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +0 -1
- package/AGENTS.md +7 -7
- package/README.md +64 -32
- package/config/gateway.example.json +10 -6
- package/dist/plugins/connector-discord/src/mapper.js +75 -0
- package/dist/src/cli/commands/doctor.js +81 -2
- package/dist/src/cli/commands/extension.js +3 -1
- package/dist/src/cli/commands/init.js +43 -173
- package/dist/src/cli/commands/topology.js +38 -14
- package/dist/src/cli/program.js +15 -131
- package/dist/src/cli/shared/config-io.js +3 -31
- package/dist/src/cli/shared/config-mutators.js +33 -9
- package/dist/src/cli/shared/configure-sections.js +52 -12
- package/dist/src/cli/shared/init-catalog.js +89 -46
- package/dist/src/cli/shared/local-extension-specs.js +85 -0
- package/dist/src/cli/shared/schema-prompts.js +26 -2
- package/dist/src/cli/tests/config-io.test.js +5 -5
- package/dist/src/cli/tests/discord-mapper.test.js +90 -0
- package/dist/src/cli/tests/doctor.test.js +145 -0
- package/dist/src/cli/tests/init-catalog.test.js +108 -61
- package/dist/src/cli/tests/program-options.test.js +14 -28
- package/dist/src/cli/tests/routing-config.test.js +59 -4
- package/dist/src/core/gateway.js +3 -1
- package/dist/src/core/routing.js +53 -38
- package/dist/src/main.js +0 -0
- package/dist/src/shared/dobby-repo.js +40 -0
- package/docs/RUNBOOK.md +28 -27
- package/package.json +3 -2
- package/plugins/connector-discord/package-lock.json +2 -2
- package/plugins/connector-discord/package.json +1 -1
- package/plugins/connector-discord/src/connector.ts +0 -5
- package/plugins/connector-discord/src/mapper.ts +3 -4
- package/plugins/connector-feishu/package-lock.json +2 -2
- package/plugins/connector-feishu/package.json +1 -1
- package/plugins/plugin-sdk/package-lock.json +2 -2
- package/plugins/plugin-sdk/package.json +1 -1
- package/plugins/provider-claude/package-lock.json +2 -2
- package/plugins/provider-claude/package.json +1 -1
- package/plugins/provider-claude-cli/package-lock.json +2 -2
- package/plugins/provider-claude-cli/package.json +1 -1
- package/plugins/provider-pi/package-lock.json +2 -2
- package/plugins/provider-pi/package.json +1 -1
- package/plugins/provider-pi/src/contribution.ts +139 -9
- package/src/cli/commands/doctor.ts +103 -2
- package/src/cli/commands/extension.ts +3 -1
- package/src/cli/commands/init.ts +45 -230
- package/src/cli/commands/topology.ts +48 -16
- package/src/cli/program.ts +16 -167
- package/src/cli/shared/config-io.ts +3 -35
- package/src/cli/shared/config-mutators.ts +39 -9
- package/src/cli/shared/config-types.ts +10 -2
- package/src/cli/shared/configure-sections.ts +55 -11
- package/src/cli/shared/init-catalog.ts +126 -66
- package/src/cli/shared/local-extension-specs.ts +108 -0
- package/src/cli/shared/schema-prompts.ts +30 -1
- package/src/cli/tests/config-io.test.ts +5 -5
- package/src/cli/tests/discord-mapper.test.ts +128 -0
- package/src/cli/tests/doctor.test.ts +149 -0
- package/src/cli/tests/init-catalog.test.ts +112 -64
- package/src/cli/tests/program-options.test.ts +14 -32
- package/src/cli/tests/routing-config.test.ts +76 -4
- package/src/core/gateway.ts +3 -1
- package/src/core/routing.ts +70 -45
- package/src/core/types.ts +8 -2
- package/src/shared/dobby-repo.ts +48 -0
- package/config/models.custom.example.json +0 -27
- package/dist/src/agent/tests/event-forwarder.test.js +0 -113
- package/dist/src/cli/shared/config-path.js +0 -207
- package/dist/src/cli/shared/init-models-file.js +0 -65
- package/dist/src/cli/shared/presets.js +0 -86
- package/dist/src/cli/tests/config-path.test.js +0 -21
- package/dist/src/cli/tests/discord-config.test.js +0 -23
- package/dist/src/cli/tests/presets.test.js +0 -41
- package/dist/src/cli/tests/routing-legacy.test.js +0 -191
- package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
- package/src/cli/shared/init-models-file.ts +0 -77
package/src/cli/commands/init.ts
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cancel,
|
|
3
|
-
confirm,
|
|
4
3
|
intro,
|
|
5
4
|
isCancel,
|
|
6
5
|
multiselect,
|
|
7
|
-
note,
|
|
8
6
|
outro,
|
|
9
|
-
password,
|
|
10
7
|
select,
|
|
11
8
|
spinner,
|
|
12
|
-
text,
|
|
13
9
|
} from "@clack/prompts";
|
|
14
10
|
import { ExtensionStoreManager } from "../../extension/manager.js";
|
|
15
11
|
import {
|
|
@@ -20,8 +16,7 @@ import {
|
|
|
20
16
|
upsertProviderInstance,
|
|
21
17
|
upsertRoute,
|
|
22
18
|
} from "../shared/config-mutators.js";
|
|
23
|
-
import {
|
|
24
|
-
import { applyAndValidateContributionSchemas, loadContributionSchemaCatalog } from "../shared/config-schema.js";
|
|
19
|
+
import { applyAndValidateContributionSchemas } from "../shared/config-schema.js";
|
|
25
20
|
import {
|
|
26
21
|
readRawConfig,
|
|
27
22
|
resolveConfigPath,
|
|
@@ -37,53 +32,17 @@ import {
|
|
|
37
32
|
type InitConnectorChoiceId,
|
|
38
33
|
type InitProviderChoiceId,
|
|
39
34
|
} from "../shared/init-catalog.js";
|
|
40
|
-
import {
|
|
35
|
+
import { resolveExtensionInstallSpecs } from "../shared/local-extension-specs.js";
|
|
41
36
|
import { createLogger } from "../shared/runtime.js";
|
|
42
|
-
import { promptConfigFromSchema } from "../shared/schema-prompts.js";
|
|
43
37
|
|
|
44
38
|
interface InitInput {
|
|
45
39
|
providerChoiceIds: InitProviderChoiceId[];
|
|
46
40
|
routeProviderChoiceId: InitProviderChoiceId;
|
|
47
|
-
|
|
48
|
-
projectRoot: string;
|
|
49
|
-
channelId: string;
|
|
50
|
-
routeId: string;
|
|
51
|
-
botName: string;
|
|
52
|
-
botToken: string;
|
|
53
|
-
allowAllMessages: boolean;
|
|
41
|
+
connectorChoiceIds: InitConnectorChoiceId[];
|
|
54
42
|
}
|
|
55
43
|
|
|
56
44
|
/**
|
|
57
|
-
*
|
|
58
|
-
*/
|
|
59
|
-
async function promptRequiredText(params: {
|
|
60
|
-
message: string;
|
|
61
|
-
placeholder?: string;
|
|
62
|
-
initialValue?: string;
|
|
63
|
-
}): Promise<string> {
|
|
64
|
-
while (true) {
|
|
65
|
-
const promptOptions = {
|
|
66
|
-
message: params.message,
|
|
67
|
-
...(params.placeholder !== undefined ? { placeholder: params.placeholder } : {}),
|
|
68
|
-
...(params.initialValue !== undefined ? { initialValue: params.initialValue } : {}),
|
|
69
|
-
};
|
|
70
|
-
const result = await text(promptOptions);
|
|
71
|
-
if (isCancel(result)) {
|
|
72
|
-
cancel("Initialization cancelled.");
|
|
73
|
-
throw new Error("Initialization cancelled.");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const value = String(result ?? "").trim();
|
|
77
|
-
if (value.length > 0) {
|
|
78
|
-
return value;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
await note("This field is required.", "Validation");
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Collects init inputs from interactive prompts.
|
|
45
|
+
* Collects high-level starter choices only; config values are written as templates.
|
|
87
46
|
*/
|
|
88
47
|
async function collectInitInput(): Promise<InitInput> {
|
|
89
48
|
intro("dobby init");
|
|
@@ -102,6 +61,7 @@ async function collectInitInput(): Promise<InitInput> {
|
|
|
102
61
|
cancel("Initialization cancelled.");
|
|
103
62
|
throw new Error("Initialization cancelled.");
|
|
104
63
|
}
|
|
64
|
+
|
|
105
65
|
const providerChoiceIds = (providerChoiceResult as unknown[]).map((value) => String(value));
|
|
106
66
|
if (providerChoiceIds.length === 0) {
|
|
107
67
|
throw new Error("At least one provider must be selected");
|
|
@@ -110,12 +70,12 @@ async function collectInitInput(): Promise<InitInput> {
|
|
|
110
70
|
const invalidChoice = providerChoiceIds.find((providerChoiceId) => !isInitProviderChoiceId(providerChoiceId));
|
|
111
71
|
throw new Error(`Unsupported provider choice '${invalidChoice}'`);
|
|
112
72
|
}
|
|
113
|
-
const providerChoicesById = new Map(providerChoices.map((choice) => [choice.id, choice]));
|
|
114
73
|
|
|
74
|
+
const providerChoicesById = new Map(providerChoices.map((choice) => [choice.id, choice]));
|
|
115
75
|
let routeProviderChoiceId = providerChoiceIds[0] as InitProviderChoiceId;
|
|
116
76
|
if (providerChoiceIds.length > 1) {
|
|
117
77
|
const routeProviderChoiceResult = await select({
|
|
118
|
-
message: "Choose
|
|
78
|
+
message: "Choose default provider",
|
|
119
79
|
options: providerChoiceIds.map((providerChoiceId) => ({
|
|
120
80
|
value: providerChoiceId,
|
|
121
81
|
label: providerChoicesById.get(providerChoiceId as InitProviderChoiceId)?.label ?? providerChoiceId,
|
|
@@ -135,115 +95,63 @@ async function collectInitInput(): Promise<InitInput> {
|
|
|
135
95
|
}
|
|
136
96
|
|
|
137
97
|
const connectorChoices = listInitConnectorChoices();
|
|
138
|
-
const connectorChoiceResult = await
|
|
139
|
-
message: "Choose connector",
|
|
98
|
+
const connectorChoiceResult = await multiselect({
|
|
99
|
+
message: "Choose connector(s) (space to select multiple)",
|
|
140
100
|
options: connectorChoices.map((item) => ({
|
|
141
101
|
value: item.id,
|
|
142
102
|
label: item.label,
|
|
143
103
|
})),
|
|
144
|
-
|
|
104
|
+
initialValues: ["connector.discord"],
|
|
105
|
+
required: true,
|
|
145
106
|
});
|
|
146
107
|
if (isCancel(connectorChoiceResult)) {
|
|
147
108
|
cancel("Initialization cancelled.");
|
|
148
109
|
throw new Error("Initialization cancelled.");
|
|
149
110
|
}
|
|
150
|
-
const connectorChoiceId = String(connectorChoiceResult);
|
|
151
|
-
if (!isInitConnectorChoiceId(connectorChoiceId)) {
|
|
152
|
-
throw new Error(`Unsupported connector choice '${connectorChoiceId}'`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const projectRoot = await promptRequiredText({
|
|
156
|
-
message: "Project root",
|
|
157
|
-
initialValue: process.cwd(),
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const channelId = await promptRequiredText({
|
|
161
|
-
message: "Discord channel ID",
|
|
162
|
-
placeholder: "1234567890",
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const routeIdResult = await text({
|
|
166
|
-
message: "Route ID",
|
|
167
|
-
initialValue: "main",
|
|
168
|
-
});
|
|
169
|
-
if (isCancel(routeIdResult)) {
|
|
170
|
-
cancel("Initialization cancelled.");
|
|
171
|
-
throw new Error("Initialization cancelled.");
|
|
172
|
-
}
|
|
173
111
|
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
});
|
|
178
|
-
if (isCancel(botNameResult)) {
|
|
179
|
-
cancel("Initialization cancelled.");
|
|
180
|
-
throw new Error("Initialization cancelled.");
|
|
112
|
+
const connectorChoiceIds = (connectorChoiceResult as unknown[]).map((value) => String(value));
|
|
113
|
+
if (connectorChoiceIds.length === 0) {
|
|
114
|
+
throw new Error("At least one connector must be selected");
|
|
181
115
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
mask: "*",
|
|
186
|
-
validate: (value) => (value.trim().length > 0 ? undefined : "Token is required"),
|
|
187
|
-
});
|
|
188
|
-
if (isCancel(botTokenResult)) {
|
|
189
|
-
cancel("Initialization cancelled.");
|
|
190
|
-
throw new Error("Initialization cancelled.");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const allowAllMessagesResult = await confirm({
|
|
194
|
-
message: "Allow all group messages (not mention-only)?",
|
|
195
|
-
initialValue: false,
|
|
196
|
-
});
|
|
197
|
-
if (isCancel(allowAllMessagesResult)) {
|
|
198
|
-
cancel("Initialization cancelled.");
|
|
199
|
-
throw new Error("Initialization cancelled.");
|
|
116
|
+
if (!connectorChoiceIds.every((connectorChoiceId) => isInitConnectorChoiceId(connectorChoiceId))) {
|
|
117
|
+
const invalidChoice = connectorChoiceIds.find((connectorChoiceId) => !isInitConnectorChoiceId(connectorChoiceId));
|
|
118
|
+
throw new Error(`Unsupported connector choice '${invalidChoice}'`);
|
|
200
119
|
}
|
|
201
120
|
|
|
202
121
|
return {
|
|
203
122
|
providerChoiceIds: providerChoiceIds as InitProviderChoiceId[],
|
|
204
123
|
routeProviderChoiceId,
|
|
205
|
-
|
|
206
|
-
projectRoot,
|
|
207
|
-
channelId,
|
|
208
|
-
routeId: String(routeIdResult ?? "").trim() || "main",
|
|
209
|
-
botName: String(botNameResult ?? "").trim() || DEFAULT_DISCORD_BOT_NAME,
|
|
210
|
-
botToken: String(botTokenResult ?? "").trim(),
|
|
211
|
-
allowAllMessages: allowAllMessagesResult === true,
|
|
124
|
+
connectorChoiceIds: connectorChoiceIds as InitConnectorChoiceId[],
|
|
212
125
|
};
|
|
213
126
|
}
|
|
214
127
|
|
|
215
128
|
/**
|
|
216
|
-
* Executes first-time initialization
|
|
129
|
+
* Executes first-time initialization by installing starter extensions and writing template config.
|
|
217
130
|
*/
|
|
218
131
|
export async function runInitCommand(): Promise<void> {
|
|
219
132
|
const configPath = resolveConfigPath();
|
|
220
133
|
const existingConfig = await readRawConfig(configPath);
|
|
221
134
|
if (existingConfig) {
|
|
222
135
|
throw new Error(
|
|
223
|
-
`Config '${configPath}' already exists.
|
|
136
|
+
`Config '${configPath}' already exists. Edit the file directly to update existing values.`,
|
|
224
137
|
);
|
|
225
138
|
}
|
|
226
139
|
|
|
227
140
|
const input = await collectInitInput();
|
|
228
|
-
const selected = createInitSelectionConfig(input.providerChoiceIds, input.
|
|
229
|
-
routeId: input.routeId,
|
|
230
|
-
projectRoot: input.projectRoot,
|
|
231
|
-
allowAllMessages: input.allowAllMessages,
|
|
232
|
-
botName: input.botName,
|
|
233
|
-
botToken: input.botToken,
|
|
234
|
-
channelId: input.channelId,
|
|
141
|
+
const selected = createInitSelectionConfig(input.providerChoiceIds, input.connectorChoiceIds, {
|
|
235
142
|
routeProviderChoiceId: input.routeProviderChoiceId,
|
|
143
|
+
defaultProjectRoot: process.cwd(),
|
|
236
144
|
});
|
|
237
145
|
|
|
238
146
|
const next = ensureGatewayConfigShape({});
|
|
239
|
-
|
|
240
147
|
const rootDir = resolveDataRootDir(configPath, next);
|
|
241
148
|
const manager = new ExtensionStoreManager(createLogger(), `${rootDir}/extensions`);
|
|
149
|
+
const extensionInstallSpecs = await resolveExtensionInstallSpecs(selected.extensionPackages);
|
|
242
150
|
|
|
243
151
|
const installSpinner = spinner();
|
|
244
152
|
installSpinner.start(`Installing required extensions (${selected.extensionPackages.length} packages)`);
|
|
245
153
|
try {
|
|
246
|
-
const installedPackages = await manager.installMany(
|
|
154
|
+
const installedPackages = await manager.installMany(extensionInstallSpecs);
|
|
247
155
|
for (const installed of installedPackages) {
|
|
248
156
|
upsertAllowListPackage(next, installed.packageName, true);
|
|
249
157
|
}
|
|
@@ -253,79 +161,13 @@ export async function runInitCommand(): Promise<void> {
|
|
|
253
161
|
throw error;
|
|
254
162
|
}
|
|
255
163
|
|
|
256
|
-
const catalog = await loadContributionSchemaCatalog(configPath, next);
|
|
257
|
-
const schemaByContributionId = new Map(
|
|
258
|
-
catalog
|
|
259
|
-
.filter((item) => item.configSchema)
|
|
260
|
-
.map((item) => [item.contributionId, item.configSchema!] as const),
|
|
261
|
-
);
|
|
262
|
-
const schemaStateByContributionId = new Map(
|
|
263
|
-
catalog.map((item) => [item.contributionId, item.configSchema ? "with_schema" : "without_schema"] as const),
|
|
264
|
-
);
|
|
265
|
-
const warnedSchemaFallback = new Set<string>();
|
|
266
|
-
const noteSchemaFallback = async (contributionId: string): Promise<"without_schema" | "not_loaded"> => {
|
|
267
|
-
if (warnedSchemaFallback.has(contributionId)) {
|
|
268
|
-
const existingState = schemaStateByContributionId.get(contributionId);
|
|
269
|
-
return existingState === "without_schema" ? "without_schema" : "not_loaded";
|
|
270
|
-
}
|
|
271
|
-
warnedSchemaFallback.add(contributionId);
|
|
272
|
-
|
|
273
|
-
const state = schemaStateByContributionId.get(contributionId);
|
|
274
|
-
if (state === "without_schema") {
|
|
275
|
-
await note(
|
|
276
|
-
`Contribution '${contributionId}' is loaded but does not expose configSchema. Falling back to built-in defaults/JSON.`,
|
|
277
|
-
"Schema",
|
|
278
|
-
);
|
|
279
|
-
return "without_schema";
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
await note(
|
|
283
|
-
`No loaded schema for contribution '${contributionId}'. The extension may be disabled or not installed.`,
|
|
284
|
-
"Schema",
|
|
285
|
-
);
|
|
286
|
-
return "not_loaded";
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
const resolveFallbackConfig = async (
|
|
290
|
-
kind: "provider" | "connector",
|
|
291
|
-
instanceId: string,
|
|
292
|
-
contributionId: string,
|
|
293
|
-
fallbackConfig: Record<string, unknown>,
|
|
294
|
-
): Promise<Record<string, unknown>> => {
|
|
295
|
-
const state = await noteSchemaFallback(contributionId);
|
|
296
|
-
if (state === "not_loaded") {
|
|
297
|
-
throw new Error(
|
|
298
|
-
`Cannot initialize ${kind} '${instanceId}' because schema for contribution '${contributionId}' is not loaded. ` +
|
|
299
|
-
`Ensure the extension is installed and enabled, then retry.`,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
return fallbackConfig;
|
|
303
|
-
};
|
|
304
|
-
|
|
305
164
|
for (const provider of selected.providerInstances) {
|
|
306
|
-
|
|
307
|
-
const providerConfig = schema
|
|
308
|
-
? await promptConfigFromSchema(schema, provider.config, {
|
|
309
|
-
title: `Provider '${provider.instanceId}' (${provider.contributionId})`,
|
|
310
|
-
})
|
|
311
|
-
: await resolveFallbackConfig("provider", provider.instanceId, provider.contributionId, provider.config);
|
|
312
|
-
upsertProviderInstance(next, provider.instanceId, provider.contributionId, providerConfig);
|
|
165
|
+
upsertProviderInstance(next, provider.instanceId, provider.contributionId, provider.config);
|
|
313
166
|
}
|
|
314
167
|
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
: connectorSchema
|
|
319
|
-
? await promptConfigFromSchema(connectorSchema, selected.connectorConfig, {
|
|
320
|
-
title: `Connector '${selected.connectorInstanceId}' (${selected.connectorContributionId})`,
|
|
321
|
-
})
|
|
322
|
-
: await resolveFallbackConfig(
|
|
323
|
-
"connector",
|
|
324
|
-
selected.connectorInstanceId,
|
|
325
|
-
selected.connectorContributionId,
|
|
326
|
-
selected.connectorConfig,
|
|
327
|
-
);
|
|
328
|
-
upsertConnectorInstance(next, selected.connectorInstanceId, selected.connectorContributionId, connectorConfig);
|
|
168
|
+
for (const connector of selected.connectorInstances) {
|
|
169
|
+
upsertConnectorInstance(next, connector.instanceId, connector.contributionId, connector.config);
|
|
170
|
+
}
|
|
329
171
|
|
|
330
172
|
next.providers = {
|
|
331
173
|
...next.providers,
|
|
@@ -334,34 +176,26 @@ export async function runInitCommand(): Promise<void> {
|
|
|
334
176
|
};
|
|
335
177
|
next.routes = {
|
|
336
178
|
...next.routes,
|
|
337
|
-
|
|
338
|
-
...next.routes.
|
|
339
|
-
|
|
179
|
+
default: {
|
|
180
|
+
...next.routes.default,
|
|
181
|
+
...selected.routeDefaults,
|
|
340
182
|
},
|
|
341
183
|
};
|
|
342
184
|
|
|
343
|
-
upsertRoute(next,
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
185
|
+
upsertRoute(next, selected.routeId, selected.routeProfile);
|
|
186
|
+
if (selected.defaultBinding) {
|
|
187
|
+
next.bindings = {
|
|
188
|
+
...next.bindings,
|
|
189
|
+
default: selected.defaultBinding,
|
|
190
|
+
items: next.bindings.items,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
for (const binding of selected.bindings) {
|
|
194
|
+
upsertBinding(next, binding.id, binding.config);
|
|
195
|
+
}
|
|
348
196
|
|
|
349
197
|
const validatedConfig = await applyAndValidateContributionSchemas(configPath, next);
|
|
350
198
|
|
|
351
|
-
const createdModelsFiles: string[] = [];
|
|
352
|
-
for (const provider of selected.providerInstances) {
|
|
353
|
-
if (provider.contributionId !== "provider.pi") {
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
const resolvedProvider = validatedConfig.providers?.items?.[provider.instanceId];
|
|
358
|
-
const { type: _type, ...providerConfig } = resolvedProvider ?? {};
|
|
359
|
-
const ensured = await ensureProviderPiModelsFile(configPath, Object.keys(providerConfig).length > 0 ? providerConfig : provider.config);
|
|
360
|
-
if (ensured.created) {
|
|
361
|
-
createdModelsFiles.push(ensured.path);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
199
|
await writeConfigWithValidation(configPath, validatedConfig, {
|
|
366
200
|
validate: true,
|
|
367
201
|
createBackup: false,
|
|
@@ -370,27 +204,8 @@ export async function runInitCommand(): Promise<void> {
|
|
|
370
204
|
outro("Initialization completed.");
|
|
371
205
|
|
|
372
206
|
console.log(`Config written: ${configPath}`);
|
|
373
|
-
if (createdModelsFiles.length > 0) {
|
|
374
|
-
console.log("Generated model files:");
|
|
375
|
-
for (const path of createdModelsFiles) {
|
|
376
|
-
console.log(`- ${path}`);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
207
|
console.log("Next steps:");
|
|
380
|
-
console.log("1.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
message: "Show quick validation commands?",
|
|
384
|
-
initialValue: true,
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
if (!isCancel(showHint) && showHint) {
|
|
388
|
-
await note(
|
|
389
|
-
[
|
|
390
|
-
"dobby extension list",
|
|
391
|
-
"dobby doctor",
|
|
392
|
-
].join("\n"),
|
|
393
|
-
"Validation",
|
|
394
|
-
);
|
|
395
|
-
}
|
|
208
|
+
console.log("1. Edit gateway.json and replace all REPLACE_WITH_* / YOUR_* placeholders");
|
|
209
|
+
console.log("2. Run 'dobby doctor' to validate the edited config");
|
|
210
|
+
console.log("3. Run 'dobby start' when the placeholders are replaced");
|
|
396
211
|
}
|
|
@@ -18,7 +18,7 @@ interface DiscordConnectorView {
|
|
|
18
18
|
interface BindingView {
|
|
19
19
|
bindingId: string;
|
|
20
20
|
connectorId: string;
|
|
21
|
-
sourceType:
|
|
21
|
+
sourceType: string;
|
|
22
22
|
sourceId: string;
|
|
23
23
|
routeId: string;
|
|
24
24
|
routeExists: boolean;
|
|
@@ -35,6 +35,18 @@ interface RouteView {
|
|
|
35
35
|
bindings: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function effectiveRouteProjectRoot(
|
|
39
|
+
normalized: ReturnType<typeof ensureGatewayConfigShape>,
|
|
40
|
+
routeId: string,
|
|
41
|
+
): string | undefined {
|
|
42
|
+
const route = normalized.routes.items[routeId];
|
|
43
|
+
if (!route) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return route.projectRoot ?? normalized.routes.default.projectRoot;
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
function listDiscordConnectors(rawConfig: unknown): DiscordConnectorView[] {
|
|
39
51
|
const normalized = ensureGatewayConfigShape(rawConfig as RawGatewayConfig);
|
|
40
52
|
const items: DiscordConnectorView[] = [];
|
|
@@ -82,12 +94,11 @@ function getDiscordConnectorOrThrow(
|
|
|
82
94
|
|
|
83
95
|
function listBindings(rawConfig: unknown, connectorFilter?: string): BindingView[] {
|
|
84
96
|
const normalized = ensureGatewayConfigShape(rawConfig as RawGatewayConfig);
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
return Object.entries(normalized.bindings.items)
|
|
97
|
+
const bindings: BindingView[] = Object.entries(normalized.bindings.items)
|
|
88
98
|
.filter(([, binding]) => !connectorFilter || binding.connector === connectorFilter)
|
|
89
99
|
.map(([bindingId, binding]) => {
|
|
90
|
-
const route = routes[binding.route];
|
|
100
|
+
const route = normalized.routes.items[binding.route];
|
|
101
|
+
const projectRoot = route ? effectiveRouteProjectRoot(normalized, binding.route) : undefined;
|
|
91
102
|
return {
|
|
92
103
|
bindingId,
|
|
93
104
|
connectorId: binding.connector,
|
|
@@ -95,15 +106,30 @@ function listBindings(rawConfig: unknown, connectorFilter?: string): BindingView
|
|
|
95
106
|
sourceId: binding.source.id,
|
|
96
107
|
routeId: binding.route,
|
|
97
108
|
routeExists: Boolean(route),
|
|
98
|
-
...(
|
|
109
|
+
...(projectRoot ? { projectRoot } : {}),
|
|
99
110
|
};
|
|
100
|
-
})
|
|
101
|
-
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!connectorFilter && normalized.bindings.default) {
|
|
114
|
+
const projectRoot = effectiveRouteProjectRoot(normalized, normalized.bindings.default.route);
|
|
115
|
+
bindings.push({
|
|
116
|
+
bindingId: "bindings.default",
|
|
117
|
+
connectorId: "*",
|
|
118
|
+
sourceType: "direct_message",
|
|
119
|
+
sourceId: "*",
|
|
120
|
+
routeId: normalized.bindings.default.route,
|
|
121
|
+
routeExists: Boolean(normalized.routes.items[normalized.bindings.default.route]),
|
|
122
|
+
...(projectRoot ? { projectRoot } : {}),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return bindings.sort((a, b) => a.bindingId.localeCompare(b.bindingId));
|
|
102
127
|
}
|
|
103
128
|
|
|
104
129
|
function buildRouteBindingCounts(rawConfig: unknown): Map<string, number> {
|
|
130
|
+
const normalized = ensureGatewayConfigShape(rawConfig as RawGatewayConfig);
|
|
105
131
|
const counts = new Map<string, number>();
|
|
106
|
-
for (const binding of listBindings(
|
|
132
|
+
for (const binding of listBindings(normalized)) {
|
|
107
133
|
counts.set(binding.routeId, (counts.get(binding.routeId) ?? 0) + 1);
|
|
108
134
|
}
|
|
109
135
|
return counts;
|
|
@@ -116,7 +142,7 @@ function listRoutes(rawConfig: unknown): RouteView[] {
|
|
|
116
142
|
return Object.entries(normalized.routes.items)
|
|
117
143
|
.map(([routeId, route]): RouteView => ({
|
|
118
144
|
routeId,
|
|
119
|
-
projectRoot:
|
|
145
|
+
projectRoot: effectiveRouteProjectRoot(normalized, routeId) ?? "(unset)",
|
|
120
146
|
tools: route.tools === "readonly" ? "readonly" : "full",
|
|
121
147
|
mentions: route.mentions === "optional" ? "optional" : "required",
|
|
122
148
|
...(route.provider ? { provider: route.provider } : {}),
|
|
@@ -319,7 +345,7 @@ export async function runRouteSetCommand(options: {
|
|
|
319
345
|
const existing = normalized.routes.items[options.routeId];
|
|
320
346
|
|
|
321
347
|
const projectRoot = options.projectRoot?.trim() || existing?.projectRoot;
|
|
322
|
-
if (!projectRoot) {
|
|
348
|
+
if (!projectRoot && !normalized.routes.default.projectRoot) {
|
|
323
349
|
throw new Error("--project-root is required when creating a new route");
|
|
324
350
|
}
|
|
325
351
|
|
|
@@ -339,7 +365,7 @@ export async function runRouteSetCommand(options: {
|
|
|
339
365
|
}
|
|
340
366
|
|
|
341
367
|
upsertRoute(normalized, options.routeId, {
|
|
342
|
-
projectRoot,
|
|
368
|
+
...(projectRoot ? { projectRoot } : {}),
|
|
343
369
|
...(toolsRaw ? { tools: toolsRaw } : {}),
|
|
344
370
|
...((options.mentions ?? existing?.mentions) ? { mentions: (options.mentions ?? existing?.mentions)! } : {}),
|
|
345
371
|
...(provider ? { provider } : {}),
|
|
@@ -363,18 +389,24 @@ export async function runRouteRemoveCommand(options: {
|
|
|
363
389
|
throw new Error(`Route '${options.routeId}' not found`);
|
|
364
390
|
}
|
|
365
391
|
|
|
366
|
-
const bindingRefs = listBindings(normalized).filter(
|
|
367
|
-
|
|
392
|
+
const bindingRefs = listBindings(normalized).filter(
|
|
393
|
+
(binding) => binding.routeId === options.routeId && binding.bindingId !== "bindings.default",
|
|
394
|
+
);
|
|
395
|
+
const hasDefaultBindingRef = normalized.bindings.default?.route === options.routeId;
|
|
396
|
+
if ((bindingRefs.length > 0 || hasDefaultBindingRef) && !options.cascadeBindings) {
|
|
368
397
|
const refList = bindingRefs.map((binding) => binding.bindingId).join(", ");
|
|
369
398
|
throw new Error(
|
|
370
|
-
`Route '${options.routeId}' is referenced by bindings (${refList}). Re-run with --cascade-bindings to remove these bindings automatically.`,
|
|
399
|
+
`Route '${options.routeId}' is referenced by bindings (${[refList, hasDefaultBindingRef ? "bindings.default" : ""].filter(Boolean).join(", ")}). Re-run with --cascade-bindings to remove these bindings automatically.`,
|
|
371
400
|
);
|
|
372
401
|
}
|
|
373
402
|
|
|
374
|
-
if (
|
|
403
|
+
if (options.cascadeBindings) {
|
|
375
404
|
for (const binding of bindingRefs) {
|
|
376
405
|
delete normalized.bindings.items[binding.bindingId];
|
|
377
406
|
}
|
|
407
|
+
if (hasDefaultBindingRef) {
|
|
408
|
+
delete normalized.bindings.default;
|
|
409
|
+
}
|
|
378
410
|
}
|
|
379
411
|
|
|
380
412
|
delete normalized.routes.items[options.routeId];
|