micode 0.8.3 → 0.8.5
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 +2 -2
- package/dist/index.js +21020 -0
- package/package.json +10 -6
- package/src/agents/artifact-searcher.ts +0 -1
- package/src/agents/bootstrapper.ts +164 -0
- package/src/agents/brainstormer.ts +140 -33
- package/src/agents/codebase-analyzer.ts +0 -1
- package/src/agents/codebase-locator.ts +0 -1
- package/src/agents/commander.ts +99 -10
- package/src/agents/executor.ts +18 -1
- package/src/agents/implementer.ts +83 -6
- package/src/agents/index.ts +29 -19
- package/src/agents/ledger-creator.ts +0 -1
- package/src/agents/octto.ts +132 -0
- package/src/agents/pattern-finder.ts +0 -1
- package/src/agents/planner.ts +139 -49
- package/src/agents/probe.ts +152 -0
- package/src/agents/project-initializer.ts +0 -1
- package/src/agents/reviewer.ts +75 -5
- package/src/config-loader.test.ts +226 -0
- package/src/config-loader.ts +132 -6
- package/src/hooks/artifact-auto-index.ts +2 -1
- package/src/hooks/auto-compact.ts +14 -21
- package/src/hooks/context-injector.ts +6 -13
- package/src/hooks/context-window-monitor.ts +8 -13
- package/src/hooks/ledger-loader.ts +4 -6
- package/src/hooks/token-aware-truncation.ts +11 -17
- package/src/index.ts +54 -22
- package/src/indexing/milestone-artifact-classifier.ts +26 -0
- package/src/indexing/milestone-artifact-ingest.ts +42 -0
- package/src/octto/constants.ts +20 -0
- package/src/octto/session/browser.ts +32 -0
- package/src/octto/session/index.ts +25 -0
- package/src/octto/session/server.ts +89 -0
- package/src/octto/session/sessions.ts +383 -0
- package/src/octto/session/types.ts +305 -0
- package/src/octto/session/utils.ts +25 -0
- package/src/octto/session/waiter.ts +139 -0
- package/src/octto/state/index.ts +5 -0
- package/src/octto/state/persistence.ts +65 -0
- package/src/octto/state/store.ts +161 -0
- package/src/octto/state/types.ts +51 -0
- package/src/octto/types.ts +376 -0
- package/src/octto/ui/bundle.ts +1650 -0
- package/src/octto/ui/index.ts +2 -0
- package/src/tools/artifact-index/index.ts +152 -3
- package/src/tools/artifact-index/schema.sql +21 -0
- package/src/tools/milestone-artifact-search.ts +48 -0
- package/src/tools/octto/brainstorm.ts +332 -0
- package/src/tools/octto/extractor.ts +95 -0
- package/src/tools/octto/factory.ts +89 -0
- package/src/tools/octto/formatters.ts +63 -0
- package/src/tools/octto/index.ts +27 -0
- package/src/tools/octto/processor.ts +165 -0
- package/src/tools/octto/questions.ts +508 -0
- package/src/tools/octto/responses.ts +135 -0
- package/src/tools/octto/session.ts +114 -0
- package/src/tools/octto/types.ts +21 -0
- package/src/tools/octto/utils.ts +4 -0
- package/src/tools/pty/manager.ts +13 -7
- package/src/tools/spawn-agent.ts +1 -3
- package/src/utils/config.ts +123 -0
- package/src/utils/errors.ts +57 -0
- package/src/utils/logger.ts +50 -0
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
3
3
|
import { readFile, readdir } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
const LEDGER_DIR = "thoughts/ledgers";
|
|
7
|
-
const LEDGER_PREFIX = "CONTINUITY_";
|
|
5
|
+
import { config } from "../utils/config";
|
|
8
6
|
|
|
9
7
|
export interface LedgerInfo {
|
|
10
8
|
sessionName: string;
|
|
@@ -13,11 +11,11 @@ export interface LedgerInfo {
|
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
export async function findCurrentLedger(directory: string): Promise<LedgerInfo | null> {
|
|
16
|
-
const ledgerDir = join(directory,
|
|
14
|
+
const ledgerDir = join(directory, config.paths.ledgerDir);
|
|
17
15
|
|
|
18
16
|
try {
|
|
19
17
|
const files = await readdir(ledgerDir);
|
|
20
|
-
const ledgerFiles = files.filter((f) => f.startsWith(
|
|
18
|
+
const ledgerFiles = files.filter((f) => f.startsWith(config.paths.ledgerPrefix) && f.endsWith(".md"));
|
|
21
19
|
|
|
22
20
|
if (ledgerFiles.length === 0) return null;
|
|
23
21
|
|
|
@@ -40,7 +38,7 @@ export async function findCurrentLedger(directory: string): Promise<LedgerInfo |
|
|
|
40
38
|
|
|
41
39
|
const filePath = join(ledgerDir, latestFile);
|
|
42
40
|
const content = await readFile(filePath, "utf-8");
|
|
43
|
-
const sessionName = latestFile.replace(
|
|
41
|
+
const sessionName = latestFile.replace(config.paths.ledgerPrefix, "").replace(".md", "");
|
|
44
42
|
|
|
45
43
|
return { sessionName, filePath, content };
|
|
46
44
|
} catch {
|
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
import { config } from "../utils/config";
|
|
2
3
|
|
|
3
4
|
// Tools that benefit from truncation
|
|
4
5
|
const TRUNCATABLE_TOOLS = ["grep", "Grep", "glob", "Glob", "ast_grep_search"];
|
|
5
6
|
|
|
6
|
-
// Token estimation (conservative: 4 chars = 1 token)
|
|
7
|
-
const CHARS_PER_TOKEN = 4;
|
|
8
|
-
const DEFAULT_CONTEXT_LIMIT = 200_000;
|
|
9
|
-
const DEFAULT_MAX_OUTPUT_TOKENS = 50_000;
|
|
10
|
-
const SAFETY_MARGIN = 0.5; // Keep 50% headroom
|
|
11
|
-
const PRESERVE_HEADER_LINES = 3;
|
|
12
|
-
|
|
13
7
|
function estimateTokens(text: string): number {
|
|
14
|
-
return Math.ceil(text.length /
|
|
8
|
+
return Math.ceil(text.length / config.tokens.charsPerToken);
|
|
15
9
|
}
|
|
16
10
|
|
|
17
11
|
function truncateToTokenLimit(
|
|
18
12
|
output: string,
|
|
19
13
|
maxTokens: number,
|
|
20
|
-
preserveLines: number =
|
|
14
|
+
preserveLines: number = config.tokens.preserveHeaderLines,
|
|
21
15
|
): string {
|
|
22
16
|
const currentTokens = estimateTokens(output);
|
|
23
17
|
|
|
@@ -85,7 +79,7 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
|
|
|
85
79
|
|
|
86
80
|
const messages = (resp as { data?: unknown[] }).data;
|
|
87
81
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
88
|
-
return { used: 0, limit:
|
|
82
|
+
return { used: 0, limit: config.tokens.defaultContextLimit };
|
|
89
83
|
}
|
|
90
84
|
|
|
91
85
|
// Find last assistant message with usage info
|
|
@@ -96,7 +90,7 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
|
|
|
96
90
|
}) as Record<string, unknown> | undefined;
|
|
97
91
|
|
|
98
92
|
if (!lastAssistant) {
|
|
99
|
-
return { used: 0, limit:
|
|
93
|
+
return { used: 0, limit: config.tokens.defaultContextLimit };
|
|
100
94
|
}
|
|
101
95
|
|
|
102
96
|
const info = lastAssistant.info as Record<string, unknown> | undefined;
|
|
@@ -107,25 +101,25 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
|
|
|
107
101
|
const used = inputTokens + cacheRead;
|
|
108
102
|
|
|
109
103
|
// Get model limit (simplified - use default for now)
|
|
110
|
-
const limit =
|
|
104
|
+
const limit = config.tokens.defaultContextLimit;
|
|
111
105
|
|
|
112
106
|
const result = { used, limit };
|
|
113
107
|
state.sessionTokenUsage.set(sessionID, result);
|
|
114
108
|
return result;
|
|
115
109
|
} catch {
|
|
116
|
-
return state.sessionTokenUsage.get(sessionID) || { used: 0, limit:
|
|
110
|
+
return state.sessionTokenUsage.get(sessionID) || { used: 0, limit: config.tokens.defaultContextLimit };
|
|
117
111
|
}
|
|
118
112
|
}
|
|
119
113
|
|
|
120
114
|
function calculateMaxOutputTokens(used: number, limit: number): number {
|
|
121
115
|
const remaining = limit - used;
|
|
122
|
-
const available = Math.floor(remaining *
|
|
116
|
+
const available = Math.floor(remaining * config.tokens.safetyMargin);
|
|
123
117
|
|
|
124
118
|
if (available <= 0) {
|
|
125
119
|
return 0;
|
|
126
120
|
}
|
|
127
121
|
|
|
128
|
-
return Math.min(available,
|
|
122
|
+
return Math.min(available, config.tokens.defaultMaxOutputTokens);
|
|
129
123
|
}
|
|
130
124
|
|
|
131
125
|
return {
|
|
@@ -180,8 +174,8 @@ export function createTokenAwareTruncationHook(ctx: PluginInput) {
|
|
|
180
174
|
} catch {
|
|
181
175
|
// On error, apply static truncation as fallback
|
|
182
176
|
const currentTokens = estimateTokens(output.output);
|
|
183
|
-
if (currentTokens >
|
|
184
|
-
output.output = truncateToTokenLimit(output.output,
|
|
177
|
+
if (currentTokens > config.tokens.defaultMaxOutputTokens) {
|
|
178
|
+
output.output = truncateToTokenLimit(output.output, config.tokens.defaultMaxOutputTokens);
|
|
185
179
|
}
|
|
186
180
|
}
|
|
187
181
|
},
|
package/src/index.ts
CHANGED
|
@@ -3,30 +3,28 @@ import type { McpLocalConfig } from "@opencode-ai/sdk";
|
|
|
3
3
|
|
|
4
4
|
// Agents
|
|
5
5
|
import { agents, PRIMARY_AGENT_NAME } from "./agents";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import { btca_ask, checkBtcaAvailable } from "./tools/btca";
|
|
10
|
-
import { look_at } from "./tools/look-at";
|
|
11
|
-
import { artifact_search } from "./tools/artifact-search";
|
|
12
|
-
import { createSpawnAgentTool } from "./tools/spawn-agent";
|
|
13
|
-
|
|
6
|
+
// Config loader
|
|
7
|
+
import { loadMicodeConfig, mergeAgentConfigs } from "./config-loader";
|
|
8
|
+
import { createArtifactAutoIndexHook } from "./hooks/artifact-auto-index";
|
|
14
9
|
// Hooks
|
|
15
10
|
import { createAutoCompactHook } from "./hooks/auto-compact";
|
|
11
|
+
import { createCommentCheckerHook } from "./hooks/comment-checker";
|
|
16
12
|
import { createContextInjectorHook } from "./hooks/context-injector";
|
|
17
|
-
import { createSessionRecoveryHook } from "./hooks/session-recovery";
|
|
18
|
-
import { createTokenAwareTruncationHook } from "./hooks/token-aware-truncation";
|
|
19
13
|
import { createContextWindowMonitorHook } from "./hooks/context-window-monitor";
|
|
20
|
-
import { createCommentCheckerHook } from "./hooks/comment-checker";
|
|
21
|
-
import { createLedgerLoaderHook } from "./hooks/ledger-loader";
|
|
22
|
-
import { createArtifactAutoIndexHook } from "./hooks/artifact-auto-index";
|
|
23
14
|
import { createFileOpsTrackerHook, getFileOps } from "./hooks/file-ops-tracker";
|
|
24
|
-
|
|
15
|
+
import { createLedgerLoaderHook } from "./hooks/ledger-loader";
|
|
16
|
+
import { createSessionRecoveryHook } from "./hooks/session-recovery";
|
|
17
|
+
import { createTokenAwareTruncationHook } from "./hooks/token-aware-truncation";
|
|
18
|
+
import { artifact_search } from "./tools/artifact-search";
|
|
19
|
+
// Tools
|
|
20
|
+
import { ast_grep_replace, ast_grep_search, checkAstGrepAvailable } from "./tools/ast-grep";
|
|
21
|
+
import { btca_ask, checkBtcaAvailable } from "./tools/btca";
|
|
22
|
+
import { look_at } from "./tools/look-at";
|
|
23
|
+
import { milestone_artifact_search } from "./tools/milestone-artifact-search";
|
|
24
|
+
import { createOcttoTools, createSessionStore } from "./tools/octto";
|
|
25
25
|
// PTY System
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
// Config loader
|
|
29
|
-
import { loadMicodeConfig, mergeAgentConfigs } from "./config-loader";
|
|
26
|
+
import { createPtyTools, PTYManager } from "./tools/pty";
|
|
27
|
+
import { createSpawnAgentTool } from "./tools/spawn-agent";
|
|
30
28
|
|
|
31
29
|
// Think mode: detect keywords and enable extended thinking
|
|
32
30
|
const THINK_KEYWORDS = [
|
|
@@ -75,7 +73,7 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
75
73
|
console.warn(`[micode] ${btcaStatus.message}`);
|
|
76
74
|
}
|
|
77
75
|
|
|
78
|
-
// Load user config for model overrides
|
|
76
|
+
// Load user config for temperature/maxTokens overrides (model overrides not supported)
|
|
79
77
|
const userConfig = await loadMicodeConfig();
|
|
80
78
|
|
|
81
79
|
// Think mode state per session
|
|
@@ -99,6 +97,28 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
99
97
|
// Spawn agent tool (for subagents to spawn other subagents)
|
|
100
98
|
const spawn_agent = createSpawnAgentTool(ctx);
|
|
101
99
|
|
|
100
|
+
// Octto (browser-based brainstorming) tools
|
|
101
|
+
const octtoSessionStore = createSessionStore();
|
|
102
|
+
|
|
103
|
+
// Track octto sessions per opencode session for cleanup
|
|
104
|
+
const octtoSessionsMap = new Map<string, Set<string>>();
|
|
105
|
+
|
|
106
|
+
const octtoTools = createOcttoTools(octtoSessionStore, ctx.client, {
|
|
107
|
+
onCreated: (parentSessionId, octtoSessionId) => {
|
|
108
|
+
const sessions = octtoSessionsMap.get(parentSessionId) ?? new Set<string>();
|
|
109
|
+
sessions.add(octtoSessionId);
|
|
110
|
+
octtoSessionsMap.set(parentSessionId, sessions);
|
|
111
|
+
},
|
|
112
|
+
onEnded: (parentSessionId, octtoSessionId) => {
|
|
113
|
+
const sessions = octtoSessionsMap.get(parentSessionId);
|
|
114
|
+
if (!sessions) return;
|
|
115
|
+
sessions.delete(octtoSessionId);
|
|
116
|
+
if (sessions.size === 0) {
|
|
117
|
+
octtoSessionsMap.delete(parentSessionId);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
102
122
|
return {
|
|
103
123
|
// Tools
|
|
104
124
|
tool: {
|
|
@@ -107,8 +127,10 @@ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
|
|
|
107
127
|
btca_ask,
|
|
108
128
|
look_at,
|
|
109
129
|
artifact_search,
|
|
130
|
+
milestone_artifact_search,
|
|
110
131
|
spawn_agent,
|
|
111
132
|
...ptyTools,
|
|
133
|
+
...octtoTools,
|
|
112
134
|
},
|
|
113
135
|
|
|
114
136
|
config: async (config) => {
|
|
@@ -292,12 +314,22 @@ IMPORTANT:
|
|
|
292
314
|
},
|
|
293
315
|
|
|
294
316
|
event: async ({ event }) => {
|
|
295
|
-
// Session cleanup (think mode + PTY)
|
|
317
|
+
// Session cleanup (think mode + PTY + octto)
|
|
296
318
|
if (event.type === "session.deleted") {
|
|
297
319
|
const props = event.properties as { info?: { id?: string } } | undefined;
|
|
298
320
|
if (props?.info?.id) {
|
|
299
|
-
|
|
300
|
-
|
|
321
|
+
const sessionId = props.info.id;
|
|
322
|
+
thinkModeState.delete(sessionId);
|
|
323
|
+
ptyManager.cleanupBySession(sessionId);
|
|
324
|
+
|
|
325
|
+
// Cleanup octto sessions
|
|
326
|
+
const octtoSessions = octtoSessionsMap.get(sessionId);
|
|
327
|
+
if (octtoSessions) {
|
|
328
|
+
for (const octtoSessionId of octtoSessions) {
|
|
329
|
+
await octtoSessionStore.endSession(octtoSessionId).catch(() => {});
|
|
330
|
+
}
|
|
331
|
+
octtoSessionsMap.delete(sessionId);
|
|
332
|
+
}
|
|
301
333
|
}
|
|
302
334
|
}
|
|
303
335
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const MILESTONE_ARTIFACT_TYPES = {
|
|
2
|
+
FEATURE: "feature",
|
|
3
|
+
DECISION: "decision",
|
|
4
|
+
SESSION: "session",
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
export type MilestoneArtifactType = (typeof MILESTONE_ARTIFACT_TYPES)[keyof typeof MILESTONE_ARTIFACT_TYPES];
|
|
8
|
+
|
|
9
|
+
const FEATURE_HINTS = ["requirement", "implementation", "capability", "scope", "spec"];
|
|
10
|
+
const DECISION_HINTS = ["decision", "decided", "trade-off", "rationale", "chosen"];
|
|
11
|
+
const SESSION_HINTS = ["meeting", "status", "discussion", "notes", "update"];
|
|
12
|
+
|
|
13
|
+
const matchesAny = (content: string, hints: string[]) => hints.some((hint) => content.includes(hint));
|
|
14
|
+
|
|
15
|
+
export function classifyMilestoneArtifact(content: string): MilestoneArtifactType {
|
|
16
|
+
const normalized = content.toLowerCase();
|
|
17
|
+
const isFeature = matchesAny(normalized, FEATURE_HINTS);
|
|
18
|
+
const isDecision = matchesAny(normalized, DECISION_HINTS);
|
|
19
|
+
const isSession = matchesAny(normalized, SESSION_HINTS);
|
|
20
|
+
|
|
21
|
+
if (isFeature) return MILESTONE_ARTIFACT_TYPES.FEATURE;
|
|
22
|
+
if (isDecision) return MILESTONE_ARTIFACT_TYPES.DECISION;
|
|
23
|
+
if (isSession) return MILESTONE_ARTIFACT_TYPES.SESSION;
|
|
24
|
+
|
|
25
|
+
return MILESTONE_ARTIFACT_TYPES.SESSION;
|
|
26
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ArtifactIndex, getArtifactIndex } from "../tools/artifact-index";
|
|
2
|
+
import { log } from "../utils/logger";
|
|
3
|
+
import {
|
|
4
|
+
classifyMilestoneArtifact,
|
|
5
|
+
MILESTONE_ARTIFACT_TYPES,
|
|
6
|
+
type MilestoneArtifactType,
|
|
7
|
+
} from "./milestone-artifact-classifier";
|
|
8
|
+
|
|
9
|
+
export interface MilestoneArtifactInput {
|
|
10
|
+
id: string;
|
|
11
|
+
milestoneId: string;
|
|
12
|
+
sourceSessionId?: string;
|
|
13
|
+
createdAt?: string;
|
|
14
|
+
tags?: string[];
|
|
15
|
+
payload: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function ingestMilestoneArtifact(
|
|
19
|
+
input: MilestoneArtifactInput,
|
|
20
|
+
index?: ArtifactIndex,
|
|
21
|
+
classifier: (content: string) => MilestoneArtifactType = classifyMilestoneArtifact,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const artifactIndex = index ?? (await getArtifactIndex());
|
|
24
|
+
let artifactType: MilestoneArtifactType;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
artifactType = classifier(input.payload);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
log.error("milestone-ingest", "Failed to classify milestone artifact, defaulting to session", error);
|
|
30
|
+
artifactType = MILESTONE_ARTIFACT_TYPES.SESSION;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await artifactIndex.indexMilestoneArtifact({
|
|
34
|
+
id: input.id,
|
|
35
|
+
milestoneId: input.milestoneId,
|
|
36
|
+
artifactType,
|
|
37
|
+
sourceSessionId: input.sourceSessionId,
|
|
38
|
+
createdAt: input.createdAt,
|
|
39
|
+
tags: input.tags,
|
|
40
|
+
payload: input.payload,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/octto/constants.ts
|
|
2
|
+
// Re-exports from centralized config for backward compatibility
|
|
3
|
+
// Single source of truth is in src/utils/config.ts
|
|
4
|
+
|
|
5
|
+
import { config } from "../utils/config";
|
|
6
|
+
|
|
7
|
+
/** Default timeout for waiting for user answers (5 minutes) */
|
|
8
|
+
export const DEFAULT_ANSWER_TIMEOUT_MS = config.octto.answerTimeoutMs;
|
|
9
|
+
|
|
10
|
+
/** Default maximum number of follow-up questions per branch */
|
|
11
|
+
export const DEFAULT_MAX_QUESTIONS = config.octto.maxQuestions;
|
|
12
|
+
|
|
13
|
+
/** Default timeout for brainstorm review (10 minutes) */
|
|
14
|
+
export const DEFAULT_REVIEW_TIMEOUT_MS = config.octto.reviewTimeoutMs;
|
|
15
|
+
|
|
16
|
+
/** Maximum number of brainstorm iterations */
|
|
17
|
+
export const MAX_ITERATIONS = config.octto.maxIterations;
|
|
18
|
+
|
|
19
|
+
/** Directory for persisting brainstorm state files */
|
|
20
|
+
export const STATE_DIR = config.octto.stateDir;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/octto/session/browser.ts
|
|
2
|
+
// Cross-platform browser opener
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Opens the default browser to the specified URL.
|
|
6
|
+
* Detects platform and uses appropriate command.
|
|
7
|
+
*/
|
|
8
|
+
export async function openBrowser(url: string): Promise<void> {
|
|
9
|
+
const platform = process.platform;
|
|
10
|
+
|
|
11
|
+
let command: string[];
|
|
12
|
+
|
|
13
|
+
switch (platform) {
|
|
14
|
+
case "darwin":
|
|
15
|
+
command = ["open", url];
|
|
16
|
+
break;
|
|
17
|
+
case "win32":
|
|
18
|
+
command = ["cmd", "/c", "start", url];
|
|
19
|
+
break;
|
|
20
|
+
default:
|
|
21
|
+
// Linux and others
|
|
22
|
+
command = ["xdg-open", url];
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const proc = Bun.spawn(command, {
|
|
27
|
+
stdout: "ignore",
|
|
28
|
+
stderr: "ignore",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
await proc.exited;
|
|
32
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// src/octto/session/index.ts
|
|
2
|
+
export type { SessionStore, SessionStoreOptions } from "./sessions";
|
|
3
|
+
export { createSessionStore } from "./sessions";
|
|
4
|
+
export type {
|
|
5
|
+
Answer,
|
|
6
|
+
AskCodeAnswer,
|
|
7
|
+
AskFileAnswer,
|
|
8
|
+
AskImageAnswer,
|
|
9
|
+
AskTextAnswer,
|
|
10
|
+
BaseConfig,
|
|
11
|
+
ConfirmAnswer,
|
|
12
|
+
EmojiReactAnswer,
|
|
13
|
+
PickManyAnswer,
|
|
14
|
+
PickOneAnswer,
|
|
15
|
+
QuestionAnswers,
|
|
16
|
+
QuestionConfig,
|
|
17
|
+
QuestionType,
|
|
18
|
+
RankAnswer,
|
|
19
|
+
RateAnswer,
|
|
20
|
+
ReviewAnswer,
|
|
21
|
+
ShowOptionsAnswer,
|
|
22
|
+
SliderAnswer,
|
|
23
|
+
ThumbsAnswer,
|
|
24
|
+
} from "./types";
|
|
25
|
+
export { QUESTION_TYPES, QUESTIONS, STATUSES, WS_MESSAGES } from "./types";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/octto/session/server.ts
|
|
2
|
+
import type { Server, ServerWebSocket } from "bun";
|
|
3
|
+
|
|
4
|
+
import { config } from "../../utils/config";
|
|
5
|
+
import { getHtmlBundle } from "../ui";
|
|
6
|
+
import type { SessionStore } from "./sessions";
|
|
7
|
+
import type { WsClientMessage } from "./types";
|
|
8
|
+
|
|
9
|
+
interface WsData {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function createServer(
|
|
14
|
+
sessionId: string,
|
|
15
|
+
store: SessionStore,
|
|
16
|
+
): Promise<{ server: Server<WsData>; port: number }> {
|
|
17
|
+
const htmlBundle = getHtmlBundle();
|
|
18
|
+
|
|
19
|
+
const server = Bun.serve<WsData>({
|
|
20
|
+
port: 0, // Random available port
|
|
21
|
+
hostname: config.octto.allowRemoteBind ? config.octto.bindAddress : "127.0.0.1",
|
|
22
|
+
fetch(req, server) {
|
|
23
|
+
const url = new URL(req.url);
|
|
24
|
+
|
|
25
|
+
// WebSocket upgrade
|
|
26
|
+
if (url.pathname === "/ws") {
|
|
27
|
+
const success = server.upgrade(req, {
|
|
28
|
+
data: { sessionId },
|
|
29
|
+
});
|
|
30
|
+
if (success) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Serve the bundled HTML app
|
|
37
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
38
|
+
return new Response(htmlBundle, {
|
|
39
|
+
headers: {
|
|
40
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return new Response("Not Found", { status: 404 });
|
|
46
|
+
},
|
|
47
|
+
websocket: {
|
|
48
|
+
open(ws: ServerWebSocket<WsData>) {
|
|
49
|
+
const { sessionId } = ws.data;
|
|
50
|
+
store.handleWsConnect(sessionId, ws);
|
|
51
|
+
},
|
|
52
|
+
close(ws: ServerWebSocket<WsData>) {
|
|
53
|
+
const { sessionId } = ws.data;
|
|
54
|
+
store.handleWsDisconnect(sessionId);
|
|
55
|
+
},
|
|
56
|
+
message(ws: ServerWebSocket<WsData>, message: string | Buffer) {
|
|
57
|
+
const { sessionId } = ws.data;
|
|
58
|
+
|
|
59
|
+
let parsed: WsClientMessage;
|
|
60
|
+
try {
|
|
61
|
+
parsed = JSON.parse(message.toString()) as WsClientMessage;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("[octto] Failed to parse WebSocket message:", error);
|
|
64
|
+
ws.send(
|
|
65
|
+
JSON.stringify({
|
|
66
|
+
type: "error",
|
|
67
|
+
error: "Invalid message format",
|
|
68
|
+
details: error instanceof Error ? error.message : "Parse failed",
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
store.handleWsMessage(sessionId, parsed);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Port is always defined when using port: 0
|
|
80
|
+
const port = server.port;
|
|
81
|
+
if (port === undefined) {
|
|
82
|
+
throw new Error("Failed to get server port");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
server,
|
|
87
|
+
port,
|
|
88
|
+
};
|
|
89
|
+
}
|