@dobby.ai/dobby 0.1.0 → 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 +84 -39
- package/dist/src/agent/event-forwarder.js +185 -16
- package/dist/src/cli/commands/cron.js +39 -35
- 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 -137
- 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/core/gateway.js +3 -1
- package/dist/src/core/routing.js +53 -38
- 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/dist/src/main.js +0 -0
- package/dist/src/shared/dobby-repo.js +40 -0
- package/package.json +11 -4
- package/.env.example +0 -9
- package/AGENTS.md +0 -267
- package/ROADMAP.md +0 -34
- package/config/cron.example.json +0 -9
- package/config/gateway.example.json +0 -128
- 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-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/config-path.test.js +0 -21
- package/dist/src/cli/tests/discord-config.test.js +0 -23
- package/dist/src/cli/tests/doctor.test.js +0 -107
- package/dist/src/cli/tests/init-catalog.test.js +0 -87
- package/dist/src/cli/tests/presets.test.js +0 -41
- package/dist/src/cli/tests/program-options.test.js +0 -92
- package/dist/src/cli/tests/routing-config.test.js +0 -199
- package/dist/src/cli/tests/routing-legacy.test.js +0 -191
- package/dist/src/core/tests/control-command.test.js +0 -17
- package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
- 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 -242
- 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 -350
- package/plugins/connector-discord/src/contribution.ts +0 -21
- package/plugins/connector-discord/src/mapper.ts +0 -102
- 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 -476
- 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 -230
- package/src/cli/commands/extension.ts +0 -205
- package/src/cli/commands/init.ts +0 -396
- package/src/cli/commands/start.ts +0 -223
- package/src/cli/commands/topology.ts +0 -383
- package/src/cli/index.ts +0 -9
- package/src/cli/program.ts +0 -465
- package/src/cli/shared/config-io.ts +0 -277
- package/src/cli/shared/config-mutators.ts +0 -440
- package/src/cli/shared/config-schema.ts +0 -228
- package/src/cli/shared/config-types.ts +0 -121
- package/src/cli/shared/configure-sections.ts +0 -551
- package/src/cli/shared/discord-config.ts +0 -14
- package/src/cli/shared/init-catalog.ts +0 -189
- package/src/cli/shared/init-models-file.ts +0 -77
- package/src/cli/shared/runtime.ts +0 -33
- package/src/cli/shared/schema-prompts.ts +0 -414
- 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/doctor.test.ts +0 -120
- package/src/cli/tests/init-catalog.test.ts +0 -96
- package/src/cli/tests/program-options.test.ts +0 -113
- package/src/cli/tests/routing-config.test.ts +0 -209
- package/src/core/control-command.ts +0 -12
- package/src/core/dedup-store.ts +0 -103
- package/src/core/gateway.ts +0 -607
- package/src/core/routing.ts +0 -379
- 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 -318
- 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/tsconfig.json +0 -18
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extensions": {
|
|
3
|
-
"allowList": [
|
|
4
|
-
{
|
|
5
|
-
"package": "@dobby.ai/provider-pi",
|
|
6
|
-
"enabled": true
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
"package": "@dobby.ai/connector-discord",
|
|
10
|
-
"enabled": true
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"package": "@dobby.ai/connector-feishu",
|
|
14
|
-
"enabled": false
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"package": "@dobby.ai/provider-claude-cli",
|
|
18
|
-
"enabled": false
|
|
19
|
-
}
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
|
-
"providers": {
|
|
23
|
-
"default": "pi.main",
|
|
24
|
-
"items": {
|
|
25
|
-
"pi.main": {
|
|
26
|
-
"type": "provider.pi",
|
|
27
|
-
"provider": "custom-openai",
|
|
28
|
-
"model": "example-model",
|
|
29
|
-
"thinkingLevel": "off",
|
|
30
|
-
"modelsFile": "./models.custom.json"
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"connectors": {
|
|
35
|
-
"items": {
|
|
36
|
-
"discord.main": {
|
|
37
|
-
"type": "connector.discord",
|
|
38
|
-
"botName": "dobby-main",
|
|
39
|
-
"botToken": "REPLACE_WITH_DISCORD_BOT_TOKEN",
|
|
40
|
-
"reconnectStaleMs": 60000,
|
|
41
|
-
"reconnectCheckIntervalMs": 10000
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
"sandboxes": {
|
|
46
|
-
"default": "host.builtin",
|
|
47
|
-
"items": {}
|
|
48
|
-
},
|
|
49
|
-
"routes": {
|
|
50
|
-
"defaults": {
|
|
51
|
-
"provider": "pi.main",
|
|
52
|
-
"sandbox": "host.builtin",
|
|
53
|
-
"tools": "full",
|
|
54
|
-
"mentions": "required"
|
|
55
|
-
},
|
|
56
|
-
"items": {
|
|
57
|
-
"projectA": {
|
|
58
|
-
"projectRoot": "/Users/you/workspace/project-a"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
"bindings": {
|
|
63
|
-
"items": {
|
|
64
|
-
"discord.main.projectA": {
|
|
65
|
-
"connector": "discord.main",
|
|
66
|
-
"source": {
|
|
67
|
-
"type": "channel",
|
|
68
|
-
"id": "YOUR_DISCORD_CHANNEL_ID_A"
|
|
69
|
-
},
|
|
70
|
-
"route": "projectA"
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
"_examples": {
|
|
75
|
-
"connectors": {
|
|
76
|
-
"items": {
|
|
77
|
-
"feishu.main": {
|
|
78
|
-
"type": "connector.feishu",
|
|
79
|
-
"appId": "cli_xxx",
|
|
80
|
-
"appSecret": "REPLACE_WITH_FEISHU_APP_SECRET",
|
|
81
|
-
"domain": "feishu",
|
|
82
|
-
"botName": "dobby-feishu",
|
|
83
|
-
"messageFormat": "card_markdown",
|
|
84
|
-
"replyMode": "direct",
|
|
85
|
-
"cardTitle": "Dobby",
|
|
86
|
-
"downloadAttachments": true
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
"providers": {
|
|
91
|
-
"items": {
|
|
92
|
-
"claude-cli.main": {
|
|
93
|
-
"type": "provider.claude-cli",
|
|
94
|
-
"model": "claude-sonnet-4-5",
|
|
95
|
-
"maxTurns": 20,
|
|
96
|
-
"command": "claude",
|
|
97
|
-
"commandArgs": [],
|
|
98
|
-
"authMode": "auto",
|
|
99
|
-
"permissionMode": "bypassPermissions",
|
|
100
|
-
"streamVerbose": true
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
"routes": {
|
|
105
|
-
"items": {
|
|
106
|
-
"projectA": {
|
|
107
|
-
"provider": "claude-cli.main"
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
"bindings": {
|
|
112
|
-
"items": {
|
|
113
|
-
"feishu.main.projectA": {
|
|
114
|
-
"connector": "feishu.main",
|
|
115
|
-
"source": {
|
|
116
|
-
"type": "chat",
|
|
117
|
-
"id": "oc_xxx"
|
|
118
|
-
},
|
|
119
|
-
"route": "projectA"
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
"data": {
|
|
125
|
-
"rootDir": "./data",
|
|
126
|
-
"dedupTtlMs": 604800000
|
|
127
|
-
}
|
|
128
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"providers": {
|
|
3
|
-
"custom-openai": {
|
|
4
|
-
"baseUrl": "https://api.example.com/v1",
|
|
5
|
-
"api": "openai-completions",
|
|
6
|
-
"apiKey": "CUSTOM_PROVIDER_AUTH_TOKEN",
|
|
7
|
-
"models": [
|
|
8
|
-
{
|
|
9
|
-
"id": "example-model",
|
|
10
|
-
"name": "example-model",
|
|
11
|
-
"reasoning": false,
|
|
12
|
-
"input": [
|
|
13
|
-
"text"
|
|
14
|
-
],
|
|
15
|
-
"contextWindow": 128000,
|
|
16
|
-
"maxTokens": 8192,
|
|
17
|
-
"cost": {
|
|
18
|
-
"input": 0,
|
|
19
|
-
"output": 0,
|
|
20
|
-
"cacheRead": 0,
|
|
21
|
-
"cacheWrite": 0
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import test from "node:test";
|
|
3
|
-
import { EventForwarder } from "../event-forwarder.js";
|
|
4
|
-
class FakeConnector {
|
|
5
|
-
id = "connector.test";
|
|
6
|
-
platform = "test";
|
|
7
|
-
name = "test";
|
|
8
|
-
capabilities;
|
|
9
|
-
sent = [];
|
|
10
|
-
sentCount = 0;
|
|
11
|
-
constructor(updateStrategy, maxTextLength) {
|
|
12
|
-
this.capabilities = {
|
|
13
|
-
updateStrategy,
|
|
14
|
-
supportsThread: false,
|
|
15
|
-
supportsTyping: false,
|
|
16
|
-
supportsFileUpload: false,
|
|
17
|
-
...(maxTextLength !== undefined ? { maxTextLength } : {}),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
async start(_ctx) { }
|
|
21
|
-
async send(message) {
|
|
22
|
-
this.sent.push(message);
|
|
23
|
-
this.sentCount += 1;
|
|
24
|
-
return { messageId: `msg-${this.sentCount}` };
|
|
25
|
-
}
|
|
26
|
-
async stop() { }
|
|
27
|
-
}
|
|
28
|
-
const noopLogger = {
|
|
29
|
-
info: () => { },
|
|
30
|
-
warn: () => { },
|
|
31
|
-
error: () => { },
|
|
32
|
-
debug: () => { },
|
|
33
|
-
};
|
|
34
|
-
function createInbound() {
|
|
35
|
-
return {
|
|
36
|
-
connectorId: "connector.test",
|
|
37
|
-
platform: "test",
|
|
38
|
-
accountId: "bot",
|
|
39
|
-
routeId: "route.main",
|
|
40
|
-
routeChannelId: "route.main",
|
|
41
|
-
chatId: "chat.main",
|
|
42
|
-
messageId: "inbound-1",
|
|
43
|
-
userId: "user-1",
|
|
44
|
-
text: "hello",
|
|
45
|
-
attachments: [],
|
|
46
|
-
timestampMs: Date.now(),
|
|
47
|
-
raw: {},
|
|
48
|
-
isDirectMessage: false,
|
|
49
|
-
mentionedBot: true,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
function sleep(ms) {
|
|
53
|
-
return new Promise((resolve) => {
|
|
54
|
-
setTimeout(resolve, ms);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
test("edit strategy keeps create+update flow", async () => {
|
|
58
|
-
const connector = new FakeConnector("edit");
|
|
59
|
-
const forwarder = new EventForwarder(connector, createInbound(), null, noopLogger, {
|
|
60
|
-
updateIntervalMs: 1,
|
|
61
|
-
});
|
|
62
|
-
forwarder.handleEvent({ type: "message_complete", text: "hello" });
|
|
63
|
-
await sleep(5);
|
|
64
|
-
forwarder.handleEvent({ type: "message_complete", text: "hello world" });
|
|
65
|
-
await sleep(5);
|
|
66
|
-
await forwarder.finalize();
|
|
67
|
-
assert.equal(connector.sent.length, 2);
|
|
68
|
-
assert.equal(connector.sent[0]?.mode, "create");
|
|
69
|
-
assert.equal(connector.sent[0]?.text, "hello");
|
|
70
|
-
assert.equal(connector.sent[1]?.mode, "update");
|
|
71
|
-
assert.equal(connector.sent[1]?.text, "hello world");
|
|
72
|
-
});
|
|
73
|
-
test("final_only suppresses tool/status outbound messages", async () => {
|
|
74
|
-
const connector = new FakeConnector("final_only");
|
|
75
|
-
const forwarder = new EventForwarder(connector, createInbound(), null, noopLogger, {
|
|
76
|
-
toolMessageMode: "all",
|
|
77
|
-
updateIntervalMs: 1,
|
|
78
|
-
});
|
|
79
|
-
forwarder.handleEvent({ type: "status", message: "starting" });
|
|
80
|
-
forwarder.handleEvent({ type: "tool_start", toolName: "bash" });
|
|
81
|
-
forwarder.handleEvent({ type: "message_delta", delta: "hello " });
|
|
82
|
-
forwarder.handleEvent({ type: "tool_end", toolName: "bash", isError: false, output: "ok" });
|
|
83
|
-
forwarder.handleEvent({ type: "message_delta", delta: "world" });
|
|
84
|
-
await forwarder.finalize();
|
|
85
|
-
assert.equal(connector.sent.length, 1);
|
|
86
|
-
assert.equal(connector.sent[0]?.mode, "create");
|
|
87
|
-
assert.equal(connector.sent[0]?.text, "hello world");
|
|
88
|
-
});
|
|
89
|
-
test("final_only splits long final text into multiple create messages", async () => {
|
|
90
|
-
const connector = new FakeConnector("final_only", 5);
|
|
91
|
-
const forwarder = new EventForwarder(connector, createInbound(), null, noopLogger);
|
|
92
|
-
forwarder.handleEvent({ type: "message_complete", text: "12345678901" });
|
|
93
|
-
await forwarder.finalize();
|
|
94
|
-
assert.equal(connector.sent.length, 3);
|
|
95
|
-
assert.deepEqual(connector.sent.map((message) => ({ mode: message.mode, text: message.text })), [
|
|
96
|
-
{ mode: "create", text: "12345" },
|
|
97
|
-
{ mode: "create", text: "67890" },
|
|
98
|
-
{ mode: "create", text: "1" },
|
|
99
|
-
]);
|
|
100
|
-
});
|
|
101
|
-
test("append sends streaming increments as create messages only", async () => {
|
|
102
|
-
const connector = new FakeConnector("append");
|
|
103
|
-
const forwarder = new EventForwarder(connector, createInbound(), null, noopLogger, {
|
|
104
|
-
updateIntervalMs: 5,
|
|
105
|
-
});
|
|
106
|
-
forwarder.handleEvent({ type: "message_delta", delta: "hello" });
|
|
107
|
-
await sleep(20);
|
|
108
|
-
forwarder.handleEvent({ type: "message_delta", delta: " world" });
|
|
109
|
-
await sleep(20);
|
|
110
|
-
await forwarder.finalize();
|
|
111
|
-
assert.deepEqual(connector.sent.map((message) => message.mode), ["create", "create"]);
|
|
112
|
-
assert.deepEqual(connector.sent.map((message) => message.text), ["hello", " world"]);
|
|
113
|
-
});
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
const BLOCKED_OBJECT_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
2
|
-
/**
|
|
3
|
-
* Returns true when a path segment represents an array index.
|
|
4
|
-
*/
|
|
5
|
-
function isIndexSegment(raw) {
|
|
6
|
-
return /^[0-9]+$/.test(raw);
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Rejects dangerous object keys that could enable prototype pollution.
|
|
10
|
-
*/
|
|
11
|
-
function validatePathSegments(path) {
|
|
12
|
-
for (const segment of path) {
|
|
13
|
-
if (!isIndexSegment(segment) && BLOCKED_OBJECT_KEYS.has(segment)) {
|
|
14
|
-
throw new Error(`Invalid path segment: ${segment}`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Parses dot/bracket path syntax into normalized path segments.
|
|
20
|
-
*/
|
|
21
|
-
export function parsePath(rawPath) {
|
|
22
|
-
const trimmed = rawPath.trim();
|
|
23
|
-
if (!trimmed) {
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
const segments = [];
|
|
27
|
-
let current = "";
|
|
28
|
-
let index = 0;
|
|
29
|
-
while (index < trimmed.length) {
|
|
30
|
-
const char = trimmed[index];
|
|
31
|
-
if (char === "\\") {
|
|
32
|
-
const next = trimmed[index + 1];
|
|
33
|
-
if (next) {
|
|
34
|
-
current += next;
|
|
35
|
-
}
|
|
36
|
-
index += 2;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (char === ".") {
|
|
40
|
-
if (current) {
|
|
41
|
-
segments.push(current);
|
|
42
|
-
}
|
|
43
|
-
current = "";
|
|
44
|
-
index += 1;
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
if (char === "[") {
|
|
48
|
-
if (current) {
|
|
49
|
-
segments.push(current);
|
|
50
|
-
}
|
|
51
|
-
current = "";
|
|
52
|
-
const closeIndex = trimmed.indexOf("]", index);
|
|
53
|
-
if (closeIndex === -1) {
|
|
54
|
-
throw new Error(`Invalid path (missing ']'): ${rawPath}`);
|
|
55
|
-
}
|
|
56
|
-
const inside = trimmed.slice(index + 1, closeIndex).trim();
|
|
57
|
-
if (!inside) {
|
|
58
|
-
throw new Error(`Invalid path (empty '[]'): ${rawPath}`);
|
|
59
|
-
}
|
|
60
|
-
segments.push(inside);
|
|
61
|
-
index = closeIndex + 1;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
current += char;
|
|
65
|
-
index += 1;
|
|
66
|
-
}
|
|
67
|
-
if (current) {
|
|
68
|
-
segments.push(current);
|
|
69
|
-
}
|
|
70
|
-
const normalized = segments.map((segment) => segment.trim()).filter(Boolean);
|
|
71
|
-
validatePathSegments(normalized);
|
|
72
|
-
return normalized;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Safe own-property check wrapper.
|
|
76
|
-
*/
|
|
77
|
-
function hasOwnKey(value, key) {
|
|
78
|
-
return Object.prototype.hasOwnProperty.call(value, key);
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Reads a value from object/array structures by parsed path segments.
|
|
82
|
-
*/
|
|
83
|
-
export function getAtPath(root, path) {
|
|
84
|
-
let current = root;
|
|
85
|
-
for (const segment of path) {
|
|
86
|
-
if (!current || typeof current !== "object") {
|
|
87
|
-
return { found: false };
|
|
88
|
-
}
|
|
89
|
-
if (Array.isArray(current)) {
|
|
90
|
-
if (!isIndexSegment(segment)) {
|
|
91
|
-
return { found: false };
|
|
92
|
-
}
|
|
93
|
-
const index = Number.parseInt(segment, 10);
|
|
94
|
-
if (!Number.isFinite(index) || index < 0 || index >= current.length) {
|
|
95
|
-
return { found: false };
|
|
96
|
-
}
|
|
97
|
-
current = current[index];
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
const record = current;
|
|
101
|
-
if (!hasOwnKey(record, segment)) {
|
|
102
|
-
return { found: false };
|
|
103
|
-
}
|
|
104
|
-
current = record[segment];
|
|
105
|
-
}
|
|
106
|
-
return { found: true, value: current };
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Sets a value at path, creating intermediate objects/arrays as needed.
|
|
110
|
-
*/
|
|
111
|
-
export function setAtPath(root, path, value) {
|
|
112
|
-
if (path.length === 0) {
|
|
113
|
-
throw new Error("Path is empty.");
|
|
114
|
-
}
|
|
115
|
-
let current = root;
|
|
116
|
-
for (let i = 0; i < path.length - 1; i += 1) {
|
|
117
|
-
const segment = path[i];
|
|
118
|
-
const next = path[i + 1];
|
|
119
|
-
const nextIsIndex = Boolean(next && isIndexSegment(next));
|
|
120
|
-
if (Array.isArray(current)) {
|
|
121
|
-
if (!isIndexSegment(segment)) {
|
|
122
|
-
throw new Error(`Expected numeric index for array segment '${segment}'`);
|
|
123
|
-
}
|
|
124
|
-
const index = Number.parseInt(segment, 10);
|
|
125
|
-
const existing = current[index];
|
|
126
|
-
if (!existing || typeof existing !== "object") {
|
|
127
|
-
current[index] = nextIsIndex ? [] : {};
|
|
128
|
-
}
|
|
129
|
-
current = current[index];
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (!current || typeof current !== "object") {
|
|
133
|
-
throw new Error(`Cannot traverse into '${segment}' (not an object)`);
|
|
134
|
-
}
|
|
135
|
-
const record = current;
|
|
136
|
-
const existing = hasOwnKey(record, segment) ? record[segment] : undefined;
|
|
137
|
-
if (!existing || typeof existing !== "object") {
|
|
138
|
-
record[segment] = nextIsIndex ? [] : {};
|
|
139
|
-
}
|
|
140
|
-
current = record[segment];
|
|
141
|
-
}
|
|
142
|
-
const tail = path[path.length - 1];
|
|
143
|
-
if (Array.isArray(current)) {
|
|
144
|
-
if (!isIndexSegment(tail)) {
|
|
145
|
-
throw new Error(`Expected numeric index for array segment '${tail}'`);
|
|
146
|
-
}
|
|
147
|
-
const index = Number.parseInt(tail, 10);
|
|
148
|
-
current[index] = value;
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
if (!current || typeof current !== "object") {
|
|
152
|
-
throw new Error(`Cannot set '${tail}' (parent is not an object)`);
|
|
153
|
-
}
|
|
154
|
-
current[tail] = value;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Removes a value at path and returns whether the target existed.
|
|
158
|
-
*/
|
|
159
|
-
export function unsetAtPath(root, path) {
|
|
160
|
-
if (path.length === 0) {
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
let current = root;
|
|
164
|
-
for (let i = 0; i < path.length - 1; i += 1) {
|
|
165
|
-
const segment = path[i];
|
|
166
|
-
if (!current || typeof current !== "object") {
|
|
167
|
-
return false;
|
|
168
|
-
}
|
|
169
|
-
if (Array.isArray(current)) {
|
|
170
|
-
if (!isIndexSegment(segment)) {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
const index = Number.parseInt(segment, 10);
|
|
174
|
-
if (!Number.isFinite(index) || index < 0 || index >= current.length) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
current = current[index];
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
const record = current;
|
|
181
|
-
if (!hasOwnKey(record, segment)) {
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
|
-
current = record[segment];
|
|
185
|
-
}
|
|
186
|
-
const tail = path[path.length - 1];
|
|
187
|
-
if (Array.isArray(current)) {
|
|
188
|
-
if (!isIndexSegment(tail)) {
|
|
189
|
-
return false;
|
|
190
|
-
}
|
|
191
|
-
const index = Number.parseInt(tail, 10);
|
|
192
|
-
if (!Number.isFinite(index) || index < 0 || index >= current.length) {
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
current.splice(index, 1);
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
if (!current || typeof current !== "object") {
|
|
199
|
-
return false;
|
|
200
|
-
}
|
|
201
|
-
const record = current;
|
|
202
|
-
if (!hasOwnKey(record, tail)) {
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
delete record[tail];
|
|
206
|
-
return true;
|
|
207
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { access, mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
const DEFAULT_PROVIDER_PI_MODELS_FILE = "./models.custom.json";
|
|
5
|
-
const PROVIDER_PI_MODELS_TEMPLATE = {
|
|
6
|
-
providers: {
|
|
7
|
-
"custom-openai": {
|
|
8
|
-
baseUrl: "https://api.example.com/v1",
|
|
9
|
-
api: "openai-completions",
|
|
10
|
-
apiKey: "CUSTOM_PROVIDER_AUTH_TOKEN",
|
|
11
|
-
models: [
|
|
12
|
-
{
|
|
13
|
-
id: "example-model",
|
|
14
|
-
name: "example-model",
|
|
15
|
-
reasoning: false,
|
|
16
|
-
input: ["text"],
|
|
17
|
-
contextWindow: 128000,
|
|
18
|
-
maxTokens: 8192,
|
|
19
|
-
cost: {
|
|
20
|
-
input: 0,
|
|
21
|
-
output: 0,
|
|
22
|
-
cacheRead: 0,
|
|
23
|
-
cacheWrite: 0,
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
function expandHome(value) {
|
|
31
|
-
if (value === "~") {
|
|
32
|
-
return homedir();
|
|
33
|
-
}
|
|
34
|
-
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
35
|
-
return resolve(homedir(), value.slice(2));
|
|
36
|
-
}
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
async function fileExists(path) {
|
|
40
|
-
try {
|
|
41
|
-
await access(path);
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function resolveModelsFilePath(configPath, value) {
|
|
49
|
-
const configDir = dirname(resolve(configPath));
|
|
50
|
-
const resolvedValue = expandHome(value && value.trim().length > 0 ? value.trim() : DEFAULT_PROVIDER_PI_MODELS_FILE);
|
|
51
|
-
return isAbsolute(resolvedValue) ? resolve(resolvedValue) : resolve(configDir, resolvedValue);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Creates provider.pi models file only when missing.
|
|
55
|
-
*/
|
|
56
|
-
export async function ensureProviderPiModelsFile(configPath, providerConfig) {
|
|
57
|
-
const modelsFile = typeof providerConfig.modelsFile === "string" ? providerConfig.modelsFile : undefined;
|
|
58
|
-
const targetPath = resolveModelsFilePath(configPath, modelsFile);
|
|
59
|
-
if (await fileExists(targetPath)) {
|
|
60
|
-
return { created: false, path: targetPath };
|
|
61
|
-
}
|
|
62
|
-
await mkdir(dirname(targetPath), { recursive: true });
|
|
63
|
-
await writeFile(targetPath, `${JSON.stringify(PROVIDER_PI_MODELS_TEMPLATE, null, 2)}\n`, "utf-8");
|
|
64
|
-
return { created: true, path: targetPath };
|
|
65
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -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
|
-
});
|