@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,86 @@
|
|
|
1
|
+
import { DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID, DISCORD_CONNECTOR_CONTRIBUTION_ID, } from "./discord-config.js";
|
|
2
|
+
const PRESET_IDS = ["discord-pi", "discord-claude-cli"];
|
|
3
|
+
/**
|
|
4
|
+
* Returns all preset identifiers supported by `dobby init`.
|
|
5
|
+
*/
|
|
6
|
+
export function listPresetIds() {
|
|
7
|
+
return [...PRESET_IDS];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Type guard for validating preset ids provided by users.
|
|
11
|
+
*/
|
|
12
|
+
export function isPresetId(value) {
|
|
13
|
+
return PRESET_IDS.includes(value);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Builds preset-specific extension, instance, and route defaults for init flow.
|
|
17
|
+
*/
|
|
18
|
+
export function createPresetConfig(presetId, context) {
|
|
19
|
+
const baseRoute = {
|
|
20
|
+
projectRoot: context.projectRoot,
|
|
21
|
+
tools: "full",
|
|
22
|
+
systemPromptFile: "",
|
|
23
|
+
allowMentionsOnly: !context.allowAllMessages,
|
|
24
|
+
maxConcurrentTurns: 1,
|
|
25
|
+
sandboxId: "host.builtin",
|
|
26
|
+
};
|
|
27
|
+
if (presetId === "discord-claude-cli") {
|
|
28
|
+
return {
|
|
29
|
+
id: presetId,
|
|
30
|
+
extensionPackages: ["@dobby/provider-claude-cli", "@dobby/connector-discord"],
|
|
31
|
+
providerInstanceId: "claude-cli.main",
|
|
32
|
+
providerContributionId: "provider.claude-cli",
|
|
33
|
+
providerConfig: {
|
|
34
|
+
model: "claude-sonnet-4-5",
|
|
35
|
+
maxTurns: 20,
|
|
36
|
+
command: "claude",
|
|
37
|
+
commandArgs: [],
|
|
38
|
+
authMode: "auto",
|
|
39
|
+
permissionMode: "bypassPermissions",
|
|
40
|
+
streamVerbose: true,
|
|
41
|
+
},
|
|
42
|
+
connectorInstanceId: DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID,
|
|
43
|
+
connectorContributionId: DISCORD_CONNECTOR_CONTRIBUTION_ID,
|
|
44
|
+
connectorConfig: {
|
|
45
|
+
botName: context.botName,
|
|
46
|
+
botToken: context.botToken,
|
|
47
|
+
botChannelMap: {
|
|
48
|
+
[context.channelId]: context.routeId,
|
|
49
|
+
},
|
|
50
|
+
reconnectStaleMs: 60_000,
|
|
51
|
+
reconnectCheckIntervalMs: 10_000,
|
|
52
|
+
},
|
|
53
|
+
routeProfile: {
|
|
54
|
+
...baseRoute,
|
|
55
|
+
providerId: "claude-cli.main",
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
id: "discord-pi",
|
|
61
|
+
extensionPackages: ["@dobby/provider-pi", "@dobby/connector-discord"],
|
|
62
|
+
providerInstanceId: "pi.main",
|
|
63
|
+
providerContributionId: "provider.pi",
|
|
64
|
+
providerConfig: {
|
|
65
|
+
provider: "custom-openai",
|
|
66
|
+
model: "example-model",
|
|
67
|
+
thinkingLevel: "off",
|
|
68
|
+
modelsFile: "./models.custom.json",
|
|
69
|
+
},
|
|
70
|
+
connectorInstanceId: DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID,
|
|
71
|
+
connectorContributionId: DISCORD_CONNECTOR_CONTRIBUTION_ID,
|
|
72
|
+
connectorConfig: {
|
|
73
|
+
botName: context.botName,
|
|
74
|
+
botToken: context.botToken,
|
|
75
|
+
botChannelMap: {
|
|
76
|
+
[context.channelId]: context.routeId,
|
|
77
|
+
},
|
|
78
|
+
reconnectStaleMs: 60_000,
|
|
79
|
+
reconnectCheckIntervalMs: 10_000,
|
|
80
|
+
},
|
|
81
|
+
routeProfile: {
|
|
82
|
+
...baseRoute,
|
|
83
|
+
providerId: "pi.main",
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import pino from "pino";
|
|
4
|
+
/**
|
|
5
|
+
* Creates the shared gateway logger instance used across CLI commands.
|
|
6
|
+
*/
|
|
7
|
+
export function createLogger() {
|
|
8
|
+
return pino({
|
|
9
|
+
name: "dobby",
|
|
10
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Returns the extension store directory from normalized gateway config.
|
|
15
|
+
*/
|
|
16
|
+
export function extensionStoreDir(config) {
|
|
17
|
+
return join(config.data.rootDir, "extensions");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Ensures required runtime data directories exist before start/doctor operations.
|
|
21
|
+
*/
|
|
22
|
+
export async function ensureDataDirs(rootDir) {
|
|
23
|
+
await mkdir(rootDir, { recursive: true });
|
|
24
|
+
await mkdir(join(rootDir, "sessions"), { recursive: true });
|
|
25
|
+
await mkdir(join(rootDir, "attachments"), { recursive: true });
|
|
26
|
+
await mkdir(join(rootDir, "logs"), { recursive: true });
|
|
27
|
+
await mkdir(join(rootDir, "state"), { recursive: true });
|
|
28
|
+
await mkdir(join(rootDir, "extensions"), { recursive: true });
|
|
29
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { cancel, confirm, isCancel, multiselect, note, select, text, } from "@clack/prompts";
|
|
2
|
+
import JSON5 from "json5";
|
|
3
|
+
function isRecord(value) {
|
|
4
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function isPrimitive(value) {
|
|
7
|
+
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
8
|
+
}
|
|
9
|
+
function stringifyPreview(value) {
|
|
10
|
+
if (typeof value === "string") {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
if (isPrimitive(value)) {
|
|
14
|
+
return String(value);
|
|
15
|
+
}
|
|
16
|
+
if (value === undefined) {
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
return JSON.stringify(value);
|
|
20
|
+
}
|
|
21
|
+
function schemaType(schema) {
|
|
22
|
+
const raw = schema.type;
|
|
23
|
+
if (typeof raw === "string") {
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(raw)) {
|
|
27
|
+
const firstStringType = raw.find((item) => typeof item === "string");
|
|
28
|
+
return typeof firstStringType === "string" ? firstStringType : undefined;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function schemaEnum(schema) {
|
|
33
|
+
if (!Array.isArray(schema.enum)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const normalized = schema.enum.filter((item) => isPrimitive(item));
|
|
37
|
+
return normalized.length > 0 ? normalized : null;
|
|
38
|
+
}
|
|
39
|
+
function schemaRequiredSet(schema) {
|
|
40
|
+
if (!Array.isArray(schema.required)) {
|
|
41
|
+
return new Set();
|
|
42
|
+
}
|
|
43
|
+
return new Set(schema.required.filter((item) => typeof item === "string"));
|
|
44
|
+
}
|
|
45
|
+
function schemaProperties(schema) {
|
|
46
|
+
if (!isRecord(schema.properties)) {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
const result = {};
|
|
50
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
51
|
+
if (isRecord(value)) {
|
|
52
|
+
result[key] = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
function hasSchemaDefault(schema) {
|
|
58
|
+
return Object.prototype.hasOwnProperty.call(schema, "default");
|
|
59
|
+
}
|
|
60
|
+
function shouldPromptInMinimalMode(field) {
|
|
61
|
+
if (!field.hasDefault && field.required) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (!field.hasDefault && field.existingValue === undefined) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
async function promptNumberField(params) {
|
|
70
|
+
while (true) {
|
|
71
|
+
const result = await text({
|
|
72
|
+
message: params.message,
|
|
73
|
+
initialValue: stringifyPreview(params.initialValue),
|
|
74
|
+
placeholder: params.integer ? "integer" : "number",
|
|
75
|
+
});
|
|
76
|
+
if (isCancel(result)) {
|
|
77
|
+
cancel("Configuration cancelled.");
|
|
78
|
+
throw new Error("Configuration cancelled.");
|
|
79
|
+
}
|
|
80
|
+
const raw = String(result ?? "").trim();
|
|
81
|
+
if (raw.length === 0) {
|
|
82
|
+
if (params.required && params.existingValue === undefined) {
|
|
83
|
+
await note("This field is required.", "Validation");
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
return typeof params.existingValue === "number" ? params.existingValue : undefined;
|
|
87
|
+
}
|
|
88
|
+
const parsed = Number(raw);
|
|
89
|
+
if (!Number.isFinite(parsed)) {
|
|
90
|
+
await note("Please enter a valid number.", "Validation");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (params.integer && !Number.isInteger(parsed)) {
|
|
94
|
+
await note("Please enter an integer.", "Validation");
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
return parsed;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function promptJsonField(params) {
|
|
101
|
+
while (true) {
|
|
102
|
+
const result = await text({
|
|
103
|
+
message: params.message,
|
|
104
|
+
initialValue: stringifyPreview(params.initialValue),
|
|
105
|
+
placeholder: params.expected === "object" ? '{"key":"value"}' : '["value"]',
|
|
106
|
+
});
|
|
107
|
+
if (isCancel(result)) {
|
|
108
|
+
cancel("Configuration cancelled.");
|
|
109
|
+
throw new Error("Configuration cancelled.");
|
|
110
|
+
}
|
|
111
|
+
const raw = String(result ?? "").trim();
|
|
112
|
+
if (raw.length === 0) {
|
|
113
|
+
if (params.required && params.existingValue === undefined) {
|
|
114
|
+
await note("This field is required.", "Validation");
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
return params.existingValue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON5.parse(raw);
|
|
121
|
+
if (params.expected === "object" && (!parsed || typeof parsed !== "object" || Array.isArray(parsed))) {
|
|
122
|
+
await note("Please enter a JSON object.", "Validation");
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (params.expected === "array" && !Array.isArray(parsed)) {
|
|
126
|
+
await note("Please enter a JSON array.", "Validation");
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
return parsed;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
await note(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`, "Validation");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function promptArrayField(params) {
|
|
137
|
+
const itemSchema = isRecord(params.schema.items) ? params.schema.items : {};
|
|
138
|
+
const itemEnum = schemaEnum(itemSchema);
|
|
139
|
+
if (itemEnum && itemEnum.length > 0) {
|
|
140
|
+
const current = Array.isArray(params.initialValue) ? params.initialValue : [];
|
|
141
|
+
const initialValues = current.filter((item) => itemEnum.includes(item))
|
|
142
|
+
.map((item) => String(item));
|
|
143
|
+
const result = await multiselect({
|
|
144
|
+
message: `${params.key}${params.required ? " (required)" : ""}`,
|
|
145
|
+
options: itemEnum.map((item) => ({
|
|
146
|
+
value: String(item),
|
|
147
|
+
label: String(item),
|
|
148
|
+
})),
|
|
149
|
+
initialValues,
|
|
150
|
+
required: params.required,
|
|
151
|
+
});
|
|
152
|
+
if (isCancel(result)) {
|
|
153
|
+
cancel("Configuration cancelled.");
|
|
154
|
+
throw new Error("Configuration cancelled.");
|
|
155
|
+
}
|
|
156
|
+
return result.map((value) => itemEnum.find((candidate) => String(candidate) === value) ?? value);
|
|
157
|
+
}
|
|
158
|
+
return promptJsonField({
|
|
159
|
+
message: `${params.key}${params.required ? " (required)" : ""} (JSON array)`,
|
|
160
|
+
required: params.required,
|
|
161
|
+
initialValue: params.initialValue,
|
|
162
|
+
expected: "array",
|
|
163
|
+
existingValue: params.existingValue,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async function promptFieldValue(params) {
|
|
167
|
+
const { key, schema, required, initialValue, existingValue } = params;
|
|
168
|
+
const enumValues = schemaEnum(schema);
|
|
169
|
+
const type = schemaType(schema);
|
|
170
|
+
const description = typeof schema.description === "string" ? schema.description.trim() : "";
|
|
171
|
+
const message = `${key}${required ? " (required)" : ""}${description ? ` - ${description}` : ""}`;
|
|
172
|
+
if (enumValues && enumValues.length > 0) {
|
|
173
|
+
const fallback = enumValues[0];
|
|
174
|
+
const initialCandidate = enumValues.includes(initialValue)
|
|
175
|
+
? initialValue
|
|
176
|
+
: enumValues.includes(existingValue)
|
|
177
|
+
? existingValue
|
|
178
|
+
: fallback;
|
|
179
|
+
const result = await select({
|
|
180
|
+
message,
|
|
181
|
+
options: enumValues.map((value) => ({
|
|
182
|
+
value: String(value),
|
|
183
|
+
label: String(value),
|
|
184
|
+
})),
|
|
185
|
+
initialValue: String(initialCandidate),
|
|
186
|
+
});
|
|
187
|
+
if (isCancel(result)) {
|
|
188
|
+
cancel("Configuration cancelled.");
|
|
189
|
+
throw new Error("Configuration cancelled.");
|
|
190
|
+
}
|
|
191
|
+
return enumValues.find((value) => String(value) === String(result)) ?? result;
|
|
192
|
+
}
|
|
193
|
+
if (type === "boolean") {
|
|
194
|
+
const result = await confirm({
|
|
195
|
+
message,
|
|
196
|
+
initialValue: typeof initialValue === "boolean"
|
|
197
|
+
? initialValue
|
|
198
|
+
: typeof existingValue === "boolean"
|
|
199
|
+
? existingValue
|
|
200
|
+
: false,
|
|
201
|
+
});
|
|
202
|
+
if (isCancel(result)) {
|
|
203
|
+
cancel("Configuration cancelled.");
|
|
204
|
+
throw new Error("Configuration cancelled.");
|
|
205
|
+
}
|
|
206
|
+
return result === true;
|
|
207
|
+
}
|
|
208
|
+
if (type === "integer" || type === "number") {
|
|
209
|
+
return promptNumberField({
|
|
210
|
+
message,
|
|
211
|
+
required,
|
|
212
|
+
initialValue,
|
|
213
|
+
integer: type === "integer",
|
|
214
|
+
existingValue,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (type === "array") {
|
|
218
|
+
return promptArrayField({
|
|
219
|
+
key,
|
|
220
|
+
schema,
|
|
221
|
+
required,
|
|
222
|
+
initialValue,
|
|
223
|
+
existingValue,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (type === "object") {
|
|
227
|
+
return promptJsonField({
|
|
228
|
+
message: `${message} (JSON object)`,
|
|
229
|
+
required,
|
|
230
|
+
initialValue,
|
|
231
|
+
expected: "object",
|
|
232
|
+
existingValue,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
while (true) {
|
|
236
|
+
const result = await text({
|
|
237
|
+
message,
|
|
238
|
+
initialValue: stringifyPreview(initialValue),
|
|
239
|
+
});
|
|
240
|
+
if (isCancel(result)) {
|
|
241
|
+
cancel("Configuration cancelled.");
|
|
242
|
+
throw new Error("Configuration cancelled.");
|
|
243
|
+
}
|
|
244
|
+
const raw = String(result ?? "").trim();
|
|
245
|
+
if (raw.length === 0) {
|
|
246
|
+
if (required && existingValue === undefined) {
|
|
247
|
+
await note("This field is required.", "Validation");
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
return existingValue;
|
|
251
|
+
}
|
|
252
|
+
return raw;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Prompts one extension config object from a contribution JSON Schema.
|
|
257
|
+
* Currently supports top-level schema properties and falls back to JSON input for complex fields.
|
|
258
|
+
*/
|
|
259
|
+
export async function promptConfigFromSchema(schema, currentConfig, options) {
|
|
260
|
+
const properties = schemaProperties(schema);
|
|
261
|
+
const required = schemaRequiredSet(schema);
|
|
262
|
+
if (options?.title) {
|
|
263
|
+
await note(options.title, "Configure");
|
|
264
|
+
}
|
|
265
|
+
if (Object.keys(properties).length === 0) {
|
|
266
|
+
return structuredClone(currentConfig);
|
|
267
|
+
}
|
|
268
|
+
const next = structuredClone(currentConfig);
|
|
269
|
+
const fieldDescriptors = Object.entries(properties).map(([key, fieldSchema]) => {
|
|
270
|
+
const existingValue = next[key];
|
|
271
|
+
const defaultValue = fieldSchema.default;
|
|
272
|
+
const initialValue = existingValue !== undefined ? existingValue : defaultValue;
|
|
273
|
+
return {
|
|
274
|
+
key,
|
|
275
|
+
schema: fieldSchema,
|
|
276
|
+
required: required.has(key),
|
|
277
|
+
hasDefault: hasSchemaDefault(fieldSchema),
|
|
278
|
+
existingValue,
|
|
279
|
+
initialValue,
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
const minimalFields = fieldDescriptors.filter((field) => shouldPromptInMinimalMode(field));
|
|
283
|
+
const advancedFields = fieldDescriptors.filter((field) => !minimalFields.includes(field));
|
|
284
|
+
for (const field of minimalFields) {
|
|
285
|
+
const value = await promptFieldValue({
|
|
286
|
+
key: field.key,
|
|
287
|
+
schema: field.schema,
|
|
288
|
+
required: field.required,
|
|
289
|
+
initialValue: field.initialValue,
|
|
290
|
+
existingValue: field.existingValue,
|
|
291
|
+
});
|
|
292
|
+
if (value === undefined) {
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
next[field.key] = value;
|
|
296
|
+
}
|
|
297
|
+
if (advancedFields.length > 0) {
|
|
298
|
+
const shouldPromptAdvanced = options?.promptDefaultedFields === true
|
|
299
|
+
? true
|
|
300
|
+
: await confirm({
|
|
301
|
+
message: "Configure advanced options (defaults can be kept)?",
|
|
302
|
+
initialValue: false,
|
|
303
|
+
});
|
|
304
|
+
if (isCancel(shouldPromptAdvanced)) {
|
|
305
|
+
cancel("Configuration cancelled.");
|
|
306
|
+
throw new Error("Configuration cancelled.");
|
|
307
|
+
}
|
|
308
|
+
if (shouldPromptAdvanced === true) {
|
|
309
|
+
for (const field of advancedFields) {
|
|
310
|
+
const value = await promptFieldValue({
|
|
311
|
+
key: field.key,
|
|
312
|
+
schema: field.schema,
|
|
313
|
+
required: field.required,
|
|
314
|
+
initialValue: field.initialValue,
|
|
315
|
+
existingValue: field.existingValue,
|
|
316
|
+
});
|
|
317
|
+
if (value === undefined) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
next[field.key] = value;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return next;
|
|
325
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildConfigListEntries, CONFIG_SECTION_VALUES, isConfigSection, previewConfigValue, } from "../commands/config.js";
|
|
4
|
+
test("isConfigSection accepts only supported top-level config sections", () => {
|
|
5
|
+
for (const section of CONFIG_SECTION_VALUES) {
|
|
6
|
+
assert.equal(isConfigSection(section), true);
|
|
7
|
+
}
|
|
8
|
+
assert.equal(isConfigSection("provider"), false);
|
|
9
|
+
assert.equal(isConfigSection("bot"), false);
|
|
10
|
+
});
|
|
11
|
+
test("previewConfigValue returns stable compact previews", () => {
|
|
12
|
+
assert.equal(previewConfigValue("abc"), "\"abc\"");
|
|
13
|
+
assert.equal(previewConfigValue(123), "123");
|
|
14
|
+
assert.equal(previewConfigValue(true), "true");
|
|
15
|
+
assert.equal(previewConfigValue(null), "null");
|
|
16
|
+
assert.equal(previewConfigValue({ a: 1, b: 2, c: 3, d: 4 }), "{a, b, c, ...}");
|
|
17
|
+
});
|
|
18
|
+
test("buildConfigListEntries summarizes object values with type and child counts", () => {
|
|
19
|
+
const entries = buildConfigListEntries({
|
|
20
|
+
providers: {
|
|
21
|
+
default: "pi.main",
|
|
22
|
+
items: {
|
|
23
|
+
"pi.main": { type: "provider.pi" },
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
featureFlag: true,
|
|
27
|
+
});
|
|
28
|
+
assert.deepEqual(entries.map((entry) => ({ key: entry.key, type: entry.type, children: entry.children })), [
|
|
29
|
+
{ key: "featureFlag", type: "boolean", children: undefined },
|
|
30
|
+
{ key: "providers", type: "object", children: 2 },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
test("buildConfigListEntries handles primitive roots", () => {
|
|
34
|
+
const entries = buildConfigListEntries("plain");
|
|
35
|
+
assert.deepEqual(entries, [
|
|
36
|
+
{
|
|
37
|
+
key: "(value)",
|
|
38
|
+
type: "string",
|
|
39
|
+
preview: "\"plain\"",
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { DEFAULT_CONFIG_PATH, resolveConfigPath, resolveDataRootDir } from "../shared/config-io.js";
|
|
8
|
+
test("resolveConfigPath defaults to $HOME/.dobby/gateway.json", () => {
|
|
9
|
+
assert.equal(DEFAULT_CONFIG_PATH, resolve(homedir(), ".dobby", "gateway.json"));
|
|
10
|
+
});
|
|
11
|
+
test("resolveConfigPath falls back to default path outside dobby repository", async () => {
|
|
12
|
+
const cwd = await mkdtemp(resolve(tmpdir(), "dobby-config-path-default-"));
|
|
13
|
+
assert.equal(resolveConfigPath({ cwd, env: {} }), DEFAULT_CONFIG_PATH);
|
|
14
|
+
});
|
|
15
|
+
test("resolveConfigPath detects local dobby repository config path", async () => {
|
|
16
|
+
const repoRoot = await mkdtemp(resolve(tmpdir(), "dobby-config-path-repo-"));
|
|
17
|
+
await mkdir(resolve(repoRoot, "config"), { recursive: true });
|
|
18
|
+
await mkdir(resolve(repoRoot, "scripts"), { recursive: true });
|
|
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");
|
|
22
|
+
await writeFile(resolve(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
|
|
23
|
+
assert.equal(resolveConfigPath({
|
|
24
|
+
cwd: resolve(repoRoot, "src", "cli"),
|
|
25
|
+
env: {},
|
|
26
|
+
}), resolve(repoRoot, "config", "gateway.json"));
|
|
27
|
+
});
|
|
28
|
+
test("resolveConfigPath prioritizes DOBBY_CONFIG_PATH over repository detection", async () => {
|
|
29
|
+
const repoRoot = await mkdtemp(resolve(tmpdir(), "dobby-config-path-env-priority-"));
|
|
30
|
+
await mkdir(resolve(repoRoot, "config"), { recursive: true });
|
|
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");
|
|
34
|
+
await writeFile(resolve(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
|
|
35
|
+
const customPath = resolve(tmpdir(), "dobby-custom-gateway.json");
|
|
36
|
+
assert.equal(resolveConfigPath({
|
|
37
|
+
cwd: repoRoot,
|
|
38
|
+
env: { DOBBY_CONFIG_PATH: customPath },
|
|
39
|
+
}), customPath);
|
|
40
|
+
});
|
|
41
|
+
test("resolveConfigPath supports relative and home-prefixed DOBBY_CONFIG_PATH", async () => {
|
|
42
|
+
const cwd = await mkdtemp(resolve(tmpdir(), "dobby-config-path-env-expand-"));
|
|
43
|
+
assert.equal(resolveConfigPath({
|
|
44
|
+
cwd,
|
|
45
|
+
env: { DOBBY_CONFIG_PATH: "config/local-gateway.json" },
|
|
46
|
+
}), resolve(cwd, "config/local-gateway.json"));
|
|
47
|
+
assert.equal(resolveConfigPath({
|
|
48
|
+
cwd,
|
|
49
|
+
env: { DOBBY_CONFIG_PATH: "~/custom-gateway.json" },
|
|
50
|
+
}), resolve(homedir(), "custom-gateway.json"));
|
|
51
|
+
});
|
|
52
|
+
test("resolveDataRootDir uses repo root for repo-local config/gateway.json", async () => {
|
|
53
|
+
const repoRoot = await mkdtemp(resolve(tmpdir(), "dobby-data-root-repo-"));
|
|
54
|
+
await mkdir(resolve(repoRoot, "config"), { recursive: true });
|
|
55
|
+
await mkdir(resolve(repoRoot, "scripts"), { recursive: true });
|
|
56
|
+
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "dobby" }), "utf-8");
|
|
57
|
+
await writeFile(resolve(repoRoot, "config", "gateway.json"), "{}", "utf-8");
|
|
58
|
+
await writeFile(resolve(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
|
|
59
|
+
assert.equal(resolveDataRootDir(resolve(repoRoot, "config", "gateway.json"), {
|
|
60
|
+
data: {
|
|
61
|
+
rootDir: "./data",
|
|
62
|
+
},
|
|
63
|
+
}), resolve(repoRoot, "data"));
|
|
64
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { applyContributionTemplates, ensureGatewayConfigShape, setDefaultProviderIfMissingOrInvalid, upsertAllowListPackage, } from "../shared/config-mutators.js";
|
|
4
|
+
test("upsertAllowListPackage is idempotent", () => {
|
|
5
|
+
const config = ensureGatewayConfigShape({});
|
|
6
|
+
upsertAllowListPackage(config, "@dobby.ai/provider-pi", true);
|
|
7
|
+
upsertAllowListPackage(config, "@dobby.ai/provider-pi", true);
|
|
8
|
+
assert.equal(config.extensions?.allowList?.length, 1);
|
|
9
|
+
assert.equal(config.extensions?.allowList?.[0]?.package, "@dobby.ai/provider-pi");
|
|
10
|
+
assert.equal(config.extensions?.allowList?.[0]?.enabled, true);
|
|
11
|
+
});
|
|
12
|
+
test("applyContributionTemplates allocates new instance IDs when needed", () => {
|
|
13
|
+
const config = ensureGatewayConfigShape({
|
|
14
|
+
providers: {
|
|
15
|
+
default: "pi.main",
|
|
16
|
+
items: {
|
|
17
|
+
"pi.main": {
|
|
18
|
+
type: "provider.pi",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
const added = applyContributionTemplates(config, {
|
|
24
|
+
providers: [
|
|
25
|
+
{ id: "pi.main", type: "provider.another", config: {} },
|
|
26
|
+
{ id: "pi.main", type: "provider.third", config: {} },
|
|
27
|
+
],
|
|
28
|
+
connectors: [],
|
|
29
|
+
sandboxes: [],
|
|
30
|
+
});
|
|
31
|
+
assert.deepEqual(added.providers, ["pi.main-2", "pi.main-3"]);
|
|
32
|
+
assert.equal(config.providers.items["pi.main-2"]?.type, "provider.another");
|
|
33
|
+
assert.equal(config.providers.items["pi.main-3"]?.type, "provider.third");
|
|
34
|
+
});
|
|
35
|
+
test("setDefaultProviderIfMissingOrInvalid picks lexicographically first provider", () => {
|
|
36
|
+
const config = ensureGatewayConfigShape({
|
|
37
|
+
providers: {
|
|
38
|
+
default: "missing",
|
|
39
|
+
items: {
|
|
40
|
+
"z.main": { type: "provider.z" },
|
|
41
|
+
"a.main": { type: "provider.a" },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
setDefaultProviderIfMissingOrInvalid(config);
|
|
46
|
+
assert.equal(config.providers.default, "a.main");
|
|
47
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { getAtPath, parsePath, setAtPath, unsetAtPath } from "../shared/config-path.js";
|
|
4
|
+
test("parsePath handles dot and bracket notation", () => {
|
|
5
|
+
assert.deepEqual(parsePath("routing.routes.main.projectRoot"), ["routing", "routes", "main", "projectRoot"]);
|
|
6
|
+
assert.deepEqual(parsePath("connectors.instances[discord.main].config.botChannelMap[12345]"), ["connectors", "instances", "discord.main", "config", "botChannelMap", "12345"]);
|
|
7
|
+
});
|
|
8
|
+
test("setAtPath and getAtPath support nested objects", () => {
|
|
9
|
+
const payload = {};
|
|
10
|
+
setAtPath(payload, parsePath("a.b.c"), 42);
|
|
11
|
+
const read = getAtPath(payload, parsePath("a.b.c"));
|
|
12
|
+
assert.equal(read.found, true);
|
|
13
|
+
assert.equal(read.value, 42);
|
|
14
|
+
});
|
|
15
|
+
test("unsetAtPath removes keys", () => {
|
|
16
|
+
const payload = { a: { b: { c: 1 } } };
|
|
17
|
+
const removed = unsetAtPath(payload, parsePath("a.b.c"));
|
|
18
|
+
assert.equal(removed, true);
|
|
19
|
+
const read = getAtPath(payload, parsePath("a.b.c"));
|
|
20
|
+
assert.equal(read.found, false);
|
|
21
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { normalizeDiscordBotChannelMap } from "../shared/discord-config.js";
|
|
4
|
+
test("normalizeDiscordBotChannelMap keeps valid channel->route entries", () => {
|
|
5
|
+
const normalized = normalizeDiscordBotChannelMap({
|
|
6
|
+
"123": "projectA",
|
|
7
|
+
"456": "projectB",
|
|
8
|
+
});
|
|
9
|
+
assert.deepEqual(normalized, {
|
|
10
|
+
"123": "projectA",
|
|
11
|
+
"456": "projectB",
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
test("normalizeDiscordBotChannelMap drops invalid values", () => {
|
|
15
|
+
const normalized = normalizeDiscordBotChannelMap({
|
|
16
|
+
"123": "projectA",
|
|
17
|
+
"456": "",
|
|
18
|
+
"789": 1,
|
|
19
|
+
});
|
|
20
|
+
assert.deepEqual(normalized, {
|
|
21
|
+
"123": "projectA",
|
|
22
|
+
});
|
|
23
|
+
});
|