@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.
Files changed (76) hide show
  1. package/.env.example +0 -1
  2. package/AGENTS.md +7 -7
  3. package/README.md +64 -32
  4. package/config/gateway.example.json +10 -6
  5. package/dist/plugins/connector-discord/src/mapper.js +75 -0
  6. package/dist/src/cli/commands/doctor.js +81 -2
  7. package/dist/src/cli/commands/extension.js +3 -1
  8. package/dist/src/cli/commands/init.js +43 -173
  9. package/dist/src/cli/commands/topology.js +38 -14
  10. package/dist/src/cli/program.js +15 -131
  11. package/dist/src/cli/shared/config-io.js +3 -31
  12. package/dist/src/cli/shared/config-mutators.js +33 -9
  13. package/dist/src/cli/shared/configure-sections.js +52 -12
  14. package/dist/src/cli/shared/init-catalog.js +89 -46
  15. package/dist/src/cli/shared/local-extension-specs.js +85 -0
  16. package/dist/src/cli/shared/schema-prompts.js +26 -2
  17. package/dist/src/cli/tests/config-io.test.js +5 -5
  18. package/dist/src/cli/tests/discord-mapper.test.js +90 -0
  19. package/dist/src/cli/tests/doctor.test.js +145 -0
  20. package/dist/src/cli/tests/init-catalog.test.js +108 -61
  21. package/dist/src/cli/tests/program-options.test.js +14 -28
  22. package/dist/src/cli/tests/routing-config.test.js +59 -4
  23. package/dist/src/core/gateway.js +3 -1
  24. package/dist/src/core/routing.js +53 -38
  25. package/dist/src/main.js +0 -0
  26. package/dist/src/shared/dobby-repo.js +40 -0
  27. package/docs/RUNBOOK.md +28 -27
  28. package/package.json +3 -2
  29. package/plugins/connector-discord/package-lock.json +2 -2
  30. package/plugins/connector-discord/package.json +1 -1
  31. package/plugins/connector-discord/src/connector.ts +0 -5
  32. package/plugins/connector-discord/src/mapper.ts +3 -4
  33. package/plugins/connector-feishu/package-lock.json +2 -2
  34. package/plugins/connector-feishu/package.json +1 -1
  35. package/plugins/plugin-sdk/package-lock.json +2 -2
  36. package/plugins/plugin-sdk/package.json +1 -1
  37. package/plugins/provider-claude/package-lock.json +2 -2
  38. package/plugins/provider-claude/package.json +1 -1
  39. package/plugins/provider-claude-cli/package-lock.json +2 -2
  40. package/plugins/provider-claude-cli/package.json +1 -1
  41. package/plugins/provider-pi/package-lock.json +2 -2
  42. package/plugins/provider-pi/package.json +1 -1
  43. package/plugins/provider-pi/src/contribution.ts +139 -9
  44. package/src/cli/commands/doctor.ts +103 -2
  45. package/src/cli/commands/extension.ts +3 -1
  46. package/src/cli/commands/init.ts +45 -230
  47. package/src/cli/commands/topology.ts +48 -16
  48. package/src/cli/program.ts +16 -167
  49. package/src/cli/shared/config-io.ts +3 -35
  50. package/src/cli/shared/config-mutators.ts +39 -9
  51. package/src/cli/shared/config-types.ts +10 -2
  52. package/src/cli/shared/configure-sections.ts +55 -11
  53. package/src/cli/shared/init-catalog.ts +126 -66
  54. package/src/cli/shared/local-extension-specs.ts +108 -0
  55. package/src/cli/shared/schema-prompts.ts +30 -1
  56. package/src/cli/tests/config-io.test.ts +5 -5
  57. package/src/cli/tests/discord-mapper.test.ts +128 -0
  58. package/src/cli/tests/doctor.test.ts +149 -0
  59. package/src/cli/tests/init-catalog.test.ts +112 -64
  60. package/src/cli/tests/program-options.test.ts +14 -32
  61. package/src/cli/tests/routing-config.test.ts +76 -4
  62. package/src/core/gateway.ts +3 -1
  63. package/src/core/routing.ts +70 -45
  64. package/src/core/types.ts +8 -2
  65. package/src/shared/dobby-repo.ts +48 -0
  66. package/config/models.custom.example.json +0 -27
  67. package/dist/src/agent/tests/event-forwarder.test.js +0 -113
  68. package/dist/src/cli/shared/config-path.js +0 -207
  69. package/dist/src/cli/shared/init-models-file.js +0 -65
  70. package/dist/src/cli/shared/presets.js +0 -86
  71. package/dist/src/cli/tests/config-path.test.js +0 -21
  72. package/dist/src/cli/tests/discord-config.test.js +0 -23
  73. package/dist/src/cli/tests/presets.test.js +0 -41
  74. package/dist/src/cli/tests/routing-legacy.test.js +0 -191
  75. package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
  76. package/src/cli/shared/init-models-file.ts +0 -77
@@ -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,21 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
- import { getAtPath, parsePath, setAtPath, unsetAtPath } from "../shared/config-path.js";
4
- test("parsePath handles dot and bracket notation", () => {
5
- assert.deepEqual(parsePath("routing.routes.main.projectRoot"), ["routing", "routes", "main", "projectRoot"]);
6
- assert.deepEqual(parsePath("connectors.instances[discord.main].config.botChannelMap[12345]"), ["connectors", "instances", "discord.main", "config", "botChannelMap", "12345"]);
7
- });
8
- test("setAtPath and getAtPath support nested objects", () => {
9
- const payload = {};
10
- setAtPath(payload, parsePath("a.b.c"), 42);
11
- const read = getAtPath(payload, parsePath("a.b.c"));
12
- assert.equal(read.found, true);
13
- assert.equal(read.value, 42);
14
- });
15
- test("unsetAtPath removes keys", () => {
16
- const payload = { a: { b: { c: 1 } } };
17
- const removed = unsetAtPath(payload, parsePath("a.b.c"));
18
- assert.equal(removed, true);
19
- const read = getAtPath(payload, parsePath("a.b.c"));
20
- assert.equal(read.found, false);
21
- });
@@ -1,23 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { normalizeDiscordBotChannelMap } from "../shared/discord-config.js";
4
- test("normalizeDiscordBotChannelMap keeps valid channel->route entries", () => {
5
- const normalized = normalizeDiscordBotChannelMap({
6
- "123": "projectA",
7
- "456": "projectB",
8
- });
9
- assert.deepEqual(normalized, {
10
- "123": "projectA",
11
- "456": "projectB",
12
- });
13
- });
14
- test("normalizeDiscordBotChannelMap drops invalid values", () => {
15
- const normalized = normalizeDiscordBotChannelMap({
16
- "123": "projectA",
17
- "456": "",
18
- "789": 1,
19
- });
20
- assert.deepEqual(normalized, {
21
- "123": "projectA",
22
- });
23
- });
@@ -1,41 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { createPresetConfig } from "../shared/presets.js";
4
- test("createPresetConfig wires explicit Discord bot config for discord-pi", () => {
5
- const preset = createPresetConfig("discord-pi", {
6
- routeId: "main",
7
- projectRoot: "/tmp/project",
8
- allowAllMessages: false,
9
- botName: "dobby-main",
10
- botToken: "token-abc",
11
- channelId: "123",
12
- });
13
- assert.deepEqual(preset.connectorConfig, {
14
- botName: "dobby-main",
15
- botToken: "token-abc",
16
- botChannelMap: {
17
- "123": "main",
18
- },
19
- reconnectStaleMs: 60_000,
20
- reconnectCheckIntervalMs: 10_000,
21
- });
22
- });
23
- test("createPresetConfig wires explicit Discord bot config for discord-claude-cli", () => {
24
- const preset = createPresetConfig("discord-claude-cli", {
25
- routeId: "support",
26
- projectRoot: "/tmp/project",
27
- allowAllMessages: true,
28
- botName: "ops-bot",
29
- botToken: "token-xyz",
30
- channelId: "999",
31
- });
32
- assert.deepEqual(preset.connectorConfig, {
33
- botName: "ops-bot",
34
- botToken: "token-xyz",
35
- botChannelMap: {
36
- "999": "support",
37
- },
38
- reconnectStaleMs: 60_000,
39
- reconnectCheckIntervalMs: 10_000,
40
- });
41
- });
@@ -1,191 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import test from "node:test";
6
- import { loadGatewayConfig } from "../../core/routing.js";
7
- async function writeTempConfig(payload) {
8
- const dir = await mkdtemp(join(tmpdir(), "dobby-routing-"));
9
- const configPath = join(dir, "gateway.json");
10
- await writeFile(configPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
11
- return configPath;
12
- }
13
- function validConfig() {
14
- return {
15
- extensions: { allowList: [] },
16
- providers: {
17
- default: "pi.main",
18
- items: {
19
- "pi.main": {
20
- type: "provider.pi",
21
- },
22
- },
23
- },
24
- connectors: {
25
- items: {
26
- "discord.main": {
27
- type: "connector.discord",
28
- botName: "dobby-main",
29
- botToken: "token",
30
- },
31
- },
32
- },
33
- sandboxes: {
34
- default: "host.builtin",
35
- items: {},
36
- },
37
- routes: {
38
- defaults: {
39
- provider: "pi.main",
40
- sandbox: "host.builtin",
41
- tools: "full",
42
- mentions: "required",
43
- },
44
- items: {
45
- main: {
46
- projectRoot: "./workspace/project-a",
47
- systemPromptFile: "./prompts/main.md",
48
- },
49
- },
50
- },
51
- bindings: {
52
- items: {
53
- "discord.main.main": {
54
- connector: "discord.main",
55
- source: {
56
- type: "channel",
57
- id: "123",
58
- },
59
- route: "main",
60
- },
61
- },
62
- },
63
- data: {
64
- rootDir: "./data",
65
- dedupTtlMs: 604800000,
66
- },
67
- };
68
- }
69
- test("loadGatewayConfig applies route defaults and resolves relative paths", async () => {
70
- const payload = validConfig();
71
- const configPath = await writeTempConfig(payload);
72
- try {
73
- const loaded = await loadGatewayConfig(configPath);
74
- const configDir = dirname(configPath);
75
- assert.equal(loaded.providers.default, "pi.main");
76
- assert.deepEqual(loaded.routes.defaults, {
77
- provider: "pi.main",
78
- sandbox: "host.builtin",
79
- tools: "full",
80
- mentions: "required",
81
- });
82
- assert.deepEqual(loaded.routes.items.main, {
83
- projectRoot: join(configDir, "workspace/project-a"),
84
- systemPromptFile: join(configDir, "prompts/main.md"),
85
- provider: "pi.main",
86
- sandbox: "host.builtin",
87
- tools: "full",
88
- mentions: "required",
89
- });
90
- assert.equal(loaded.data.rootDir, join(configDir, "data"));
91
- assert.deepEqual(loaded.bindings.items["discord.main.main"], {
92
- connector: "discord.main",
93
- source: {
94
- type: "channel",
95
- id: "123",
96
- },
97
- route: "main",
98
- });
99
- }
100
- finally {
101
- await rm(dirname(configPath), { recursive: true, force: true });
102
- }
103
- });
104
- test("loadGatewayConfig fails fast on legacy top-level routing", async () => {
105
- const payload = validConfig();
106
- payload.routing = {
107
- routes: {
108
- main: {
109
- projectRoot: process.cwd(),
110
- },
111
- },
112
- };
113
- const configPath = await writeTempConfig(payload);
114
- try {
115
- await assert.rejects(loadGatewayConfig(configPath), /top-level field 'routing'/);
116
- }
117
- finally {
118
- await rm(dirname(configPath), { recursive: true, force: true });
119
- }
120
- });
121
- test("loadGatewayConfig fails fast on legacy botTokenEnv", async () => {
122
- const payload = validConfig();
123
- payload.connectors = {
124
- items: {
125
- "discord.main": {
126
- type: "connector.discord",
127
- botName: "dobby-main",
128
- botTokenEnv: "DISCORD_BOT_TOKEN",
129
- },
130
- },
131
- };
132
- const configPath = await writeTempConfig(payload);
133
- try {
134
- await assert.rejects(loadGatewayConfig(configPath), /botTokenEnv/);
135
- }
136
- finally {
137
- await rm(dirname(configPath), { recursive: true, force: true });
138
- }
139
- });
140
- test("loadGatewayConfig fails fast on legacy connector route maps", async () => {
141
- const payload = validConfig();
142
- payload.connectors = {
143
- items: {
144
- "discord.main": {
145
- type: "connector.discord",
146
- botName: "dobby-main",
147
- botToken: "token",
148
- botChannelMap: {
149
- "123": "main",
150
- },
151
- },
152
- },
153
- };
154
- const configPath = await writeTempConfig(payload);
155
- try {
156
- await assert.rejects(loadGatewayConfig(configPath), /botChannelMap/);
157
- }
158
- finally {
159
- await rm(dirname(configPath), { recursive: true, force: true });
160
- }
161
- });
162
- test("loadGatewayConfig fails fast on duplicate binding sources", async () => {
163
- const payload = validConfig();
164
- payload.bindings = {
165
- items: {
166
- "discord.main.main": {
167
- connector: "discord.main",
168
- source: {
169
- type: "channel",
170
- id: "123",
171
- },
172
- route: "main",
173
- },
174
- "discord.main.duplicate": {
175
- connector: "discord.main",
176
- source: {
177
- type: "channel",
178
- id: "123",
179
- },
180
- route: "main",
181
- },
182
- },
183
- };
184
- const configPath = await writeTempConfig(payload);
185
- try {
186
- await assert.rejects(loadGatewayConfig(configPath), /duplicates source 'discord\.main:channel:123'/);
187
- }
188
- finally {
189
- await rm(dirname(configPath), { recursive: true, force: true });
190
- }
191
- });
@@ -1,167 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { Gateway } from "../gateway.js";
4
- class FakeConnector {
5
- id = "connector.test";
6
- platform = "test";
7
- name = "test";
8
- capabilities;
9
- sent = [];
10
- sentCount = 0;
11
- constructor(updateStrategy) {
12
- this.capabilities = {
13
- updateStrategy,
14
- supportsThread: false,
15
- supportsTyping: false,
16
- supportsFileUpload: false,
17
- };
18
- }
19
- async start(_ctx) { }
20
- async send(message) {
21
- this.sent.push(message);
22
- this.sentCount += 1;
23
- return { messageId: `msg-${this.sentCount}` };
24
- }
25
- async stop() { }
26
- }
27
- class ThrowingRuntime {
28
- listener = null;
29
- subscribe(listener) {
30
- this.listener = listener;
31
- return () => {
32
- this.listener = null;
33
- };
34
- }
35
- async prompt() {
36
- this.listener?.({ type: "message_complete", text: "partial output" });
37
- throw new Error("boom");
38
- }
39
- async abort() { }
40
- dispose() {
41
- this.listener = null;
42
- }
43
- }
44
- const route = {
45
- routeId: "route.main",
46
- profile: {
47
- projectRoot: "/tmp/project",
48
- tools: "readonly",
49
- allowMentionsOnly: false,
50
- maxConcurrentTurns: 1,
51
- },
52
- };
53
- const noopLogger = {
54
- info: () => { },
55
- warn: () => { },
56
- error: () => { },
57
- debug: () => { },
58
- };
59
- const fakeExecutor = {
60
- exec: async () => ({ stdout: "", stderr: "", code: 0, killed: false }),
61
- spawn: () => {
62
- throw new Error("spawn should not be called in this test");
63
- },
64
- close: async () => { },
65
- };
66
- const fakeDedupStore = {
67
- has: () => false,
68
- add: () => { },
69
- load: async () => { },
70
- startAutoFlush: () => { },
71
- stopAutoFlush: () => { },
72
- flush: async () => { },
73
- };
74
- const fakeRuntimeRegistry = {
75
- getOrCreate: async () => {
76
- throw new Error("getOrCreate should not be called for scheduled stateless runs");
77
- },
78
- enqueue: async () => {
79
- throw new Error("enqueue should not be called for scheduled stateless runs");
80
- },
81
- abort: async () => false,
82
- closeAll: async () => { },
83
- };
84
- function buildGateway(updateStrategy) {
85
- const connector = new FakeConnector(updateStrategy);
86
- const provider = {
87
- id: "provider.main",
88
- createRuntime: async () => new ThrowingRuntime(),
89
- };
90
- const config = {
91
- extensions: { allowList: [] },
92
- providers: {
93
- defaultProviderId: "provider.main",
94
- instances: {},
95
- },
96
- connectors: {
97
- instances: {},
98
- },
99
- sandboxes: {
100
- defaultSandboxId: "sandbox.main",
101
- instances: {},
102
- },
103
- routing: {
104
- routes: {
105
- [route.routeId]: route.profile,
106
- },
107
- },
108
- data: {
109
- rootDir: "/tmp",
110
- sessionsDir: "/tmp/sessions",
111
- attachmentsDir: "/tmp/attachments",
112
- logsDir: "/tmp/logs",
113
- stateDir: "/tmp/state",
114
- dedupTtlMs: 60_000,
115
- },
116
- };
117
- const routeResolver = {
118
- resolve: (routeId) => (routeId === route.routeId ? route : null),
119
- };
120
- const gateway = new Gateway({
121
- config,
122
- connectors: [connector],
123
- providers: new Map([["provider.main", provider]]),
124
- executors: new Map([["sandbox.main", fakeExecutor]]),
125
- routeResolver,
126
- dedupStore: fakeDedupStore,
127
- runtimeRegistry: fakeRuntimeRegistry,
128
- logger: noopLogger,
129
- });
130
- return { gateway, connector };
131
- }
132
- async function runScheduledAndCollect(updateStrategy) {
133
- const { gateway, connector } = buildGateway(updateStrategy);
134
- await gateway.handleScheduled({
135
- jobId: "job-1",
136
- runId: `run-${updateStrategy}`,
137
- connectorId: connector.id,
138
- routeId: route.routeId,
139
- channelId: "chat-1",
140
- prompt: "hello",
141
- });
142
- return connector;
143
- }
144
- test("gateway error path uses update for edit strategy", async () => {
145
- const connector = await runScheduledAndCollect("edit");
146
- assert.equal(connector.sent.length, 2);
147
- assert.equal(connector.sent[0]?.mode, "create");
148
- assert.equal(connector.sent[0]?.text, "partial output");
149
- assert.equal(connector.sent[1]?.mode, "update");
150
- assert.equal(connector.sent[1]?.targetMessageId, "msg-1");
151
- assert.equal(connector.sent[1]?.text, "Error: boom");
152
- });
153
- test("gateway error path uses create for append strategy", async () => {
154
- const connector = await runScheduledAndCollect("append");
155
- assert.equal(connector.sent.length, 2);
156
- assert.equal(connector.sent[0]?.mode, "create");
157
- assert.equal(connector.sent[0]?.text, "partial output");
158
- assert.equal(connector.sent[1]?.mode, "create");
159
- assert.equal(connector.sent[1]?.replyToMessageId, "msg-1");
160
- assert.equal(connector.sent[1]?.text, "Error: boom");
161
- });
162
- test("gateway error path uses create for final_only strategy", async () => {
163
- const connector = await runScheduledAndCollect("final_only");
164
- assert.equal(connector.sent.length, 1);
165
- assert.equal(connector.sent[0]?.mode, "create");
166
- assert.equal(connector.sent[0]?.text, "Error: boom");
167
- });
@@ -1,77 +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
-
5
- const DEFAULT_PROVIDER_PI_MODELS_FILE = "./models.custom.json";
6
-
7
- const PROVIDER_PI_MODELS_TEMPLATE = {
8
- providers: {
9
- "custom-openai": {
10
- baseUrl: "https://api.example.com/v1",
11
- api: "openai-completions",
12
- apiKey: "CUSTOM_PROVIDER_AUTH_TOKEN",
13
- models: [
14
- {
15
- id: "example-model",
16
- name: "example-model",
17
- reasoning: false,
18
- input: ["text"],
19
- contextWindow: 128000,
20
- maxTokens: 8192,
21
- cost: {
22
- input: 0,
23
- output: 0,
24
- cacheRead: 0,
25
- cacheWrite: 0,
26
- },
27
- },
28
- ],
29
- },
30
- },
31
- } as const;
32
-
33
- function expandHome(value: string): string {
34
- if (value === "~") {
35
- return homedir();
36
- }
37
- if (value.startsWith("~/") || value.startsWith("~\\")) {
38
- return resolve(homedir(), value.slice(2));
39
- }
40
-
41
- return value;
42
- }
43
-
44
- async function fileExists(path: string): Promise<boolean> {
45
- try {
46
- await access(path);
47
- return true;
48
- } catch {
49
- return false;
50
- }
51
- }
52
-
53
- function resolveModelsFilePath(configPath: string, value: string | undefined): string {
54
- const configDir = dirname(resolve(configPath));
55
- const resolvedValue = expandHome(value && value.trim().length > 0 ? value.trim() : DEFAULT_PROVIDER_PI_MODELS_FILE);
56
- return isAbsolute(resolvedValue) ? resolve(resolvedValue) : resolve(configDir, resolvedValue);
57
- }
58
-
59
- /**
60
- * Creates provider.pi models file only when missing.
61
- */
62
- export async function ensureProviderPiModelsFile(
63
- configPath: string,
64
- providerConfig: Record<string, unknown>,
65
- ): Promise<{ created: boolean; path: string }> {
66
- const modelsFile = typeof providerConfig.modelsFile === "string" ? providerConfig.modelsFile : undefined;
67
- const targetPath = resolveModelsFilePath(configPath, modelsFile);
68
-
69
- if (await fileExists(targetPath)) {
70
- return { created: false, path: targetPath };
71
- }
72
-
73
- await mkdir(dirname(targetPath), { recursive: true });
74
- await writeFile(targetPath, `${JSON.stringify(PROVIDER_PI_MODELS_TEMPLATE, null, 2)}\n`, "utf-8");
75
- return { created: true, path: targetPath };
76
- }
77
-