@dobby.ai/dobby 0.1.1 → 0.1.2
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/README.md +20 -7
- package/dist/src/agent/event-forwarder.js +185 -16
- package/dist/src/cli/commands/cron.js +39 -35
- package/dist/src/cli/program.js +0 -6
- package/dist/src/core/types.js +2 -0
- package/dist/src/cron/config.js +2 -2
- package/dist/src/cron/service.js +87 -23
- package/dist/src/cron/store.js +1 -1
- package/package.json +9 -3
- package/.env.example +0 -8
- package/AGENTS.md +0 -267
- package/ROADMAP.md +0 -34
- package/config/cron.example.json +0 -9
- package/config/gateway.example.json +0 -132
- package/dist/plugins/connector-discord/src/mapper.js +0 -75
- package/dist/src/cli/tests/config-command.test.js +0 -42
- package/dist/src/cli/tests/config-io.test.js +0 -64
- package/dist/src/cli/tests/config-mutators.test.js +0 -47
- package/dist/src/cli/tests/discord-mapper.test.js +0 -90
- package/dist/src/cli/tests/doctor.test.js +0 -252
- package/dist/src/cli/tests/init-catalog.test.js +0 -134
- package/dist/src/cli/tests/program-options.test.js +0 -78
- package/dist/src/cli/tests/routing-config.test.js +0 -254
- package/dist/src/core/tests/control-command.test.js +0 -17
- package/dist/src/core/tests/runtime-registry.test.js +0 -116
- package/dist/src/core/tests/typing-controller.test.js +0 -103
- package/docs/BOXLITE_SANDBOX_FEASIBILITY.md +0 -175
- package/docs/CRON_SCHEDULER_DESIGN.md +0 -374
- package/docs/DOCKER_SANDBOX_vs_BOXLITE.md +0 -77
- package/docs/EXTENSION_SYSTEM_ARCHITECTURE.md +0 -119
- package/docs/MVP.md +0 -135
- package/docs/RUNBOOK.md +0 -243
- package/docs/TEAMWORK_HANDOFF_DESIGN.md +0 -440
- package/plugins/connector-discord/dobby.manifest.json +0 -18
- package/plugins/connector-discord/index.js +0 -1
- package/plugins/connector-discord/package-lock.json +0 -360
- package/plugins/connector-discord/package.json +0 -38
- package/plugins/connector-discord/src/connector.ts +0 -345
- package/plugins/connector-discord/src/contribution.ts +0 -21
- package/plugins/connector-discord/src/mapper.ts +0 -101
- package/plugins/connector-discord/tsconfig.json +0 -19
- package/plugins/connector-feishu/dobby.manifest.json +0 -18
- package/plugins/connector-feishu/index.js +0 -1
- package/plugins/connector-feishu/package-lock.json +0 -618
- package/plugins/connector-feishu/package.json +0 -38
- package/plugins/connector-feishu/src/connector.ts +0 -343
- package/plugins/connector-feishu/src/contribution.ts +0 -26
- package/plugins/connector-feishu/src/mapper.ts +0 -401
- package/plugins/connector-feishu/tsconfig.json +0 -19
- package/plugins/plugin-sdk/index.d.ts +0 -261
- package/plugins/plugin-sdk/index.js +0 -1
- package/plugins/plugin-sdk/package-lock.json +0 -12
- package/plugins/plugin-sdk/package.json +0 -22
- package/plugins/provider-claude/dobby.manifest.json +0 -17
- package/plugins/provider-claude/index.js +0 -1
- package/plugins/provider-claude/package-lock.json +0 -3398
- package/plugins/provider-claude/package.json +0 -39
- package/plugins/provider-claude/src/contribution.ts +0 -1018
- package/plugins/provider-claude/tsconfig.json +0 -19
- package/plugins/provider-claude-cli/dobby.manifest.json +0 -17
- package/plugins/provider-claude-cli/index.js +0 -1
- package/plugins/provider-claude-cli/package-lock.json +0 -2898
- package/plugins/provider-claude-cli/package.json +0 -38
- package/plugins/provider-claude-cli/src/contribution.ts +0 -1673
- package/plugins/provider-claude-cli/tsconfig.json +0 -19
- package/plugins/provider-pi/dobby.manifest.json +0 -17
- package/plugins/provider-pi/index.js +0 -1
- package/plugins/provider-pi/package-lock.json +0 -3877
- package/plugins/provider-pi/package.json +0 -40
- package/plugins/provider-pi/src/contribution.ts +0 -606
- package/plugins/provider-pi/tsconfig.json +0 -19
- package/plugins/sandbox-core/boxlite.js +0 -1
- package/plugins/sandbox-core/dobby.manifest.json +0 -17
- package/plugins/sandbox-core/docker.js +0 -1
- package/plugins/sandbox-core/package-lock.json +0 -136
- package/plugins/sandbox-core/package.json +0 -39
- package/plugins/sandbox-core/src/boxlite-context.ts +0 -2
- package/plugins/sandbox-core/src/boxlite-contribution.ts +0 -53
- package/plugins/sandbox-core/src/boxlite-executor.ts +0 -911
- package/plugins/sandbox-core/src/docker-contribution.ts +0 -43
- package/plugins/sandbox-core/src/docker-executor.ts +0 -217
- package/plugins/sandbox-core/tsconfig.json +0 -19
- package/scripts/local-extensions.mjs +0 -168
- package/src/agent/event-forwarder.ts +0 -414
- package/src/cli/commands/config.ts +0 -328
- package/src/cli/commands/configure.ts +0 -92
- package/src/cli/commands/cron.ts +0 -410
- package/src/cli/commands/doctor.ts +0 -331
- package/src/cli/commands/extension.ts +0 -207
- package/src/cli/commands/init.ts +0 -211
- package/src/cli/commands/start.ts +0 -223
- package/src/cli/commands/topology.ts +0 -415
- package/src/cli/index.ts +0 -9
- package/src/cli/program.ts +0 -314
- package/src/cli/shared/config-io.ts +0 -245
- package/src/cli/shared/config-mutators.ts +0 -470
- package/src/cli/shared/config-schema.ts +0 -228
- package/src/cli/shared/config-types.ts +0 -129
- package/src/cli/shared/configure-sections.ts +0 -595
- package/src/cli/shared/discord-config.ts +0 -14
- package/src/cli/shared/init-catalog.ts +0 -249
- package/src/cli/shared/local-extension-specs.ts +0 -108
- package/src/cli/shared/runtime.ts +0 -33
- package/src/cli/shared/schema-prompts.ts +0 -443
- package/src/cli/tests/config-command.test.ts +0 -56
- package/src/cli/tests/config-io.test.ts +0 -92
- package/src/cli/tests/config-mutators.test.ts +0 -59
- package/src/cli/tests/discord-mapper.test.ts +0 -128
- package/src/cli/tests/doctor.test.ts +0 -269
- package/src/cli/tests/init-catalog.test.ts +0 -144
- package/src/cli/tests/program-options.test.ts +0 -95
- package/src/cli/tests/routing-config.test.ts +0 -281
- package/src/core/control-command.ts +0 -12
- package/src/core/dedup-store.ts +0 -103
- package/src/core/gateway.ts +0 -609
- package/src/core/routing.ts +0 -404
- package/src/core/runtime-registry.ts +0 -141
- package/src/core/tests/control-command.test.ts +0 -20
- package/src/core/tests/runtime-registry.test.ts +0 -140
- package/src/core/tests/typing-controller.test.ts +0 -129
- package/src/core/types.ts +0 -324
- package/src/core/typing-controller.ts +0 -119
- package/src/cron/config.ts +0 -154
- package/src/cron/schedule.ts +0 -61
- package/src/cron/service.ts +0 -249
- package/src/cron/store.ts +0 -155
- package/src/cron/types.ts +0 -60
- package/src/extension/loader.ts +0 -145
- package/src/extension/manager.ts +0 -355
- package/src/extension/manifest.ts +0 -26
- package/src/extension/registry.ts +0 -229
- package/src/main.ts +0 -8
- package/src/sandbox/executor.ts +0 -44
- package/src/sandbox/host-executor.ts +0 -118
- package/src/shared/dobby-repo.ts +0 -48
- package/tsconfig.json +0 -18
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
function stripBotMention(text, botUserId) {
|
|
4
|
-
const mentionRegex = new RegExp(`<@!?${botUserId}>`, "g");
|
|
5
|
-
return text.replace(mentionRegex, "").trim();
|
|
6
|
-
}
|
|
7
|
-
function sanitizeFileName(value) {
|
|
8
|
-
return value.replaceAll(/[^a-zA-Z0-9._-]/g, "_");
|
|
9
|
-
}
|
|
10
|
-
async function downloadAttachment(url, targetPath) {
|
|
11
|
-
const response = await fetch(url);
|
|
12
|
-
if (!response.ok) {
|
|
13
|
-
throw new Error(`Failed to download attachment from ${url}: ${response.status}`);
|
|
14
|
-
}
|
|
15
|
-
const data = await response.arrayBuffer();
|
|
16
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
17
|
-
await writeFile(targetPath, Buffer.from(data));
|
|
18
|
-
}
|
|
19
|
-
function mapAttachmentBase(messageAttachment) {
|
|
20
|
-
return {
|
|
21
|
-
id: messageAttachment.id,
|
|
22
|
-
size: messageAttachment.size,
|
|
23
|
-
remoteUrl: messageAttachment.url,
|
|
24
|
-
...(messageAttachment.name ? { fileName: messageAttachment.name } : {}),
|
|
25
|
-
...(messageAttachment.contentType ? { mimeType: messageAttachment.contentType } : {}),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
export async function mapDiscordMessage(message, connectorId, botUserId, sourceId, attachmentsRoot, logger) {
|
|
29
|
-
if (message.author.bot)
|
|
30
|
-
return null;
|
|
31
|
-
const isDirectMessage = message.guildId == null;
|
|
32
|
-
const mentionedBot = message.mentions.users.has(botUserId);
|
|
33
|
-
const chatId = message.channelId;
|
|
34
|
-
const threadId = message.channel.isThread() ? message.channelId : undefined;
|
|
35
|
-
const cleanedText = stripBotMention(message.content ?? "", botUserId);
|
|
36
|
-
const attachments = [];
|
|
37
|
-
for (const attachment of message.attachments.values()) {
|
|
38
|
-
const base = mapAttachmentBase(attachment);
|
|
39
|
-
const attachmentDir = join(attachmentsRoot, sourceId, message.id);
|
|
40
|
-
const fileName = sanitizeFileName(attachment.name ?? attachment.id);
|
|
41
|
-
const localPath = join(attachmentDir, fileName);
|
|
42
|
-
try {
|
|
43
|
-
await downloadAttachment(attachment.url, localPath);
|
|
44
|
-
attachments.push({
|
|
45
|
-
...base,
|
|
46
|
-
localPath,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
logger.warn({ err: error, attachmentUrl: attachment.url }, "Failed to download Discord attachment; keeping metadata only");
|
|
51
|
-
attachments.push(base);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
connectorId,
|
|
56
|
-
platform: "discord",
|
|
57
|
-
accountId: botUserId,
|
|
58
|
-
source: {
|
|
59
|
-
type: "channel",
|
|
60
|
-
id: sourceId,
|
|
61
|
-
},
|
|
62
|
-
chatId,
|
|
63
|
-
messageId: message.id,
|
|
64
|
-
userId: message.author.id,
|
|
65
|
-
userName: message.author.username,
|
|
66
|
-
text: cleanedText,
|
|
67
|
-
attachments,
|
|
68
|
-
timestampMs: message.createdTimestamp,
|
|
69
|
-
raw: message.toJSON(),
|
|
70
|
-
isDirectMessage,
|
|
71
|
-
mentionedBot,
|
|
72
|
-
...(message.guildId ? { guildId: message.guildId } : {}),
|
|
73
|
-
...(threadId ? { threadId } : {}),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
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.ai/dobby" }), "utf-8");
|
|
21
|
-
await writeFile(resolve(repoRoot, "config", "gateway.example.json"), "{}\n", "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.ai/dobby" }), "utf-8");
|
|
33
|
-
await writeFile(resolve(repoRoot, "config", "gateway.example.json"), "{}\n", "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.ai/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
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
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
|
-
function createMessage(overrides) {
|
|
9
|
-
return {
|
|
10
|
-
id: overrides?.id ?? "msg-1",
|
|
11
|
-
content: overrides?.content ?? "hello",
|
|
12
|
-
author: {
|
|
13
|
-
id: "user-1",
|
|
14
|
-
username: "alice",
|
|
15
|
-
bot: false,
|
|
16
|
-
},
|
|
17
|
-
attachments: overrides?.attachments ?? new Map(),
|
|
18
|
-
mentions: {
|
|
19
|
-
users: {
|
|
20
|
-
has: () => false,
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
guildId: "guild-1",
|
|
24
|
-
channelId: "channel-1",
|
|
25
|
-
channel: {
|
|
26
|
-
isThread: () => false,
|
|
27
|
-
},
|
|
28
|
-
createdTimestamp: 1_700_000_000_000,
|
|
29
|
-
toJSON: () => ({ ok: true }),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
async function pathExists(path) {
|
|
33
|
-
try {
|
|
34
|
-
await access(path);
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const logger = pino({ enabled: false });
|
|
42
|
-
test("mapDiscordMessage does not create attachment directory when message has no attachments", async () => {
|
|
43
|
-
const root = await mkdtemp(join(tmpdir(), "dobby-discord-mapper-empty-"));
|
|
44
|
-
const message = createMessage();
|
|
45
|
-
const envelope = await mapDiscordMessage(message, "discord.main", "bot-1", "source-1", root, logger);
|
|
46
|
-
assert.ok(envelope);
|
|
47
|
-
assert.deepEqual(envelope.attachments, []);
|
|
48
|
-
assert.equal(await pathExists(join(root, "source-1", "msg-1")), false);
|
|
49
|
-
});
|
|
50
|
-
test("mapDiscordMessage only creates attachment directory when a download succeeds", async (t) => {
|
|
51
|
-
t.mock.method(globalThis, "fetch", async () => new Response("file-body", { status: 200 }));
|
|
52
|
-
const root = await mkdtemp(join(tmpdir(), "dobby-discord-mapper-file-"));
|
|
53
|
-
const message = createMessage({
|
|
54
|
-
attachments: new Map([
|
|
55
|
-
["att-1", {
|
|
56
|
-
id: "att-1",
|
|
57
|
-
name: "hello.png",
|
|
58
|
-
contentType: "image/png",
|
|
59
|
-
size: 9,
|
|
60
|
-
url: "https://example.test/hello.png",
|
|
61
|
-
}],
|
|
62
|
-
]),
|
|
63
|
-
});
|
|
64
|
-
const envelope = await mapDiscordMessage(message, "discord.main", "bot-1", "source-1", root, logger);
|
|
65
|
-
assert.ok(envelope);
|
|
66
|
-
assert.equal(envelope.attachments.length, 1);
|
|
67
|
-
assert.equal(await pathExists(join(root, "source-1", "msg-1")), true);
|
|
68
|
-
assert.equal(envelope.attachments[0]?.localPath, join(root, "source-1", "msg-1", "hello.png"));
|
|
69
|
-
assert.equal(await readFile(join(root, "source-1", "msg-1", "hello.png"), "utf-8"), "file-body");
|
|
70
|
-
});
|
|
71
|
-
test("mapDiscordMessage does not leave an empty attachment directory when download fails", async (t) => {
|
|
72
|
-
t.mock.method(globalThis, "fetch", async () => new Response("nope", { status: 500 }));
|
|
73
|
-
const root = await mkdtemp(join(tmpdir(), "dobby-discord-mapper-fail-"));
|
|
74
|
-
const message = createMessage({
|
|
75
|
-
attachments: new Map([
|
|
76
|
-
["att-1", {
|
|
77
|
-
id: "att-1",
|
|
78
|
-
name: "broken.png",
|
|
79
|
-
contentType: "image/png",
|
|
80
|
-
size: 9,
|
|
81
|
-
url: "https://example.test/broken.png",
|
|
82
|
-
}],
|
|
83
|
-
]),
|
|
84
|
-
});
|
|
85
|
-
const envelope = await mapDiscordMessage(message, "discord.main", "bot-1", "source-1", root, logger);
|
|
86
|
-
assert.ok(envelope);
|
|
87
|
-
assert.equal(envelope.attachments.length, 1);
|
|
88
|
-
assert.equal(envelope.attachments[0]?.localPath, undefined);
|
|
89
|
-
assert.equal(await pathExists(join(root, "source-1", "msg-1")), false);
|
|
90
|
-
});
|
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { spawn } from "node:child_process";
|
|
3
|
-
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
/**
|
|
8
|
-
* Writes a temporary default config under HOME/.dobby/gateway.json.
|
|
9
|
-
*/
|
|
10
|
-
async function writeTempHomeConfig(homeDir, payload) {
|
|
11
|
-
const dobbyDir = join(homeDir, ".dobby");
|
|
12
|
-
await mkdir(dobbyDir, { recursive: true });
|
|
13
|
-
const configPath = join(dobbyDir, "gateway.json");
|
|
14
|
-
await writeFile(configPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
15
|
-
return configPath;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Runs `dobby doctor` in a child process with an isolated HOME directory.
|
|
19
|
-
*/
|
|
20
|
-
async function runDoctorWithHome(homeDir, configPath) {
|
|
21
|
-
return await new Promise((resolve, reject) => {
|
|
22
|
-
const child = spawn(process.execPath, ["--import", "tsx", "src/main.ts", "doctor"], {
|
|
23
|
-
cwd: process.cwd(),
|
|
24
|
-
env: {
|
|
25
|
-
...process.env,
|
|
26
|
-
HOME: homeDir,
|
|
27
|
-
DOBBY_CONFIG_PATH: configPath,
|
|
28
|
-
},
|
|
29
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
30
|
-
});
|
|
31
|
-
let output = "";
|
|
32
|
-
child.stdout.on("data", (chunk) => {
|
|
33
|
-
output += String(chunk);
|
|
34
|
-
});
|
|
35
|
-
child.stderr.on("data", (chunk) => {
|
|
36
|
-
output += String(chunk);
|
|
37
|
-
});
|
|
38
|
-
child.once("error", (error) => reject(error));
|
|
39
|
-
child.once("close", (code) => {
|
|
40
|
-
resolve({ code, output });
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
test("doctor reports invalid binding route references", async () => {
|
|
45
|
-
const homeDir = await mkdtemp(join(tmpdir(), "dobby-doctor-home-"));
|
|
46
|
-
try {
|
|
47
|
-
const configPath = await writeTempHomeConfig(homeDir, {
|
|
48
|
-
extensions: { allowList: [] },
|
|
49
|
-
providers: {
|
|
50
|
-
default: "pi.main",
|
|
51
|
-
items: {
|
|
52
|
-
"pi.main": {
|
|
53
|
-
type: "provider.pi",
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
connectors: {
|
|
58
|
-
items: {
|
|
59
|
-
"discord.main": {
|
|
60
|
-
type: "connector.discord",
|
|
61
|
-
botName: "dobby-main",
|
|
62
|
-
botToken: "token",
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
sandboxes: {
|
|
67
|
-
default: "host.builtin",
|
|
68
|
-
items: {},
|
|
69
|
-
},
|
|
70
|
-
routes: {
|
|
71
|
-
defaults: {
|
|
72
|
-
provider: "pi.main",
|
|
73
|
-
sandbox: "host.builtin",
|
|
74
|
-
tools: "full",
|
|
75
|
-
mentions: "required",
|
|
76
|
-
},
|
|
77
|
-
items: {
|
|
78
|
-
main: {
|
|
79
|
-
projectRoot: process.cwd(),
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
bindings: {
|
|
84
|
-
items: {
|
|
85
|
-
"discord.main.123": {
|
|
86
|
-
connector: "discord.main",
|
|
87
|
-
source: {
|
|
88
|
-
type: "channel",
|
|
89
|
-
id: "123",
|
|
90
|
-
},
|
|
91
|
-
route: "missing-route",
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
data: {
|
|
96
|
-
rootDir: "./data",
|
|
97
|
-
dedupTtlMs: 604800000,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
const result = await runDoctorWithHome(homeDir, configPath);
|
|
101
|
-
assert.equal(result.code, 1);
|
|
102
|
-
assert.equal(result.output.includes("bindings.items['discord.main.123'].route") && result.output.includes("missing-route"), true);
|
|
103
|
-
}
|
|
104
|
-
finally {
|
|
105
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
test("doctor reports invalid default binding route references", async () => {
|
|
109
|
-
const homeDir = await mkdtemp(join(tmpdir(), "dobby-doctor-default-binding-"));
|
|
110
|
-
try {
|
|
111
|
-
const configPath = await writeTempHomeConfig(homeDir, {
|
|
112
|
-
extensions: { allowList: [] },
|
|
113
|
-
providers: {
|
|
114
|
-
default: "pi.main",
|
|
115
|
-
items: {
|
|
116
|
-
"pi.main": {
|
|
117
|
-
type: "provider.pi",
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
connectors: {
|
|
122
|
-
items: {
|
|
123
|
-
"discord.main": {
|
|
124
|
-
type: "connector.discord",
|
|
125
|
-
botName: "dobby-main",
|
|
126
|
-
botToken: "token",
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
sandboxes: {
|
|
131
|
-
default: "host.builtin",
|
|
132
|
-
items: {},
|
|
133
|
-
},
|
|
134
|
-
routes: {
|
|
135
|
-
defaults: {
|
|
136
|
-
projectRoot: process.cwd(),
|
|
137
|
-
provider: "pi.main",
|
|
138
|
-
sandbox: "host.builtin",
|
|
139
|
-
tools: "full",
|
|
140
|
-
mentions: "required",
|
|
141
|
-
},
|
|
142
|
-
items: {
|
|
143
|
-
main: {},
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
bindings: {
|
|
147
|
-
default: {
|
|
148
|
-
route: "missing-route",
|
|
149
|
-
},
|
|
150
|
-
items: {},
|
|
151
|
-
},
|
|
152
|
-
data: {
|
|
153
|
-
rootDir: "./data",
|
|
154
|
-
dedupTtlMs: 604800000,
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
const result = await runDoctorWithHome(homeDir, configPath);
|
|
158
|
-
assert.equal(result.code, 1);
|
|
159
|
-
assert.equal(result.output.includes("bindings.default.route") && result.output.includes("missing-route"), true);
|
|
160
|
-
}
|
|
161
|
-
finally {
|
|
162
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
test("doctor reports init template placeholders as errors and warnings", async () => {
|
|
166
|
-
const homeDir = await mkdtemp(join(tmpdir(), "dobby-doctor-placeholders-"));
|
|
167
|
-
try {
|
|
168
|
-
const configPath = await writeTempHomeConfig(homeDir, {
|
|
169
|
-
extensions: { allowList: [] },
|
|
170
|
-
providers: {
|
|
171
|
-
default: "pi.main",
|
|
172
|
-
items: {
|
|
173
|
-
"pi.main": {
|
|
174
|
-
type: "provider.pi",
|
|
175
|
-
model: "REPLACE_WITH_PROVIDER_MODEL_ID",
|
|
176
|
-
baseUrl: "REPLACE_WITH_PROVIDER_BASE_URL",
|
|
177
|
-
apiKey: "REPLACE_WITH_PROVIDER_API_KEY_OR_ENV",
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
connectors: {
|
|
182
|
-
items: {
|
|
183
|
-
"discord.main": {
|
|
184
|
-
type: "connector.discord",
|
|
185
|
-
botName: "dobby-main",
|
|
186
|
-
botToken: "REPLACE_WITH_DISCORD_BOT_TOKEN",
|
|
187
|
-
},
|
|
188
|
-
"feishu.main": {
|
|
189
|
-
type: "connector.feishu",
|
|
190
|
-
appId: "REPLACE_WITH_FEISHU_APP_ID",
|
|
191
|
-
appSecret: "REPLACE_WITH_FEISHU_APP_SECRET",
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
sandboxes: {
|
|
196
|
-
default: "host.builtin",
|
|
197
|
-
items: {},
|
|
198
|
-
},
|
|
199
|
-
routes: {
|
|
200
|
-
defaults: {
|
|
201
|
-
provider: "pi.main",
|
|
202
|
-
sandbox: "host.builtin",
|
|
203
|
-
tools: "full",
|
|
204
|
-
mentions: "required",
|
|
205
|
-
},
|
|
206
|
-
items: {
|
|
207
|
-
main: {
|
|
208
|
-
projectRoot: "./REPLACE_WITH_PROJECT_ROOT",
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
bindings: {
|
|
213
|
-
items: {
|
|
214
|
-
"discord.main.main": {
|
|
215
|
-
connector: "discord.main",
|
|
216
|
-
source: {
|
|
217
|
-
type: "channel",
|
|
218
|
-
id: "YOUR_DISCORD_CHANNEL_ID",
|
|
219
|
-
},
|
|
220
|
-
route: "main",
|
|
221
|
-
},
|
|
222
|
-
"feishu.main.main": {
|
|
223
|
-
connector: "feishu.main",
|
|
224
|
-
source: {
|
|
225
|
-
type: "chat",
|
|
226
|
-
id: "YOUR_FEISHU_CHAT_ID",
|
|
227
|
-
},
|
|
228
|
-
route: "main",
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
data: {
|
|
233
|
-
rootDir: "./data",
|
|
234
|
-
dedupTtlMs: 604800000,
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
const result = await runDoctorWithHome(homeDir, configPath);
|
|
238
|
-
assert.equal(result.code, 1);
|
|
239
|
-
assert.equal(result.output.includes("providers.items['pi.main'].model still uses placeholder value"), true);
|
|
240
|
-
assert.equal(result.output.includes("providers.items['pi.main'].baseUrl still uses placeholder value"), true);
|
|
241
|
-
assert.equal(result.output.includes("providers.items['pi.main'].apiKey still uses placeholder value"), true);
|
|
242
|
-
assert.equal(result.output.includes("connectors.items['discord.main'].botToken still uses placeholder value"), true);
|
|
243
|
-
assert.equal(result.output.includes("connectors.items['feishu.main'].appId still uses placeholder value"), true);
|
|
244
|
-
assert.equal(result.output.includes("connectors.items['feishu.main'].appSecret still uses placeholder value"), true);
|
|
245
|
-
assert.equal(result.output.includes("routes.items['main'].projectRoot still uses placeholder value"), true);
|
|
246
|
-
assert.equal(result.output.includes("bindings.items['discord.main.main'].source.id still uses placeholder value"), true);
|
|
247
|
-
assert.equal(result.output.includes("bindings.items['feishu.main.main'].source.id still uses placeholder value"), true);
|
|
248
|
-
}
|
|
249
|
-
finally {
|
|
250
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
251
|
-
}
|
|
252
|
-
});
|