oh-my-opencode-slim 0.9.7 → 0.9.9

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/dist/cli/index.js CHANGED
@@ -240,7 +240,8 @@ var BackgroundTaskConfigSchema = z2.object({
240
240
  var InterviewConfigSchema = z2.object({
241
241
  maxQuestions: z2.number().int().min(1).max(10).default(2),
242
242
  outputFolder: z2.string().min(1).default("interview"),
243
- autoOpenBrowser: z2.boolean().default(true)
243
+ autoOpenBrowser: z2.boolean().default(true),
244
+ port: z2.number().int().min(0).max(65535).default(0)
244
245
  });
245
246
  var TodoContinuationConfigSchema = z2.object({
246
247
  maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
@@ -150,6 +150,7 @@ export declare const InterviewConfigSchema: z.ZodObject<{
150
150
  maxQuestions: z.ZodDefault<z.ZodNumber>;
151
151
  outputFolder: z.ZodDefault<z.ZodString>;
152
152
  autoOpenBrowser: z.ZodDefault<z.ZodBoolean>;
153
+ port: z.ZodDefault<z.ZodNumber>;
153
154
  }, z.core.$strip>;
154
155
  export type InterviewConfig = z.infer<typeof InterviewConfigSchema>;
155
156
  export declare const TodoContinuationConfigSchema: z.ZodObject<{
@@ -282,6 +283,7 @@ export declare const PluginConfigSchema: z.ZodObject<{
282
283
  maxQuestions: z.ZodDefault<z.ZodNumber>;
283
284
  outputFolder: z.ZodDefault<z.ZodString>;
284
285
  autoOpenBrowser: z.ZodDefault<z.ZodBoolean>;
286
+ port: z.ZodDefault<z.ZodNumber>;
285
287
  }, z.core.$strip>>;
286
288
  todoContinuation: z.ZodOptional<z.ZodObject<{
287
289
  maxContinuations: z.ZodDefault<z.ZodNumber>;
package/dist/index.js CHANGED
@@ -534,7 +534,8 @@ var BackgroundTaskConfigSchema = z2.object({
534
534
  var InterviewConfigSchema = z2.object({
535
535
  maxQuestions: z2.number().int().min(1).max(10).default(2),
536
536
  outputFolder: z2.string().min(1).default("interview"),
537
- autoOpenBrowser: z2.boolean().default(true)
537
+ autoOpenBrowser: z2.boolean().default(true),
538
+ port: z2.number().int().min(0).max(65535).default(0)
538
539
  });
539
540
  var TodoContinuationConfigSchema = z2.object({
540
541
  maxContinuations: z2.number().int().min(1).max(50).default(5).describe("Maximum consecutive auto-continuations before stopping to ask user"),
@@ -4528,7 +4529,7 @@ function renderInterviewPage(interviewId) {
4528
4529
  </div>
4529
4530
 
4530
4531
  <script>
4531
- const interviewId = ${JSON.stringify(interviewId)};
4532
+ const interviewId = ${JSON.stringify(interviewId).replace(/</g, "\\u003c")};
4532
4533
  const state = { data: null, answers: {}, activeQuestionIndex: 0, lastSig: null, customMode: {} };
4533
4534
 
4534
4535
  function updateSubmitButton() {
@@ -4885,12 +4886,17 @@ function renderInterviewPage(interviewId) {
4885
4886
  }
4886
4887
  });
4887
4888
 
4889
+ function schedulePoll() {
4890
+ setTimeout(async () => {
4891
+ try { await refresh(); } catch (_) {}
4892
+ if (state.data?.mode !== 'abandoned') schedulePoll();
4893
+ }, 2500);
4894
+ }
4895
+
4888
4896
  refresh().catch((error) => {
4889
4897
  document.getElementById('submitStatus').textContent = error.message || 'Failed to load interview.';
4890
4898
  });
4891
- setInterval(() => {
4892
- refresh().catch(() => {});
4893
- }, 2500);
4899
+ schedulePoll();
4894
4900
  </script>
4895
4901
  </body>
4896
4902
  </html>`;
@@ -4944,6 +4950,7 @@ async function readJsonBody(request) {
4944
4950
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4945
4951
  size += buffer.length;
4946
4952
  if (size > 64 * 1024) {
4953
+ request.destroy();
4947
4954
  throw new Error("Request body too large");
4948
4955
  }
4949
4956
  chunks.push(buffer);
@@ -4965,8 +4972,15 @@ function sendHtml(response, html) {
4965
4972
  function createInterviewServer(deps) {
4966
4973
  let baseUrl = null;
4967
4974
  let startPromise = null;
4975
+ let activeServer = null;
4968
4976
  async function handle(request, response) {
4969
- const url = new URL2(request.url ?? "/", "http://127.0.0.1");
4977
+ let url;
4978
+ try {
4979
+ url = new URL2(request.url ?? "/", "http://127.0.0.1");
4980
+ } catch {
4981
+ sendJson(response, 400, { error: "Invalid request URL" });
4982
+ return;
4983
+ }
4970
4984
  const pathname = url.pathname;
4971
4985
  if (request.method === "GET" && pathname.startsWith("/interview/")) {
4972
4986
  sendHtml(response, renderInterviewPage(pathname.split("/").pop() ?? "unknown"));
@@ -4978,9 +4992,9 @@ function createInterviewServer(deps) {
4978
4992
  const state = await deps.getState(stateMatch[1]);
4979
4993
  sendJson(response, 200, state);
4980
4994
  } catch (error) {
4981
- sendJson(response, 404, {
4982
- error: error instanceof Error ? error.message : "Interview not found"
4983
- });
4995
+ const message = error instanceof Error ? error.message : "Interview not found";
4996
+ const status = message === "Interview not found" ? 404 : 500;
4997
+ sendJson(response, status, { error: message });
4984
4998
  }
4985
4999
  return;
4986
5000
  }
@@ -5020,11 +5034,20 @@ function createInterviewServer(deps) {
5020
5034
  });
5021
5035
  });
5022
5036
  });
5037
+ server.requestTimeout = 30000;
5038
+ server.headersTimeout = 1e4;
5039
+ activeServer = server;
5023
5040
  server.on("error", (error) => {
5041
+ server.close();
5042
+ activeServer = null;
5024
5043
  startPromise = null;
5025
- reject(error);
5044
+ if (error.code === "EADDRINUSE") {
5045
+ reject(new Error(`Interview server port ${deps.port} is already in use. Choose a different port or set port to 0 for an OS-assigned port.`));
5046
+ } else {
5047
+ reject(error);
5048
+ }
5026
5049
  });
5027
- server.listen(0, "127.0.0.1", () => {
5050
+ server.listen(deps.port, "127.0.0.1", () => {
5028
5051
  const address = server.address();
5029
5052
  if (!address || typeof address === "string") {
5030
5053
  startPromise = null;
@@ -5038,7 +5061,16 @@ function createInterviewServer(deps) {
5038
5061
  return startPromise;
5039
5062
  }
5040
5063
  return {
5041
- ensureStarted
5064
+ ensureStarted,
5065
+ close: () => {
5066
+ if (activeServer) {
5067
+ activeServer.closeAllConnections();
5068
+ activeServer.close();
5069
+ activeServer = null;
5070
+ }
5071
+ baseUrl = null;
5072
+ startPromise = null;
5073
+ }
5042
5074
  };
5043
5075
  }
5044
5076
 
@@ -5300,6 +5332,15 @@ async function ensureInterviewFile(record) {
5300
5332
  }
5301
5333
  }
5302
5334
  async function readInterviewDocument(record) {
5335
+ try {
5336
+ return await fs5.readFile(record.markdownPath, "utf8");
5337
+ } catch (error) {
5338
+ if (error.code === "ENOENT") {
5339
+ try {
5340
+ return await fs5.readFile(record.markdownPath, "utf8");
5341
+ } catch {}
5342
+ }
5343
+ }
5303
5344
  await ensureInterviewFile(record);
5304
5345
  return fs5.readFile(record.markdownPath, "utf8");
5305
5346
  }
@@ -5334,6 +5375,7 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
5334
5375
  }
5335
5376
  const outputDir = createInterviewDirectoryPath(directory, outputFolder);
5336
5377
  const candidates = new Set;
5378
+ const resolvedRoot = path6.resolve(directory);
5337
5379
  if (path6.isAbsolute(trimmed)) {
5338
5380
  candidates.add(trimmed);
5339
5381
  } else {
@@ -5347,6 +5389,10 @@ function resolveExistingInterviewPath(directory, outputFolder, value) {
5347
5389
  if (path6.extname(candidate) !== ".md") {
5348
5390
  continue;
5349
5391
  }
5392
+ const resolved = path6.resolve(candidate);
5393
+ if (!resolved.startsWith(resolvedRoot + path6.sep) && resolved !== resolvedRoot) {
5394
+ continue;
5395
+ }
5350
5396
  if (fsSync.existsSync(candidate)) {
5351
5397
  return candidate;
5352
5398
  }
@@ -5363,6 +5409,7 @@ function createInterviewService(ctx, config, deps) {
5363
5409
  const sessionBusy = new Map;
5364
5410
  const browserOpened = new Set;
5365
5411
  let resolveBaseUrl = null;
5412
+ let idCounter = 0;
5366
5413
  function setBaseUrlResolver(resolver) {
5367
5414
  resolveBaseUrl = resolver;
5368
5415
  }
@@ -5439,7 +5486,7 @@ function createInterviewService(ctx, config, deps) {
5439
5486
  }
5440
5487
  const messages = await loadMessages(sessionID);
5441
5488
  const record = {
5442
- id: `${Date.now()}-${slugify(idea) || "interview"}`,
5489
+ id: `${Date.now()}-${++idCounter}-${slugify(idea) || "interview"}`,
5443
5490
  sessionID,
5444
5491
  idea: normalizedIdea,
5445
5492
  markdownPath: createInterviewFilePath(ctx.directory, outputFolder, idea),
@@ -5467,7 +5514,7 @@ function createInterviewService(ctx, config, deps) {
5467
5514
  const messages = await loadMessages(sessionID);
5468
5515
  const title = extractTitle(document);
5469
5516
  const record = {
5470
- id: `${Date.now()}-${slugify(path6.basename(markdownPath, ".md")) || "interview"}`,
5517
+ id: `${Date.now()}-${++idCounter}-${slugify(path6.basename(markdownPath, ".md")) || "interview"}`,
5471
5518
  sessionID,
5472
5519
  idea: title || path6.basename(markdownPath, ".md"),
5473
5520
  markdownPath,
@@ -5668,7 +5715,8 @@ function createInterviewManager(ctx, config) {
5668
5715
  const service = createInterviewService(ctx, config.interview);
5669
5716
  const server = createInterviewServer({
5670
5717
  getState: async (interviewId) => service.getInterviewState(interviewId),
5671
- submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers)
5718
+ submitAnswers: async (interviewId, answers) => service.submitAnswers(interviewId, answers),
5719
+ port: config.interview?.port ?? 0
5672
5720
  });
5673
5721
  service.setBaseUrlResolver(() => server.ensureStarted());
5674
5722
  return {
@@ -2,6 +2,8 @@ import type { InterviewAnswer, InterviewState } from './types';
2
2
  export declare function createInterviewServer(deps: {
3
3
  getState: (interviewId: string) => Promise<InterviewState>;
4
4
  submitAnswers: (interviewId: string, answers: InterviewAnswer[]) => Promise<void>;
5
+ port: number;
5
6
  }): {
6
7
  ensureStarted: () => Promise<string>;
8
+ close: () => void;
7
9
  };
@@ -430,6 +430,12 @@
430
430
  "autoOpenBrowser": {
431
431
  "default": true,
432
432
  "type": "boolean"
433
+ },
434
+ "port": {
435
+ "default": 0,
436
+ "type": "integer",
437
+ "minimum": 0,
438
+ "maximum": 65535
433
439
  }
434
440
  }
435
441
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode-slim",
3
- "version": "0.9.7",
3
+ "version": "0.9.9",
4
4
  "description": "Lightweight agent orchestration plugin for OpenCode - a slimmed-down fork of oh-my-opencode",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",