@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
|
@@ -42,6 +42,7 @@ function asRouteDefaults(value) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
return {
|
|
45
|
+
...(typeof value.projectRoot === "string" && value.projectRoot.trim().length > 0 ? { projectRoot: value.projectRoot } : {}),
|
|
45
46
|
...(typeof value.provider === "string" && value.provider.trim().length > 0 ? { provider: value.provider } : {}),
|
|
46
47
|
...(typeof value.sandbox === "string" && value.sandbox.trim().length > 0 ? { sandbox: value.sandbox } : {}),
|
|
47
48
|
tools: value.tools === "readonly" ? "readonly" : "full",
|
|
@@ -54,12 +55,12 @@ function asRoutes(value) {
|
|
|
54
55
|
}
|
|
55
56
|
const normalized = {};
|
|
56
57
|
for (const [routeId, route] of Object.entries(value)) {
|
|
57
|
-
if (!isRecord(route)
|
|
58
|
+
if (!isRecord(route)) {
|
|
58
59
|
continue;
|
|
59
60
|
}
|
|
60
61
|
normalized[routeId] = {
|
|
61
62
|
...route,
|
|
62
|
-
projectRoot: route.projectRoot,
|
|
63
|
+
...(typeof route.projectRoot === "string" && route.projectRoot.trim().length > 0 ? { projectRoot: route.projectRoot } : {}),
|
|
63
64
|
...(route.tools === "readonly" ? { tools: "readonly" } : {}),
|
|
64
65
|
...(route.mentions === "optional" ? { mentions: "optional" } : {}),
|
|
65
66
|
...(typeof route.provider === "string" && route.provider.trim().length > 0 ? { provider: route.provider } : {}),
|
|
@@ -69,6 +70,15 @@ function asRoutes(value) {
|
|
|
69
70
|
}
|
|
70
71
|
return normalized;
|
|
71
72
|
}
|
|
73
|
+
function asDefaultBinding(value) {
|
|
74
|
+
if (!isRecord(value) || typeof value.route !== "string" || value.route.trim().length === 0) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
...value,
|
|
79
|
+
route: value.route,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
72
82
|
function asBindings(value) {
|
|
73
83
|
if (!isRecord(value)) {
|
|
74
84
|
return {};
|
|
@@ -109,13 +119,14 @@ export function ensureGatewayConfigShape(config) {
|
|
|
109
119
|
const normalizedSandboxesDefault = typeof config.sandboxes?.default === "string" && config.sandboxes.default.trim().length > 0
|
|
110
120
|
? config.sandboxes.default
|
|
111
121
|
: "host.builtin";
|
|
112
|
-
const routeDefaults = asRouteDefaults(config.routes?.
|
|
122
|
+
const routeDefaults = asRouteDefaults(config.routes?.default);
|
|
113
123
|
if (!routeDefaults.provider && normalizedProvidersDefault) {
|
|
114
124
|
routeDefaults.provider = normalizedProvidersDefault;
|
|
115
125
|
}
|
|
116
126
|
if (!routeDefaults.sandbox && normalizedSandboxesDefault) {
|
|
117
127
|
routeDefaults.sandbox = normalizedSandboxesDefault;
|
|
118
128
|
}
|
|
129
|
+
const defaultBinding = asDefaultBinding(config.bindings?.default);
|
|
119
130
|
return {
|
|
120
131
|
...config,
|
|
121
132
|
extensions: {
|
|
@@ -138,11 +149,12 @@ export function ensureGatewayConfigShape(config) {
|
|
|
138
149
|
},
|
|
139
150
|
routes: {
|
|
140
151
|
...(isRecord(config.routes) ? config.routes : {}),
|
|
141
|
-
|
|
152
|
+
default: routeDefaults,
|
|
142
153
|
items: asRoutes(config.routes?.items),
|
|
143
154
|
},
|
|
144
155
|
bindings: {
|
|
145
156
|
...(isRecord(config.bindings) ? config.bindings : {}),
|
|
157
|
+
...(defaultBinding ? { default: defaultBinding } : {}),
|
|
146
158
|
items: asBindings(config.bindings?.items),
|
|
147
159
|
},
|
|
148
160
|
data: {
|
|
@@ -283,11 +295,11 @@ export function setDefaultProviderIfMissingOrInvalid(config) {
|
|
|
283
295
|
const defaultProvider = next.providers.default;
|
|
284
296
|
if (defaultProvider && items[defaultProvider]) {
|
|
285
297
|
config.providers = next.providers;
|
|
286
|
-
if (!next.routes.
|
|
298
|
+
if (!next.routes.default.provider) {
|
|
287
299
|
config.routes = {
|
|
288
300
|
...next.routes,
|
|
289
301
|
defaults: {
|
|
290
|
-
...next.routes.
|
|
302
|
+
...next.routes.default,
|
|
291
303
|
provider: defaultProvider,
|
|
292
304
|
},
|
|
293
305
|
};
|
|
@@ -306,8 +318,8 @@ export function setDefaultProviderIfMissingOrInvalid(config) {
|
|
|
306
318
|
};
|
|
307
319
|
config.routes = {
|
|
308
320
|
...next.routes,
|
|
309
|
-
|
|
310
|
-
...next.routes.
|
|
321
|
+
default: {
|
|
322
|
+
...next.routes.default,
|
|
311
323
|
provider: candidates[0],
|
|
312
324
|
},
|
|
313
325
|
};
|
|
@@ -315,7 +327,7 @@ export function setDefaultProviderIfMissingOrInvalid(config) {
|
|
|
315
327
|
export function upsertRoute(config, routeId, profile) {
|
|
316
328
|
const next = ensureGatewayConfigShape(config);
|
|
317
329
|
next.routes.items[routeId] = {
|
|
318
|
-
projectRoot: profile.projectRoot,
|
|
330
|
+
...(typeof profile.projectRoot === "string" && profile.projectRoot.trim().length > 0 ? { projectRoot: profile.projectRoot } : {}),
|
|
319
331
|
...(profile.tools ? { tools: profile.tools } : {}),
|
|
320
332
|
...(profile.mentions ? { mentions: profile.mentions } : {}),
|
|
321
333
|
...(profile.provider ? { provider: profile.provider } : {}),
|
|
@@ -335,6 +347,18 @@ export function upsertBinding(config, bindingId, binding) {
|
|
|
335
347
|
items: next.bindings.items,
|
|
336
348
|
};
|
|
337
349
|
}
|
|
350
|
+
export function setDefaultBinding(config, binding) {
|
|
351
|
+
const next = ensureGatewayConfigShape(config);
|
|
352
|
+
const normalizedBinding = binding ? structuredClone(binding) : undefined;
|
|
353
|
+
config.bindings = {
|
|
354
|
+
...next.bindings,
|
|
355
|
+
...(normalizedBinding ? { default: normalizedBinding } : {}),
|
|
356
|
+
items: next.bindings.items,
|
|
357
|
+
};
|
|
358
|
+
if (!normalizedBinding) {
|
|
359
|
+
delete config.bindings.default;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
338
362
|
export function listContributionIds(config) {
|
|
339
363
|
const next = ensureGatewayConfigShape(config);
|
|
340
364
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { cancel, confirm, isCancel, note, password, select, text, } from "@clack/prompts";
|
|
2
2
|
import JSON5 from "json5";
|
|
3
|
-
import { ensureGatewayConfigShape, setDefaultProviderIfMissingOrInvalid, upsertBinding, upsertConnectorInstance, upsertProviderInstance, upsertRoute, } from "./config-mutators.js";
|
|
3
|
+
import { ensureGatewayConfigShape, setDefaultBinding, setDefaultProviderIfMissingOrInvalid, upsertBinding, upsertConnectorInstance, upsertProviderInstance, upsertRoute, } from "./config-mutators.js";
|
|
4
4
|
import { DEFAULT_DISCORD_BOT_NAME, DISCORD_CONNECTOR_CONTRIBUTION_ID, } from "./discord-config.js";
|
|
5
5
|
import { promptConfigFromSchema } from "./schema-prompts.js";
|
|
6
6
|
export const CONFIGURE_SECTION_VALUES = ["provider", "connector", "route", "binding", "sandbox", "data"];
|
|
@@ -147,7 +147,7 @@ async function configureProviderSection(config, context) {
|
|
|
147
147
|
throw new Error("Configure cancelled.");
|
|
148
148
|
}
|
|
149
149
|
next.providers.default = String(defaultProvider);
|
|
150
|
-
next.routes.
|
|
150
|
+
next.routes.default.provider = String(defaultProvider);
|
|
151
151
|
}
|
|
152
152
|
Object.assign(config, next);
|
|
153
153
|
}
|
|
@@ -229,11 +229,32 @@ async function configureRouteSection(config) {
|
|
|
229
229
|
}
|
|
230
230
|
const routeId = String(targetRoute) === "__new" ? await requiredText("New route ID", "main") : String(targetRoute);
|
|
231
231
|
const existing = routeItems[routeId];
|
|
232
|
-
const
|
|
232
|
+
const defaultProjectRoot = next.routes.default.projectRoot;
|
|
233
|
+
let projectRoot = existing?.projectRoot;
|
|
234
|
+
if (defaultProjectRoot) {
|
|
235
|
+
const projectRootMode = await select({
|
|
236
|
+
message: "projectRoot",
|
|
237
|
+
options: [
|
|
238
|
+
{ value: "__default", label: `Use route default (${defaultProjectRoot})` },
|
|
239
|
+
{ value: "__custom", label: "Set explicit projectRoot" },
|
|
240
|
+
],
|
|
241
|
+
initialValue: existing?.projectRoot ? "__custom" : "__default",
|
|
242
|
+
});
|
|
243
|
+
if (isCancel(projectRootMode)) {
|
|
244
|
+
cancel("Configure cancelled.");
|
|
245
|
+
throw new Error("Configure cancelled.");
|
|
246
|
+
}
|
|
247
|
+
projectRoot = projectRootMode === "__custom"
|
|
248
|
+
? await requiredText("projectRoot", existing?.projectRoot ?? defaultProjectRoot)
|
|
249
|
+
: undefined;
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
projectRoot = await requiredText("projectRoot", existing?.projectRoot ?? process.cwd());
|
|
253
|
+
}
|
|
233
254
|
const tools = await select({
|
|
234
255
|
message: "tools",
|
|
235
256
|
options: [
|
|
236
|
-
{ value: "__default", label: `Use route default (${next.routes.
|
|
257
|
+
{ value: "__default", label: `Use route default (${next.routes.default.tools ?? "full"})` },
|
|
237
258
|
{ value: "full", label: "full" },
|
|
238
259
|
{ value: "readonly", label: "readonly" },
|
|
239
260
|
],
|
|
@@ -246,7 +267,7 @@ async function configureRouteSection(config) {
|
|
|
246
267
|
const mentions = await select({
|
|
247
268
|
message: "mentions",
|
|
248
269
|
options: [
|
|
249
|
-
{ value: "__default", label: `Use route default (${next.routes.
|
|
270
|
+
{ value: "__default", label: `Use route default (${next.routes.default.mentions ?? "required"})` },
|
|
250
271
|
{ value: "required", label: "required" },
|
|
251
272
|
{ value: "optional", label: "optional" },
|
|
252
273
|
],
|
|
@@ -261,7 +282,7 @@ async function configureRouteSection(config) {
|
|
|
261
282
|
? await select({
|
|
262
283
|
message: "provider",
|
|
263
284
|
options: [
|
|
264
|
-
{ value: "__default", label: `Use route default (${(next.routes.
|
|
285
|
+
{ value: "__default", label: `Use route default (${(next.routes.default.provider ?? next.providers.default) || "(unset)"})` },
|
|
265
286
|
...providerIds.map((id) => ({ value: id, label: id })),
|
|
266
287
|
],
|
|
267
288
|
initialValue: existing?.provider ?? "__default",
|
|
@@ -275,7 +296,7 @@ async function configureRouteSection(config) {
|
|
|
275
296
|
const sandboxValue = await select({
|
|
276
297
|
message: "sandbox",
|
|
277
298
|
options: [
|
|
278
|
-
{ value: "__default", label: `Use route default (${next.routes.
|
|
299
|
+
{ value: "__default", label: `Use route default (${next.routes.default.sandbox ?? next.sandboxes.default})` },
|
|
279
300
|
...sandboxIds.map((id) => ({ value: id, label: id })),
|
|
280
301
|
],
|
|
281
302
|
initialValue: existing?.sandbox ?? "__default",
|
|
@@ -286,7 +307,7 @@ async function configureRouteSection(config) {
|
|
|
286
307
|
}
|
|
287
308
|
const systemPromptFile = await optionalText("systemPromptFile (optional)", existing?.systemPromptFile ?? "");
|
|
288
309
|
upsertRoute(next, routeId, {
|
|
289
|
-
projectRoot,
|
|
310
|
+
...(projectRoot ? { projectRoot } : {}),
|
|
290
311
|
...(tools !== "__default" ? { tools: String(tools) } : {}),
|
|
291
312
|
...(mentions !== "__default" ? { mentions: String(mentions) } : {}),
|
|
292
313
|
...(providerValue !== "__default" ? { provider: String(providerValue) } : {}),
|
|
@@ -306,19 +327,39 @@ async function configureBindingSection(config) {
|
|
|
306
327
|
throw new Error("No routes found. Configure routes first.");
|
|
307
328
|
}
|
|
308
329
|
const targetBinding = bindingChoices.length === 0
|
|
309
|
-
? "__new"
|
|
330
|
+
? (next.bindings.default ? "__default" : "__new")
|
|
310
331
|
: await select({
|
|
311
332
|
message: "Select binding",
|
|
312
333
|
options: [
|
|
334
|
+
{ value: "__default", label: next.bindings.default ? "Edit default direct-message binding" : "Create default direct-message binding" },
|
|
313
335
|
...bindingChoices.map((id) => ({ value: id, label: id })),
|
|
314
336
|
{ value: "__new", label: "Create new binding" },
|
|
315
337
|
],
|
|
316
|
-
initialValue: bindingChoices[0],
|
|
338
|
+
initialValue: next.bindings.default ? "__default" : bindingChoices[0],
|
|
317
339
|
});
|
|
318
340
|
if (isCancel(targetBinding)) {
|
|
319
341
|
cancel("Configure cancelled.");
|
|
320
342
|
throw new Error("Configure cancelled.");
|
|
321
343
|
}
|
|
344
|
+
const routeIds = Object.keys(next.routes.items).sort((a, b) => a.localeCompare(b));
|
|
345
|
+
if (String(targetBinding) === "__default") {
|
|
346
|
+
const defaultRouteId = await select({
|
|
347
|
+
message: "Default direct-message route",
|
|
348
|
+
options: routeIds.map((id) => ({ value: id, label: id })),
|
|
349
|
+
initialValue: next.bindings.default?.route && routeIds.includes(next.bindings.default.route)
|
|
350
|
+
? next.bindings.default.route
|
|
351
|
+
: routeIds[0],
|
|
352
|
+
});
|
|
353
|
+
if (isCancel(defaultRouteId)) {
|
|
354
|
+
cancel("Configure cancelled.");
|
|
355
|
+
throw new Error("Configure cancelled.");
|
|
356
|
+
}
|
|
357
|
+
setDefaultBinding(next, {
|
|
358
|
+
route: String(defaultRouteId),
|
|
359
|
+
});
|
|
360
|
+
Object.assign(config, next);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
322
363
|
const bindingId = String(targetBinding) === "__new"
|
|
323
364
|
? await requiredText("New binding ID", "discord.main.main")
|
|
324
365
|
: String(targetBinding);
|
|
@@ -346,7 +387,6 @@ async function configureBindingSection(config) {
|
|
|
346
387
|
throw new Error("Configure cancelled.");
|
|
347
388
|
}
|
|
348
389
|
const sourceId = await requiredText("source.id", existing?.source.id);
|
|
349
|
-
const routeIds = Object.keys(next.routes.items).sort((a, b) => a.localeCompare(b));
|
|
350
390
|
const routeId = await select({
|
|
351
391
|
message: "route",
|
|
352
392
|
options: routeIds.map((id) => ({ value: id, label: id })),
|
|
@@ -386,7 +426,7 @@ async function configureSandboxSection(config) {
|
|
|
386
426
|
throw new Error("Configure cancelled.");
|
|
387
427
|
}
|
|
388
428
|
next.sandboxes.default = String(defaultSandbox);
|
|
389
|
-
next.routes.
|
|
429
|
+
next.routes.default.sandbox = String(defaultSandbox);
|
|
390
430
|
Object.assign(config, next);
|
|
391
431
|
}
|
|
392
432
|
async function configureDataSection(config) {
|
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
import { DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID, DISCORD_CONNECTOR_CONTRIBUTION_ID, } from "./discord-config.js";
|
|
1
|
+
import { DEFAULT_DISCORD_BOT_NAME, DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID, DISCORD_CONNECTOR_CONTRIBUTION_ID, } from "./discord-config.js";
|
|
2
|
+
export const DEFAULT_INIT_ROUTE_ID = "main";
|
|
3
|
+
export const DEFAULT_INIT_PROJECT_ROOT = "./REPLACE_WITH_PROJECT_ROOT";
|
|
2
4
|
const PROVIDER_CATALOG = {
|
|
3
5
|
"provider.pi": {
|
|
4
6
|
id: "provider.pi",
|
|
5
7
|
label: "Pi provider",
|
|
6
|
-
|
|
8
|
+
package: "@dobby.ai/provider-pi",
|
|
7
9
|
instanceId: "pi.main",
|
|
8
10
|
contributionId: "provider.pi",
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
modelsFile: "./models.custom.json",
|
|
11
|
+
defaultConfig: {
|
|
12
|
+
model: "REPLACE_WITH_PROVIDER_MODEL_ID",
|
|
13
|
+
baseUrl: "REPLACE_WITH_PROVIDER_BASE_URL",
|
|
14
|
+
apiKey: "REPLACE_WITH_PROVIDER_API_KEY_OR_ENV",
|
|
14
15
|
},
|
|
15
16
|
},
|
|
16
17
|
"provider.claude-cli": {
|
|
17
18
|
id: "provider.claude-cli",
|
|
18
19
|
label: "Claude CLI provider",
|
|
19
|
-
|
|
20
|
+
package: "@dobby.ai/provider-claude-cli",
|
|
20
21
|
instanceId: "claude-cli.main",
|
|
21
22
|
contributionId: "provider.claude-cli",
|
|
22
|
-
|
|
23
|
+
defaultConfig: {
|
|
23
24
|
model: "claude-sonnet-4-5",
|
|
24
25
|
maxTurns: 20,
|
|
25
26
|
command: "claude",
|
|
@@ -34,11 +35,52 @@ const CONNECTOR_CATALOG = {
|
|
|
34
35
|
"connector.discord": {
|
|
35
36
|
id: "connector.discord",
|
|
36
37
|
label: "Discord connector",
|
|
37
|
-
|
|
38
|
+
package: "@dobby.ai/connector-discord",
|
|
38
39
|
instanceId: DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID,
|
|
39
40
|
contributionId: DISCORD_CONNECTOR_CONTRIBUTION_ID,
|
|
41
|
+
defaultConfig: {
|
|
42
|
+
botName: DEFAULT_DISCORD_BOT_NAME,
|
|
43
|
+
botToken: "REPLACE_WITH_DISCORD_BOT_TOKEN",
|
|
44
|
+
reconnectStaleMs: 60_000,
|
|
45
|
+
reconnectCheckIntervalMs: 10_000,
|
|
46
|
+
},
|
|
47
|
+
bindingTemplate: {
|
|
48
|
+
sourceType: "channel",
|
|
49
|
+
sourceId: "YOUR_DISCORD_CHANNEL_ID",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
"connector.feishu": {
|
|
53
|
+
id: "connector.feishu",
|
|
54
|
+
label: "Feishu connector",
|
|
55
|
+
package: "@dobby.ai/connector-feishu",
|
|
56
|
+
instanceId: "feishu.main",
|
|
57
|
+
contributionId: "connector.feishu",
|
|
58
|
+
defaultConfig: {
|
|
59
|
+
appId: "REPLACE_WITH_FEISHU_APP_ID",
|
|
60
|
+
appSecret: "REPLACE_WITH_FEISHU_APP_SECRET",
|
|
61
|
+
domain: "feishu",
|
|
62
|
+
messageFormat: "card_markdown",
|
|
63
|
+
replyMode: "direct",
|
|
64
|
+
downloadAttachments: true,
|
|
65
|
+
},
|
|
66
|
+
bindingTemplate: {
|
|
67
|
+
sourceType: "chat",
|
|
68
|
+
sourceId: "YOUR_FEISHU_CHAT_ID",
|
|
69
|
+
},
|
|
40
70
|
},
|
|
41
71
|
};
|
|
72
|
+
function dedupeChoiceIds(choiceIds) {
|
|
73
|
+
const dedupedChoiceIds = [];
|
|
74
|
+
const seenChoiceIds = new Set();
|
|
75
|
+
for (const choiceId of choiceIds) {
|
|
76
|
+
if (seenChoiceIds.has(choiceId)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
seenChoiceIds.add(choiceId);
|
|
80
|
+
dedupedChoiceIds.push(choiceId);
|
|
81
|
+
}
|
|
82
|
+
return dedupedChoiceIds;
|
|
83
|
+
}
|
|
42
84
|
export function listInitProviderChoices() {
|
|
43
85
|
return Object.values(PROVIDER_CATALOG);
|
|
44
86
|
}
|
|
@@ -51,65 +93,66 @@ export function isInitProviderChoiceId(value) {
|
|
|
51
93
|
export function isInitConnectorChoiceId(value) {
|
|
52
94
|
return Object.prototype.hasOwnProperty.call(CONNECTOR_CATALOG, value);
|
|
53
95
|
}
|
|
54
|
-
export function createInitSelectionConfig(providerChoiceIds,
|
|
55
|
-
const dedupedProviderChoiceIds =
|
|
56
|
-
const seenProviderChoiceIds = new Set();
|
|
57
|
-
for (const providerChoiceId of providerChoiceIds) {
|
|
58
|
-
if (!seenProviderChoiceIds.has(providerChoiceId)) {
|
|
59
|
-
seenProviderChoiceIds.add(providerChoiceId);
|
|
60
|
-
dedupedProviderChoiceIds.push(providerChoiceId);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
96
|
+
export function createInitSelectionConfig(providerChoiceIds, connectorChoiceIds, context) {
|
|
97
|
+
const dedupedProviderChoiceIds = dedupeChoiceIds(providerChoiceIds);
|
|
63
98
|
if (dedupedProviderChoiceIds.length === 0) {
|
|
64
99
|
throw new Error("At least one provider choice is required");
|
|
65
100
|
}
|
|
101
|
+
const dedupedConnectorChoiceIds = dedupeChoiceIds(connectorChoiceIds);
|
|
102
|
+
if (dedupedConnectorChoiceIds.length === 0) {
|
|
103
|
+
throw new Error("At least one connector choice is required");
|
|
104
|
+
}
|
|
66
105
|
if (!dedupedProviderChoiceIds.includes(context.routeProviderChoiceId)) {
|
|
67
106
|
throw new Error(`route provider choice '${context.routeProviderChoiceId}' must be one of selected providers: ${dedupedProviderChoiceIds.join(", ")}`);
|
|
68
107
|
}
|
|
69
108
|
const providerChoices = dedupedProviderChoiceIds.map((providerChoiceId) => PROVIDER_CATALOG[providerChoiceId]);
|
|
109
|
+
const connectorChoices = dedupedConnectorChoiceIds.map((connectorChoiceId) => CONNECTOR_CATALOG[connectorChoiceId]);
|
|
70
110
|
const primaryProviderChoice = PROVIDER_CATALOG[context.routeProviderChoiceId];
|
|
71
|
-
const connectorChoice = CONNECTOR_CATALOG[connectorChoiceId];
|
|
72
111
|
return {
|
|
73
112
|
providerChoiceIds: dedupedProviderChoiceIds,
|
|
74
113
|
routeProviderChoiceId: primaryProviderChoice.id,
|
|
75
|
-
|
|
76
|
-
connectorChoiceId,
|
|
114
|
+
connectorChoiceIds: dedupedConnectorChoiceIds,
|
|
77
115
|
extensionPackages: [
|
|
78
|
-
...new Set([
|
|
116
|
+
...new Set([
|
|
117
|
+
...providerChoices.map((item) => item.package),
|
|
118
|
+
...connectorChoices.map((item) => item.package),
|
|
119
|
+
]),
|
|
79
120
|
],
|
|
80
121
|
providerInstances: providerChoices.map((providerChoice) => ({
|
|
81
122
|
choiceId: providerChoice.id,
|
|
82
123
|
instanceId: providerChoice.instanceId,
|
|
83
124
|
contributionId: providerChoice.contributionId,
|
|
84
|
-
config: structuredClone(providerChoice.
|
|
125
|
+
config: structuredClone(providerChoice.defaultConfig),
|
|
126
|
+
})),
|
|
127
|
+
connectorInstances: connectorChoices.map((connectorChoice) => ({
|
|
128
|
+
choiceId: connectorChoice.id,
|
|
129
|
+
instanceId: connectorChoice.instanceId,
|
|
130
|
+
contributionId: connectorChoice.contributionId,
|
|
131
|
+
config: structuredClone(connectorChoice.defaultConfig),
|
|
85
132
|
})),
|
|
86
133
|
providerInstanceId: primaryProviderChoice.instanceId,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
connectorContributionId: connectorChoice.contributionId,
|
|
91
|
-
connectorConfig: {
|
|
92
|
-
botName: context.botName,
|
|
93
|
-
botToken: context.botToken,
|
|
94
|
-
reconnectStaleMs: 60_000,
|
|
95
|
-
reconnectCheckIntervalMs: 10_000,
|
|
96
|
-
},
|
|
97
|
-
routeProfile: {
|
|
98
|
-
projectRoot: context.projectRoot,
|
|
134
|
+
routeId: DEFAULT_INIT_ROUTE_ID,
|
|
135
|
+
routeDefaults: {
|
|
136
|
+
projectRoot: context.defaultProjectRoot ?? DEFAULT_INIT_PROJECT_ROOT,
|
|
99
137
|
tools: "full",
|
|
100
|
-
|
|
101
|
-
mentions: context.allowAllMessages ? "optional" : "required",
|
|
138
|
+
mentions: "required",
|
|
102
139
|
provider: primaryProviderChoice.instanceId,
|
|
103
140
|
sandbox: "host.builtin",
|
|
104
141
|
},
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
source: {
|
|
109
|
-
type: "channel",
|
|
110
|
-
id: context.channelId,
|
|
111
|
-
},
|
|
112
|
-
route: context.routeId,
|
|
142
|
+
routeProfile: {},
|
|
143
|
+
defaultBinding: {
|
|
144
|
+
route: DEFAULT_INIT_ROUTE_ID,
|
|
113
145
|
},
|
|
146
|
+
bindings: connectorChoices.map((connectorChoice) => ({
|
|
147
|
+
id: `${connectorChoice.instanceId}.${DEFAULT_INIT_ROUTE_ID}`,
|
|
148
|
+
config: {
|
|
149
|
+
connector: connectorChoice.instanceId,
|
|
150
|
+
source: {
|
|
151
|
+
type: connectorChoice.bindingTemplate.sourceType,
|
|
152
|
+
id: connectorChoice.bindingTemplate.sourceId,
|
|
153
|
+
},
|
|
154
|
+
route: DEFAULT_INIT_ROUTE_ID,
|
|
155
|
+
},
|
|
156
|
+
})),
|
|
114
157
|
};
|
|
115
158
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { findDobbyRepoRoot } from "../../shared/dobby-repo.js";
|
|
5
|
+
function isExplicitInstallSpec(value) {
|
|
6
|
+
return value.startsWith("file:")
|
|
7
|
+
|| value.startsWith("git+")
|
|
8
|
+
|| value.startsWith("http://")
|
|
9
|
+
|| value.startsWith("https://")
|
|
10
|
+
|| value.startsWith("./")
|
|
11
|
+
|| value.startsWith("../")
|
|
12
|
+
|| value.startsWith("/");
|
|
13
|
+
}
|
|
14
|
+
async function listRepoLocalExtensionPackages(repoRoot) {
|
|
15
|
+
const pluginsRoot = resolve(repoRoot, "plugins");
|
|
16
|
+
const entries = await readdir(pluginsRoot, { withFileTypes: true });
|
|
17
|
+
const packages = new Map();
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (!entry.isDirectory() || entry.name === "plugin-sdk") {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const packageDir = resolve(pluginsRoot, entry.name);
|
|
23
|
+
const packageJsonPath = resolve(packageDir, "package.json");
|
|
24
|
+
const manifestPath = resolve(packageDir, "dobby.manifest.json");
|
|
25
|
+
try {
|
|
26
|
+
await access(packageJsonPath);
|
|
27
|
+
await access(manifestPath);
|
|
28
|
+
const raw = await readFile(packageJsonPath, "utf-8");
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (typeof parsed.name !== "string" || parsed.name.trim().length === 0) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
packages.set(parsed.name, {
|
|
34
|
+
packageName: parsed.name,
|
|
35
|
+
packageDir,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return packages;
|
|
43
|
+
}
|
|
44
|
+
async function assertLocalExtensionBuildReady(localPackage) {
|
|
45
|
+
const manifestPath = resolve(localPackage.packageDir, "dobby.manifest.json");
|
|
46
|
+
const rawManifest = await readFile(manifestPath, "utf-8");
|
|
47
|
+
const parsed = JSON.parse(rawManifest);
|
|
48
|
+
for (const contribution of parsed.contributions ?? []) {
|
|
49
|
+
if (typeof contribution.entry !== "string" || contribution.entry.trim().length === 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const entryPath = resolve(localPackage.packageDir, contribution.entry);
|
|
53
|
+
try {
|
|
54
|
+
await access(entryPath);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
const contributionId = typeof contribution.id === "string" ? contribution.id : "unknown";
|
|
58
|
+
throw new Error(`Local extension '${localPackage.packageName}' is not built for contribution '${contributionId}'. `
|
|
59
|
+
+ `Missing '${entryPath}'. Run 'npm run build --prefix ${localPackage.packageDir}' first.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function resolveExtensionInstallSpecs(packageSpecs, cwd = process.cwd()) {
|
|
64
|
+
const repoRoot = findDobbyRepoRoot(cwd);
|
|
65
|
+
if (!repoRoot) {
|
|
66
|
+
return packageSpecs;
|
|
67
|
+
}
|
|
68
|
+
const repoPackages = await listRepoLocalExtensionPackages(repoRoot);
|
|
69
|
+
const resolvedSpecs = [];
|
|
70
|
+
for (const rawSpec of packageSpecs) {
|
|
71
|
+
const packageSpec = rawSpec.trim();
|
|
72
|
+
if (packageSpec.length === 0 || isExplicitInstallSpec(packageSpec)) {
|
|
73
|
+
resolvedSpecs.push(packageSpec);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const localPackage = repoPackages.get(packageSpec);
|
|
77
|
+
if (!localPackage) {
|
|
78
|
+
resolvedSpecs.push(packageSpec);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
await assertLocalExtensionBuildReady(localPackage);
|
|
82
|
+
resolvedSpecs.push(`file:${localPackage.packageDir}`);
|
|
83
|
+
}
|
|
84
|
+
return resolvedSpecs;
|
|
85
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cancel, confirm, isCancel, multiselect, note, select, text, } from "@clack/prompts";
|
|
1
|
+
import { cancel, confirm, isCancel, multiselect, note, password, select, text, } from "@clack/prompts";
|
|
2
2
|
import JSON5 from "json5";
|
|
3
3
|
function isRecord(value) {
|
|
4
4
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
@@ -61,11 +61,14 @@ function shouldPromptInMinimalMode(field) {
|
|
|
61
61
|
if (!field.hasDefault && field.required) {
|
|
62
62
|
return true;
|
|
63
63
|
}
|
|
64
|
-
if (
|
|
64
|
+
if (field.existingValue !== undefined) {
|
|
65
65
|
return true;
|
|
66
66
|
}
|
|
67
67
|
return false;
|
|
68
68
|
}
|
|
69
|
+
function isSensitiveStringField(key) {
|
|
70
|
+
return /(token|secret|api[-_]?key)$/i.test(key);
|
|
71
|
+
}
|
|
69
72
|
async function promptNumberField(params) {
|
|
70
73
|
while (true) {
|
|
71
74
|
const result = await text({
|
|
@@ -232,6 +235,27 @@ async function promptFieldValue(params) {
|
|
|
232
235
|
existingValue,
|
|
233
236
|
});
|
|
234
237
|
}
|
|
238
|
+
if (isSensitiveStringField(key)) {
|
|
239
|
+
while (true) {
|
|
240
|
+
const result = await password({
|
|
241
|
+
message,
|
|
242
|
+
mask: "*",
|
|
243
|
+
});
|
|
244
|
+
if (isCancel(result)) {
|
|
245
|
+
cancel("Configuration cancelled.");
|
|
246
|
+
throw new Error("Configuration cancelled.");
|
|
247
|
+
}
|
|
248
|
+
const raw = String(result ?? "").trim();
|
|
249
|
+
if (raw.length === 0) {
|
|
250
|
+
if (required && existingValue === undefined) {
|
|
251
|
+
await note("This field is required.", "Validation");
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
return existingValue;
|
|
255
|
+
}
|
|
256
|
+
return raw;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
235
259
|
while (true) {
|
|
236
260
|
const result = await text({
|
|
237
261
|
message,
|
|
@@ -17,8 +17,8 @@ test("resolveConfigPath detects local dobby repository config path", async () =>
|
|
|
17
17
|
await mkdir(resolve(repoRoot, "config"), { recursive: true });
|
|
18
18
|
await mkdir(resolve(repoRoot, "scripts"), { recursive: true });
|
|
19
19
|
await mkdir(resolve(repoRoot, "src", "cli"), { recursive: true });
|
|
20
|
-
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "dobby" }), "utf-8");
|
|
21
|
-
await writeFile(resolve(repoRoot, "config", "gateway.json"), "{}", "utf-8");
|
|
20
|
+
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "@dobby.ai/dobby" }), "utf-8");
|
|
21
|
+
await writeFile(resolve(repoRoot, "config", "gateway.example.json"), "{}\n", "utf-8");
|
|
22
22
|
await writeFile(resolve(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
|
|
23
23
|
assert.equal(resolveConfigPath({
|
|
24
24
|
cwd: resolve(repoRoot, "src", "cli"),
|
|
@@ -29,8 +29,8 @@ test("resolveConfigPath prioritizes DOBBY_CONFIG_PATH over repository detection"
|
|
|
29
29
|
const repoRoot = await mkdtemp(resolve(tmpdir(), "dobby-config-path-env-priority-"));
|
|
30
30
|
await mkdir(resolve(repoRoot, "config"), { recursive: true });
|
|
31
31
|
await mkdir(resolve(repoRoot, "scripts"), { recursive: true });
|
|
32
|
-
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "dobby" }), "utf-8");
|
|
33
|
-
await writeFile(resolve(repoRoot, "config", "gateway.json"), "{}", "utf-8");
|
|
32
|
+
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "@dobby.ai/dobby" }), "utf-8");
|
|
33
|
+
await writeFile(resolve(repoRoot, "config", "gateway.example.json"), "{}\n", "utf-8");
|
|
34
34
|
await writeFile(resolve(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
|
|
35
35
|
const customPath = resolve(tmpdir(), "dobby-custom-gateway.json");
|
|
36
36
|
assert.equal(resolveConfigPath({
|
|
@@ -53,7 +53,7 @@ test("resolveDataRootDir uses repo root for repo-local config/gateway.json", asy
|
|
|
53
53
|
const repoRoot = await mkdtemp(resolve(tmpdir(), "dobby-data-root-repo-"));
|
|
54
54
|
await mkdir(resolve(repoRoot, "config"), { recursive: true });
|
|
55
55
|
await mkdir(resolve(repoRoot, "scripts"), { recursive: true });
|
|
56
|
-
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "dobby" }), "utf-8");
|
|
56
|
+
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "@dobby.ai/dobby" }), "utf-8");
|
|
57
57
|
await writeFile(resolve(repoRoot, "config", "gateway.json"), "{}", "utf-8");
|
|
58
58
|
await writeFile(resolve(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
|
|
59
59
|
assert.equal(resolveDataRootDir(resolve(repoRoot, "config", "gateway.json"), {
|