@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
|
@@ -1,44 +1,54 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
RawBindingConfig,
|
|
3
|
+
RawDefaultBindingConfig,
|
|
4
|
+
RawRouteDefaults,
|
|
5
|
+
RawRouteProfile,
|
|
6
|
+
} from "./config-types.js";
|
|
2
7
|
import {
|
|
8
|
+
DEFAULT_DISCORD_BOT_NAME,
|
|
3
9
|
DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID,
|
|
4
10
|
DISCORD_CONNECTOR_CONTRIBUTION_ID,
|
|
5
11
|
} from "./discord-config.js";
|
|
6
12
|
|
|
7
13
|
export type InitProviderChoiceId = "provider.pi" | "provider.claude-cli";
|
|
8
|
-
export type InitConnectorChoiceId = "connector.discord";
|
|
14
|
+
export type InitConnectorChoiceId = "connector.discord" | "connector.feishu";
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_INIT_ROUTE_ID = "main";
|
|
17
|
+
export const DEFAULT_INIT_PROJECT_ROOT = "./REPLACE_WITH_PROJECT_ROOT";
|
|
9
18
|
|
|
10
19
|
interface ProviderCatalogEntry {
|
|
11
20
|
id: InitProviderChoiceId;
|
|
12
21
|
label: string;
|
|
13
|
-
|
|
22
|
+
package: string;
|
|
14
23
|
instanceId: string;
|
|
15
24
|
contributionId: string;
|
|
16
|
-
|
|
25
|
+
defaultConfig: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface ConnectorBindingTemplate {
|
|
29
|
+
sourceType: RawBindingConfig["source"]["type"];
|
|
30
|
+
sourceId: string;
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
interface ConnectorCatalogEntry {
|
|
20
34
|
id: InitConnectorChoiceId;
|
|
21
35
|
label: string;
|
|
22
|
-
|
|
36
|
+
package: string;
|
|
23
37
|
instanceId: string;
|
|
24
38
|
contributionId: string;
|
|
39
|
+
defaultConfig: Record<string, unknown>;
|
|
40
|
+
bindingTemplate: ConnectorBindingTemplate;
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
export interface InitSelectionContext {
|
|
28
|
-
routeId: string;
|
|
29
|
-
projectRoot: string;
|
|
30
|
-
allowAllMessages: boolean;
|
|
31
|
-
botName: string;
|
|
32
|
-
botToken: string;
|
|
33
|
-
channelId: string;
|
|
34
44
|
routeProviderChoiceId: InitProviderChoiceId;
|
|
45
|
+
defaultProjectRoot?: string;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
export interface InitSelectionResult {
|
|
38
49
|
providerChoiceIds: InitProviderChoiceId[];
|
|
39
50
|
routeProviderChoiceId: InitProviderChoiceId;
|
|
40
|
-
|
|
41
|
-
connectorChoiceId: InitConnectorChoiceId;
|
|
51
|
+
connectorChoiceIds: InitConnectorChoiceId[];
|
|
42
52
|
extensionPackages: string[];
|
|
43
53
|
providerInstances: Array<{
|
|
44
54
|
choiceId: InitProviderChoiceId;
|
|
@@ -46,38 +56,43 @@ export interface InitSelectionResult {
|
|
|
46
56
|
contributionId: string;
|
|
47
57
|
config: Record<string, unknown>;
|
|
48
58
|
}>;
|
|
59
|
+
connectorInstances: Array<{
|
|
60
|
+
choiceId: InitConnectorChoiceId;
|
|
61
|
+
instanceId: string;
|
|
62
|
+
contributionId: string;
|
|
63
|
+
config: Record<string, unknown>;
|
|
64
|
+
}>;
|
|
49
65
|
providerInstanceId: string;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
connectorInstanceId: string;
|
|
53
|
-
connectorContributionId: string;
|
|
54
|
-
connectorConfig: Record<string, unknown>;
|
|
66
|
+
routeId: string;
|
|
67
|
+
routeDefaults: RawRouteDefaults;
|
|
55
68
|
routeProfile: RawRouteProfile;
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
defaultBinding?: RawDefaultBindingConfig;
|
|
70
|
+
bindings: Array<{
|
|
71
|
+
id: string;
|
|
72
|
+
config: RawBindingConfig;
|
|
73
|
+
}>;
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
const PROVIDER_CATALOG: Record<InitProviderChoiceId, ProviderCatalogEntry> = {
|
|
61
77
|
"provider.pi": {
|
|
62
78
|
id: "provider.pi",
|
|
63
79
|
label: "Pi provider",
|
|
64
|
-
|
|
80
|
+
package: "@dobby.ai/provider-pi",
|
|
65
81
|
instanceId: "pi.main",
|
|
66
82
|
contributionId: "provider.pi",
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
modelsFile: "./models.custom.json",
|
|
83
|
+
defaultConfig: {
|
|
84
|
+
model: "REPLACE_WITH_PROVIDER_MODEL_ID",
|
|
85
|
+
baseUrl: "REPLACE_WITH_PROVIDER_BASE_URL",
|
|
86
|
+
apiKey: "REPLACE_WITH_PROVIDER_API_KEY_OR_ENV",
|
|
72
87
|
},
|
|
73
88
|
},
|
|
74
89
|
"provider.claude-cli": {
|
|
75
90
|
id: "provider.claude-cli",
|
|
76
91
|
label: "Claude CLI provider",
|
|
77
|
-
|
|
92
|
+
package: "@dobby.ai/provider-claude-cli",
|
|
78
93
|
instanceId: "claude-cli.main",
|
|
79
94
|
contributionId: "provider.claude-cli",
|
|
80
|
-
|
|
95
|
+
defaultConfig: {
|
|
81
96
|
model: "claude-sonnet-4-5",
|
|
82
97
|
maxTurns: 20,
|
|
83
98
|
command: "claude",
|
|
@@ -93,12 +108,56 @@ const CONNECTOR_CATALOG: Record<InitConnectorChoiceId, ConnectorCatalogEntry> =
|
|
|
93
108
|
"connector.discord": {
|
|
94
109
|
id: "connector.discord",
|
|
95
110
|
label: "Discord connector",
|
|
96
|
-
|
|
111
|
+
package: "@dobby.ai/connector-discord",
|
|
97
112
|
instanceId: DEFAULT_DISCORD_CONNECTOR_INSTANCE_ID,
|
|
98
113
|
contributionId: DISCORD_CONNECTOR_CONTRIBUTION_ID,
|
|
114
|
+
defaultConfig: {
|
|
115
|
+
botName: DEFAULT_DISCORD_BOT_NAME,
|
|
116
|
+
botToken: "REPLACE_WITH_DISCORD_BOT_TOKEN",
|
|
117
|
+
reconnectStaleMs: 60_000,
|
|
118
|
+
reconnectCheckIntervalMs: 10_000,
|
|
119
|
+
},
|
|
120
|
+
bindingTemplate: {
|
|
121
|
+
sourceType: "channel",
|
|
122
|
+
sourceId: "YOUR_DISCORD_CHANNEL_ID",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
"connector.feishu": {
|
|
126
|
+
id: "connector.feishu",
|
|
127
|
+
label: "Feishu connector",
|
|
128
|
+
package: "@dobby.ai/connector-feishu",
|
|
129
|
+
instanceId: "feishu.main",
|
|
130
|
+
contributionId: "connector.feishu",
|
|
131
|
+
defaultConfig: {
|
|
132
|
+
appId: "REPLACE_WITH_FEISHU_APP_ID",
|
|
133
|
+
appSecret: "REPLACE_WITH_FEISHU_APP_SECRET",
|
|
134
|
+
domain: "feishu",
|
|
135
|
+
messageFormat: "card_markdown",
|
|
136
|
+
replyMode: "direct",
|
|
137
|
+
downloadAttachments: true,
|
|
138
|
+
},
|
|
139
|
+
bindingTemplate: {
|
|
140
|
+
sourceType: "chat",
|
|
141
|
+
sourceId: "YOUR_FEISHU_CHAT_ID",
|
|
142
|
+
},
|
|
99
143
|
},
|
|
100
144
|
};
|
|
101
145
|
|
|
146
|
+
function dedupeChoiceIds<T extends string>(choiceIds: T[]): T[] {
|
|
147
|
+
const dedupedChoiceIds: T[] = [];
|
|
148
|
+
const seenChoiceIds = new Set<T>();
|
|
149
|
+
|
|
150
|
+
for (const choiceId of choiceIds) {
|
|
151
|
+
if (seenChoiceIds.has(choiceId)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
seenChoiceIds.add(choiceId);
|
|
155
|
+
dedupedChoiceIds.push(choiceId);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return dedupedChoiceIds;
|
|
159
|
+
}
|
|
160
|
+
|
|
102
161
|
export function listInitProviderChoices(): ProviderCatalogEntry[] {
|
|
103
162
|
return Object.values(PROVIDER_CATALOG);
|
|
104
163
|
}
|
|
@@ -117,22 +176,19 @@ export function isInitConnectorChoiceId(value: string): value is InitConnectorCh
|
|
|
117
176
|
|
|
118
177
|
export function createInitSelectionConfig(
|
|
119
178
|
providerChoiceIds: InitProviderChoiceId[],
|
|
120
|
-
|
|
179
|
+
connectorChoiceIds: InitConnectorChoiceId[],
|
|
121
180
|
context: InitSelectionContext,
|
|
122
181
|
): InitSelectionResult {
|
|
123
|
-
const dedupedProviderChoiceIds
|
|
124
|
-
const seenProviderChoiceIds = new Set<InitProviderChoiceId>();
|
|
125
|
-
for (const providerChoiceId of providerChoiceIds) {
|
|
126
|
-
if (!seenProviderChoiceIds.has(providerChoiceId)) {
|
|
127
|
-
seenProviderChoiceIds.add(providerChoiceId);
|
|
128
|
-
dedupedProviderChoiceIds.push(providerChoiceId);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
182
|
+
const dedupedProviderChoiceIds = dedupeChoiceIds(providerChoiceIds);
|
|
132
183
|
if (dedupedProviderChoiceIds.length === 0) {
|
|
133
184
|
throw new Error("At least one provider choice is required");
|
|
134
185
|
}
|
|
135
186
|
|
|
187
|
+
const dedupedConnectorChoiceIds = dedupeChoiceIds(connectorChoiceIds);
|
|
188
|
+
if (dedupedConnectorChoiceIds.length === 0) {
|
|
189
|
+
throw new Error("At least one connector choice is required");
|
|
190
|
+
}
|
|
191
|
+
|
|
136
192
|
if (!dedupedProviderChoiceIds.includes(context.routeProviderChoiceId)) {
|
|
137
193
|
throw new Error(
|
|
138
194
|
`route provider choice '${context.routeProviderChoiceId}' must be one of selected providers: ${dedupedProviderChoiceIds.join(", ")}`,
|
|
@@ -140,50 +196,54 @@ export function createInitSelectionConfig(
|
|
|
140
196
|
}
|
|
141
197
|
|
|
142
198
|
const providerChoices = dedupedProviderChoiceIds.map((providerChoiceId) => PROVIDER_CATALOG[providerChoiceId]);
|
|
199
|
+
const connectorChoices = dedupedConnectorChoiceIds.map((connectorChoiceId) => CONNECTOR_CATALOG[connectorChoiceId]);
|
|
143
200
|
const primaryProviderChoice = PROVIDER_CATALOG[context.routeProviderChoiceId];
|
|
144
|
-
const connectorChoice = CONNECTOR_CATALOG[connectorChoiceId];
|
|
145
201
|
|
|
146
202
|
return {
|
|
147
203
|
providerChoiceIds: dedupedProviderChoiceIds,
|
|
148
204
|
routeProviderChoiceId: primaryProviderChoice.id,
|
|
149
|
-
|
|
150
|
-
connectorChoiceId,
|
|
205
|
+
connectorChoiceIds: dedupedConnectorChoiceIds,
|
|
151
206
|
extensionPackages: [
|
|
152
|
-
...new Set([
|
|
207
|
+
...new Set([
|
|
208
|
+
...providerChoices.map((item) => item.package),
|
|
209
|
+
...connectorChoices.map((item) => item.package),
|
|
210
|
+
]),
|
|
153
211
|
],
|
|
154
212
|
providerInstances: providerChoices.map((providerChoice) => ({
|
|
155
213
|
choiceId: providerChoice.id,
|
|
156
214
|
instanceId: providerChoice.instanceId,
|
|
157
215
|
contributionId: providerChoice.contributionId,
|
|
158
|
-
config: structuredClone(providerChoice.
|
|
216
|
+
config: structuredClone(providerChoice.defaultConfig),
|
|
217
|
+
})),
|
|
218
|
+
connectorInstances: connectorChoices.map((connectorChoice) => ({
|
|
219
|
+
choiceId: connectorChoice.id,
|
|
220
|
+
instanceId: connectorChoice.instanceId,
|
|
221
|
+
contributionId: connectorChoice.contributionId,
|
|
222
|
+
config: structuredClone(connectorChoice.defaultConfig),
|
|
159
223
|
})),
|
|
160
224
|
providerInstanceId: primaryProviderChoice.instanceId,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
connectorContributionId: connectorChoice.contributionId,
|
|
165
|
-
connectorConfig: {
|
|
166
|
-
botName: context.botName,
|
|
167
|
-
botToken: context.botToken,
|
|
168
|
-
reconnectStaleMs: 60_000,
|
|
169
|
-
reconnectCheckIntervalMs: 10_000,
|
|
170
|
-
},
|
|
171
|
-
routeProfile: {
|
|
172
|
-
projectRoot: context.projectRoot,
|
|
225
|
+
routeId: DEFAULT_INIT_ROUTE_ID,
|
|
226
|
+
routeDefaults: {
|
|
227
|
+
projectRoot: context.defaultProjectRoot ?? DEFAULT_INIT_PROJECT_ROOT,
|
|
173
228
|
tools: "full",
|
|
174
|
-
|
|
175
|
-
mentions: context.allowAllMessages ? "optional" : "required",
|
|
229
|
+
mentions: "required",
|
|
176
230
|
provider: primaryProviderChoice.instanceId,
|
|
177
231
|
sandbox: "host.builtin",
|
|
178
232
|
},
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
source: {
|
|
183
|
-
type: "channel",
|
|
184
|
-
id: context.channelId,
|
|
185
|
-
},
|
|
186
|
-
route: context.routeId,
|
|
233
|
+
routeProfile: {},
|
|
234
|
+
defaultBinding: {
|
|
235
|
+
route: DEFAULT_INIT_ROUTE_ID,
|
|
187
236
|
},
|
|
237
|
+
bindings: connectorChoices.map((connectorChoice) => ({
|
|
238
|
+
id: `${connectorChoice.instanceId}.${DEFAULT_INIT_ROUTE_ID}`,
|
|
239
|
+
config: {
|
|
240
|
+
connector: connectorChoice.instanceId,
|
|
241
|
+
source: {
|
|
242
|
+
type: connectorChoice.bindingTemplate.sourceType,
|
|
243
|
+
id: connectorChoice.bindingTemplate.sourceId,
|
|
244
|
+
},
|
|
245
|
+
route: DEFAULT_INIT_ROUTE_ID,
|
|
246
|
+
},
|
|
247
|
+
})),
|
|
188
248
|
};
|
|
189
249
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
|
|
6
|
+
interface LocalExtensionPackage {
|
|
7
|
+
packageName: string;
|
|
8
|
+
packageDir: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isExplicitInstallSpec(value: string): boolean {
|
|
12
|
+
return value.startsWith("file:")
|
|
13
|
+
|| value.startsWith("git+")
|
|
14
|
+
|| value.startsWith("http://")
|
|
15
|
+
|| value.startsWith("https://")
|
|
16
|
+
|| value.startsWith("./")
|
|
17
|
+
|| value.startsWith("../")
|
|
18
|
+
|| value.startsWith("/");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function listRepoLocalExtensionPackages(repoRoot: string): Promise<Map<string, LocalExtensionPackage>> {
|
|
22
|
+
const pluginsRoot = resolve(repoRoot, "plugins");
|
|
23
|
+
const entries = await readdir(pluginsRoot, { withFileTypes: true });
|
|
24
|
+
const packages = new Map<string, LocalExtensionPackage>();
|
|
25
|
+
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (!entry.isDirectory() || entry.name === "plugin-sdk") {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const packageDir = resolve(pluginsRoot, entry.name);
|
|
32
|
+
const packageJsonPath = resolve(packageDir, "package.json");
|
|
33
|
+
const manifestPath = resolve(packageDir, "dobby.manifest.json");
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await access(packageJsonPath);
|
|
37
|
+
await access(manifestPath);
|
|
38
|
+
const raw = await readFile(packageJsonPath, "utf-8");
|
|
39
|
+
const parsed = JSON.parse(raw) as { name?: unknown };
|
|
40
|
+
if (typeof parsed.name !== "string" || parsed.name.trim().length === 0) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
packages.set(parsed.name, {
|
|
45
|
+
packageName: parsed.name,
|
|
46
|
+
packageDir,
|
|
47
|
+
});
|
|
48
|
+
} catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return packages;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function assertLocalExtensionBuildReady(localPackage: LocalExtensionPackage): Promise<void> {
|
|
57
|
+
const manifestPath = resolve(localPackage.packageDir, "dobby.manifest.json");
|
|
58
|
+
const rawManifest = await readFile(manifestPath, "utf-8");
|
|
59
|
+
const parsed = JSON.parse(rawManifest) as {
|
|
60
|
+
contributions?: Array<{ id?: unknown; entry?: unknown }>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (const contribution of parsed.contributions ?? []) {
|
|
64
|
+
if (typeof contribution.entry !== "string" || contribution.entry.trim().length === 0) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const entryPath = resolve(localPackage.packageDir, contribution.entry);
|
|
69
|
+
try {
|
|
70
|
+
await access(entryPath);
|
|
71
|
+
} catch {
|
|
72
|
+
const contributionId = typeof contribution.id === "string" ? contribution.id : "unknown";
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Local extension '${localPackage.packageName}' is not built for contribution '${contributionId}'. `
|
|
75
|
+
+ `Missing '${entryPath}'. Run 'npm run build --prefix ${localPackage.packageDir}' first.`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function resolveExtensionInstallSpecs(packageSpecs: string[], cwd = process.cwd()): Promise<string[]> {
|
|
82
|
+
const repoRoot = findDobbyRepoRoot(cwd);
|
|
83
|
+
if (!repoRoot) {
|
|
84
|
+
return packageSpecs;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const repoPackages = await listRepoLocalExtensionPackages(repoRoot);
|
|
88
|
+
const resolvedSpecs: string[] = [];
|
|
89
|
+
|
|
90
|
+
for (const rawSpec of packageSpecs) {
|
|
91
|
+
const packageSpec = rawSpec.trim();
|
|
92
|
+
if (packageSpec.length === 0 || isExplicitInstallSpec(packageSpec)) {
|
|
93
|
+
resolvedSpecs.push(packageSpec);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const localPackage = repoPackages.get(packageSpec);
|
|
98
|
+
if (!localPackage) {
|
|
99
|
+
resolvedSpecs.push(packageSpec);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await assertLocalExtensionBuildReady(localPackage);
|
|
104
|
+
resolvedSpecs.push(`file:${localPackage.packageDir}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return resolvedSpecs;
|
|
108
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isCancel,
|
|
5
5
|
multiselect,
|
|
6
6
|
note,
|
|
7
|
+
password,
|
|
7
8
|
select,
|
|
8
9
|
text,
|
|
9
10
|
} from "@clack/prompts";
|
|
@@ -95,13 +96,17 @@ function shouldPromptInMinimalMode(field: FieldPromptDescriptor): boolean {
|
|
|
95
96
|
return true;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
if (
|
|
99
|
+
if (field.existingValue !== undefined) {
|
|
99
100
|
return true;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
return false;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
function isSensitiveStringField(key: string): boolean {
|
|
107
|
+
return /(token|secret|api[-_]?key)$/i.test(key);
|
|
108
|
+
}
|
|
109
|
+
|
|
105
110
|
async function promptNumberField(params: {
|
|
106
111
|
message: string;
|
|
107
112
|
required: boolean;
|
|
@@ -306,6 +311,30 @@ async function promptFieldValue(params: {
|
|
|
306
311
|
});
|
|
307
312
|
}
|
|
308
313
|
|
|
314
|
+
if (isSensitiveStringField(key)) {
|
|
315
|
+
while (true) {
|
|
316
|
+
const result = await password({
|
|
317
|
+
message,
|
|
318
|
+
mask: "*",
|
|
319
|
+
});
|
|
320
|
+
if (isCancel(result)) {
|
|
321
|
+
cancel("Configuration cancelled.");
|
|
322
|
+
throw new Error("Configuration cancelled.");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const raw = String(result ?? "").trim();
|
|
326
|
+
if (raw.length === 0) {
|
|
327
|
+
if (required && existingValue === undefined) {
|
|
328
|
+
await note("This field is required.", "Validation");
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
return existingValue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return raw;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
309
338
|
while (true) {
|
|
310
339
|
const result = await text({
|
|
311
340
|
message,
|
|
@@ -21,8 +21,8 @@ test("resolveConfigPath detects local dobby repository config path", async () =>
|
|
|
21
21
|
await mkdir(resolve(repoRoot, "scripts"), { recursive: true });
|
|
22
22
|
await mkdir(resolve(repoRoot, "src", "cli"), { recursive: true });
|
|
23
23
|
|
|
24
|
-
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "dobby" }), "utf-8");
|
|
25
|
-
await writeFile(resolve(repoRoot, "config", "gateway.json"), "{}", "utf-8");
|
|
24
|
+
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "@dobby.ai/dobby" }), "utf-8");
|
|
25
|
+
await writeFile(resolve(repoRoot, "config", "gateway.example.json"), "{}\n", "utf-8");
|
|
26
26
|
await writeFile(resolve(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
|
|
27
27
|
|
|
28
28
|
assert.equal(
|
|
@@ -38,8 +38,8 @@ test("resolveConfigPath prioritizes DOBBY_CONFIG_PATH over repository detection"
|
|
|
38
38
|
const repoRoot = await mkdtemp(resolve(tmpdir(), "dobby-config-path-env-priority-"));
|
|
39
39
|
await mkdir(resolve(repoRoot, "config"), { recursive: true });
|
|
40
40
|
await mkdir(resolve(repoRoot, "scripts"), { recursive: true });
|
|
41
|
-
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "dobby" }), "utf-8");
|
|
42
|
-
await writeFile(resolve(repoRoot, "config", "gateway.json"), "{}", "utf-8");
|
|
41
|
+
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "@dobby.ai/dobby" }), "utf-8");
|
|
42
|
+
await writeFile(resolve(repoRoot, "config", "gateway.example.json"), "{}\n", "utf-8");
|
|
43
43
|
await writeFile(resolve(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
|
|
44
44
|
|
|
45
45
|
const customPath = resolve(tmpdir(), "dobby-custom-gateway.json");
|
|
@@ -77,7 +77,7 @@ test("resolveDataRootDir uses repo root for repo-local config/gateway.json", asy
|
|
|
77
77
|
await mkdir(resolve(repoRoot, "config"), { recursive: true });
|
|
78
78
|
await mkdir(resolve(repoRoot, "scripts"), { recursive: true });
|
|
79
79
|
|
|
80
|
-
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "dobby" }), "utf-8");
|
|
80
|
+
await writeFile(resolve(repoRoot, "package.json"), JSON.stringify({ name: "@dobby.ai/dobby" }), "utf-8");
|
|
81
81
|
await writeFile(resolve(repoRoot, "config", "gateway.json"), "{}", "utf-8");
|
|
82
82
|
await writeFile(resolve(repoRoot, "scripts", "local-extensions.mjs"), "#!/usr/bin/env node\n", "utf-8");
|
|
83
83
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { access, mkdtemp, readFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import pino from "pino";
|
|
7
|
+
import { mapDiscordMessage } from "../../../plugins/connector-discord/src/mapper.js";
|
|
8
|
+
|
|
9
|
+
function createMessage(overrides?: {
|
|
10
|
+
id?: string;
|
|
11
|
+
content?: string;
|
|
12
|
+
attachments?: Map<string, unknown>;
|
|
13
|
+
}): unknown {
|
|
14
|
+
return {
|
|
15
|
+
id: overrides?.id ?? "msg-1",
|
|
16
|
+
content: overrides?.content ?? "hello",
|
|
17
|
+
author: {
|
|
18
|
+
id: "user-1",
|
|
19
|
+
username: "alice",
|
|
20
|
+
bot: false,
|
|
21
|
+
},
|
|
22
|
+
attachments: overrides?.attachments ?? new Map(),
|
|
23
|
+
mentions: {
|
|
24
|
+
users: {
|
|
25
|
+
has: () => false,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
guildId: "guild-1",
|
|
29
|
+
channelId: "channel-1",
|
|
30
|
+
channel: {
|
|
31
|
+
isThread: () => false,
|
|
32
|
+
},
|
|
33
|
+
createdTimestamp: 1_700_000_000_000,
|
|
34
|
+
toJSON: () => ({ ok: true }),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function pathExists(path: string): Promise<boolean> {
|
|
39
|
+
try {
|
|
40
|
+
await access(path);
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const logger = pino({ enabled: false });
|
|
48
|
+
|
|
49
|
+
test("mapDiscordMessage does not create attachment directory when message has no attachments", async () => {
|
|
50
|
+
const root = await mkdtemp(join(tmpdir(), "dobby-discord-mapper-empty-"));
|
|
51
|
+
const message = createMessage();
|
|
52
|
+
|
|
53
|
+
const envelope = await mapDiscordMessage(
|
|
54
|
+
message as never,
|
|
55
|
+
"discord.main",
|
|
56
|
+
"bot-1",
|
|
57
|
+
"source-1",
|
|
58
|
+
root,
|
|
59
|
+
logger,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
assert.ok(envelope);
|
|
63
|
+
assert.deepEqual(envelope.attachments, []);
|
|
64
|
+
assert.equal(await pathExists(join(root, "source-1", "msg-1")), false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("mapDiscordMessage only creates attachment directory when a download succeeds", async (t) => {
|
|
68
|
+
t.mock.method(globalThis, "fetch", async () => new Response("file-body", { status: 200 }));
|
|
69
|
+
|
|
70
|
+
const root = await mkdtemp(join(tmpdir(), "dobby-discord-mapper-file-"));
|
|
71
|
+
const message = createMessage({
|
|
72
|
+
attachments: new Map([
|
|
73
|
+
["att-1", {
|
|
74
|
+
id: "att-1",
|
|
75
|
+
name: "hello.png",
|
|
76
|
+
contentType: "image/png",
|
|
77
|
+
size: 9,
|
|
78
|
+
url: "https://example.test/hello.png",
|
|
79
|
+
}],
|
|
80
|
+
]),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const envelope = await mapDiscordMessage(
|
|
84
|
+
message as never,
|
|
85
|
+
"discord.main",
|
|
86
|
+
"bot-1",
|
|
87
|
+
"source-1",
|
|
88
|
+
root,
|
|
89
|
+
logger,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
assert.ok(envelope);
|
|
93
|
+
assert.equal(envelope.attachments.length, 1);
|
|
94
|
+
assert.equal(await pathExists(join(root, "source-1", "msg-1")), true);
|
|
95
|
+
assert.equal(envelope.attachments[0]?.localPath, join(root, "source-1", "msg-1", "hello.png"));
|
|
96
|
+
assert.equal(await readFile(join(root, "source-1", "msg-1", "hello.png"), "utf-8"), "file-body");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("mapDiscordMessage does not leave an empty attachment directory when download fails", async (t) => {
|
|
100
|
+
t.mock.method(globalThis, "fetch", async () => new Response("nope", { status: 500 }));
|
|
101
|
+
|
|
102
|
+
const root = await mkdtemp(join(tmpdir(), "dobby-discord-mapper-fail-"));
|
|
103
|
+
const message = createMessage({
|
|
104
|
+
attachments: new Map([
|
|
105
|
+
["att-1", {
|
|
106
|
+
id: "att-1",
|
|
107
|
+
name: "broken.png",
|
|
108
|
+
contentType: "image/png",
|
|
109
|
+
size: 9,
|
|
110
|
+
url: "https://example.test/broken.png",
|
|
111
|
+
}],
|
|
112
|
+
]),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const envelope = await mapDiscordMessage(
|
|
116
|
+
message as never,
|
|
117
|
+
"discord.main",
|
|
118
|
+
"bot-1",
|
|
119
|
+
"source-1",
|
|
120
|
+
root,
|
|
121
|
+
logger,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
assert.ok(envelope);
|
|
125
|
+
assert.equal(envelope.attachments.length, 1);
|
|
126
|
+
assert.equal(envelope.attachments[0]?.localPath, undefined);
|
|
127
|
+
assert.equal(await pathExists(join(root, "source-1", "msg-1")), false);
|
|
128
|
+
});
|