octto 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -104,12 +104,32 @@ Optional `~/.config/opencode/octto.json`:
104
104
 
105
105
  ```json
106
106
  {
107
+ "port": 3000,
107
108
  "agents": {
108
109
  "probe": { "model": "anthropic/claude-sonnet-4" }
109
110
  }
110
111
  }
111
112
  ```
112
113
 
114
+ ### Options
115
+
116
+ | Option | Type | Default | Description |
117
+ |--------|------|---------|-------------|
118
+ | `port` | number | `0` (random) | Fixed port for the browser UI server |
119
+ | `agents` | object | - | Override agent models/settings |
120
+
121
+ ### Environment Variables
122
+
123
+ | Variable | Description |
124
+ |----------|-------------|
125
+ | `OCTTO_PORT` | Override port (takes precedence over config file) |
126
+
127
+ For Docker workflows, set a fixed port:
128
+
129
+ ```bash
130
+ OCTTO_PORT=3000 opencode
131
+ ```
132
+
113
133
  ## Development
114
134
 
115
135
  ```bash
@@ -1,3 +1,3 @@
1
- export { loadCustomConfig } from "./loader";
2
- export type { AgentOverride, OcttoConfig } from "./schema";
3
- export { AgentOverrideSchema, OcttoConfigSchema } from "./schema";
1
+ export type { AgentOverride, CustomConfig, OcttoConfig } from "./loader";
2
+ export { loadCustomConfig, resolvePort } from "./loader";
3
+ export { AgentOverrideSchema, OcttoConfigSchema, PortSchema } from "./schema";
@@ -1,8 +1,17 @@
1
1
  import type { AgentConfig } from "@opencode-ai/sdk";
2
- import type { AGENTS } from "@/agents";
2
+ import { AGENTS } from "@/agents";
3
3
  export type { AgentOverride, OcttoConfig } from "./schema";
4
+ /**
5
+ * Resolve port from environment variable or config.
6
+ * Priority: OCTTO_PORT env var > config port > default (0 = random)
7
+ */
8
+ export declare function resolvePort(configPort?: number): number;
9
+ export interface CustomConfig {
10
+ agents: Record<AGENTS, AgentConfig>;
11
+ port: number;
12
+ }
4
13
  /**
5
14
  * Load user configuration and merge with plugin agents.
6
- * Returns merged agent configs with user overrides applied.
15
+ * Returns merged agent configs with user overrides applied, and resolved port.
7
16
  */
8
- export declare function loadCustomConfig(agents: Record<AGENTS, AgentConfig>, configDir?: string): Promise<Record<AGENTS, AgentConfig>>;
17
+ export declare function loadCustomConfig(agents: Record<AGENTS, AgentConfig>, configDir?: string): Promise<CustomConfig>;
@@ -38,6 +38,7 @@ export declare const AgentOverrideSchema: Omit<v.ObjectSchema<{
38
38
  readonly issue: v.ObjectIssue | v.StringIssue | v.NumberIssue | v.MinValueIssue<number, 0> | v.MaxValueIssue<number, 2> | v.IntegerIssue<number> | v.MinValueIssue<number, 1>;
39
39
  } | undefined;
40
40
  };
41
+ export declare const PortSchema: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>, v.MaxValueAction<number, 65535, undefined>]>;
41
42
  export declare const OcttoConfigSchema: v.ObjectSchema<{
42
43
  readonly agents: v.OptionalSchema<v.RecordSchema<v.EnumSchema<typeof AGENTS, undefined>, Omit<v.ObjectSchema<{
43
44
  readonly model: v.StringSchema<undefined>;
@@ -77,6 +78,7 @@ export declare const OcttoConfigSchema: v.ObjectSchema<{
77
78
  readonly issue: v.ObjectIssue | v.StringIssue | v.NumberIssue | v.MinValueIssue<number, 0> | v.MaxValueIssue<number, 2> | v.IntegerIssue<number> | v.MinValueIssue<number, 1>;
78
79
  } | undefined;
79
80
  }, undefined>, undefined>;
81
+ readonly port: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>, v.MaxValueAction<number, 65535, undefined>]>, undefined>;
80
82
  }, undefined>;
81
83
  export type AgentOverride = v.InferOutput<typeof AgentOverrideSchema>;
82
84
  export type OcttoConfig = v.InferOutput<typeof OcttoConfigSchema>;
package/dist/index.js CHANGED
@@ -795,36 +795,93 @@ var AgentOverrideSchema = partial(object({
795
795
  temperature: pipe(number(), minValue(0), maxValue(2)),
796
796
  maxSteps: pipe(number(), integer(), minValue(1))
797
797
  }));
798
+ var PortSchema = pipe(number(), integer(), minValue(0), maxValue(65535));
798
799
  var OcttoConfigSchema = object({
799
- agents: optional(record(enum_(AGENTS), AgentOverrideSchema))
800
+ agents: optional(record(enum_(AGENTS), AgentOverrideSchema)),
801
+ port: optional(PortSchema)
800
802
  });
801
803
 
802
804
  // src/config/loader.ts
805
+ var OCTTO_PORT_ENV = "OCTTO_PORT";
806
+ var DEFAULT_PORT = 0;
807
+ function resolvePort(configPort) {
808
+ const envValue = process.env[OCTTO_PORT_ENV];
809
+ if (envValue !== undefined) {
810
+ const parsed = Number(envValue);
811
+ if (Number.isInteger(parsed) && parsed >= 0 && parsed <= 65535) {
812
+ return parsed;
813
+ }
814
+ }
815
+ return configPort ?? DEFAULT_PORT;
816
+ }
817
+ var VALID_AGENT_NAMES = Object.values(AGENTS);
818
+ function formatValidationErrors(issues) {
819
+ return issues.map((issue) => {
820
+ const path = issue.path?.map((p) => p.key).join(".") ?? "root";
821
+ return ` - ${path}: ${issue.message}`;
822
+ }).join(`
823
+ `);
824
+ }
803
825
  async function load(configDir) {
804
826
  const baseDir = configDir ?? join(homedir(), ".config", "opencode");
805
827
  const configPath = join(baseDir, "octto.json");
828
+ let parsed;
806
829
  try {
807
830
  const content = await readFile(configPath, "utf-8");
808
- const parsed = JSON.parse(content);
809
- const result = safeParse(OcttoConfigSchema, parsed);
810
- if (!result.success) {
811
- return null;
812
- }
813
- return result.output;
831
+ parsed = JSON.parse(content);
814
832
  } catch {
815
833
  return null;
816
834
  }
835
+ const result = safeParse(OcttoConfigSchema, parsed);
836
+ if (result.success) {
837
+ return result.output;
838
+ }
839
+ console.warn(`[octto] Config validation errors in ${configPath}:`);
840
+ console.warn(formatValidationErrors(result.issues));
841
+ if (typeof parsed !== "object" || parsed === null || !("agents" in parsed)) {
842
+ console.warn("[octto] No valid agents found in config, using defaults");
843
+ return null;
844
+ }
845
+ const rawAgents = parsed.agents;
846
+ if (typeof rawAgents !== "object" || rawAgents === null) {
847
+ console.warn("[octto] Invalid agents format, using defaults");
848
+ return null;
849
+ }
850
+ const validAgents = {};
851
+ let hasValidAgent = false;
852
+ for (const [name, override] of Object.entries(rawAgents)) {
853
+ if (!VALID_AGENT_NAMES.includes(name)) {
854
+ console.warn(`[octto] Unknown agent "${name}" - valid names: ${VALID_AGENT_NAMES.join(", ")}`);
855
+ continue;
856
+ }
857
+ const agentResult = safeParse(AgentOverrideSchema, override);
858
+ if (agentResult.success) {
859
+ validAgents[name] = agentResult.output;
860
+ hasValidAgent = true;
861
+ } else {
862
+ console.warn(`[octto] Invalid config for agent "${name}":`);
863
+ console.warn(formatValidationErrors(agentResult.issues));
864
+ }
865
+ }
866
+ if (!hasValidAgent) {
867
+ console.warn("[octto] No valid agent overrides found, using defaults");
868
+ return null;
869
+ }
870
+ console.warn("[octto] Partial config loaded - some overrides applied despite errors");
871
+ return { agents: validAgents };
817
872
  }
818
873
  async function loadCustomConfig(agents2, configDir) {
819
874
  const config = await load(configDir);
820
- if (!config?.agents) {
821
- return agents2;
822
- }
823
- const result = { ...agents2 };
824
- for (const [name, override] of Object.entries(config.agents)) {
825
- result[name] = { ...agents2[name], ...override };
875
+ const mergedAgents = { ...agents2 };
876
+ if (config?.agents) {
877
+ for (const [name, override] of Object.entries(config.agents)) {
878
+ mergedAgents[name] = { ...agents2[name], ...override };
879
+ }
826
880
  }
827
- return result;
881
+ return {
882
+ agents: mergedAgents,
883
+ port: resolvePort(config?.port)
884
+ };
828
885
  }
829
886
  // src/constants.ts
830
887
  var DEFAULT_ANSWER_TIMEOUT_MS = 300000;
@@ -2463,10 +2520,10 @@ function getHtmlBundle() {
2463
2520
  </html>`;
2464
2521
  }
2465
2522
  // src/session/server.ts
2466
- async function createServer(sessionId, store) {
2523
+ async function createServer(sessionId, store, configuredPort) {
2467
2524
  const htmlBundle = getHtmlBundle();
2468
2525
  const server = Bun.serve({
2469
- port: 0,
2526
+ port: configuredPort ?? 0,
2470
2527
  fetch(req, server2) {
2471
2528
  const url = new URL(req.url);
2472
2529
  if (url.pathname === "/ws") {
@@ -2624,7 +2681,7 @@ function createSessionStore(options = {}) {
2624
2681
  const store = {
2625
2682
  async startSession(input) {
2626
2683
  const sessionId = generateSessionId();
2627
- const { server, port } = await createServer(sessionId, store);
2684
+ const { server, port } = await createServer(sessionId, store, options.port);
2628
2685
  const url = `http://localhost:${port}`;
2629
2686
  const session = {
2630
2687
  id: sessionId,
@@ -16484,7 +16541,7 @@ function createOcttoTools(sessions, client) {
16484
16541
  // src/index.ts
16485
16542
  var Octto = async ({ client }) => {
16486
16543
  const customConfig = await loadCustomConfig(agents);
16487
- const sessions = createSessionStore();
16544
+ const sessions = createSessionStore({ port: customConfig.port });
16488
16545
  const tracked = new Map;
16489
16546
  const tools = createOcttoTools(sessions, client);
16490
16547
  const originalExecute = tools.start_session.execute;
@@ -16502,7 +16559,7 @@ var Octto = async ({ client }) => {
16502
16559
  return {
16503
16560
  tool: tools,
16504
16561
  config: async (config2) => {
16505
- config2.agent = { ...config2.agent, ...customConfig };
16562
+ config2.agent = { ...config2.agent, ...customConfig.agents };
16506
16563
  },
16507
16564
  event: async ({ event }) => {
16508
16565
  if (event.type !== "session.deleted")
@@ -3,7 +3,7 @@ import type { SessionStore } from "./sessions";
3
3
  interface WsData {
4
4
  sessionId: string;
5
5
  }
6
- export declare function createServer(sessionId: string, store: SessionStore): Promise<{
6
+ export declare function createServer(sessionId: string, store: SessionStore, configuredPort?: number): Promise<{
7
7
  server: Server<WsData>;
8
8
  port: number;
9
9
  }>;
@@ -3,6 +3,8 @@ import { type BaseConfig, type EndSessionOutput, type GetAnswerInput, type GetAn
3
3
  export interface SessionStoreOptions {
4
4
  /** Skip opening browser - useful for tests */
5
5
  skipBrowser?: boolean;
6
+ /** Fixed port for the server (0 = random available port) */
7
+ port?: number;
6
8
  }
7
9
  export interface SessionStore {
8
10
  startSession: (input: StartSessionInput) => Promise<StartSessionOutput>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octto",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "OpenCode plugin that turns rough ideas into designs through branch-based exploration",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",