ubume 0.1.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.
Files changed (182) hide show
  1. package/README.md +88 -0
  2. package/bin/codexa-local-harness-bridge.js +3 -0
  3. package/bin/codexa.js +28 -0
  4. package/bin/ubume-local-harness-bridge.js +325 -0
  5. package/bin/ubume.js +398 -0
  6. package/package.json +66 -0
  7. package/src/app.tsx +5759 -0
  8. package/src/commands/handler.ts +889 -0
  9. package/src/config/appVersion.ts +69 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.ts +196 -0
  12. package/src/config/layeredConfig.ts +853 -0
  13. package/src/config/legacyEnv.ts +16 -0
  14. package/src/config/persistence.ts +377 -0
  15. package/src/config/runtimeConfig.ts +558 -0
  16. package/src/config/settings.ts +405 -0
  17. package/src/config/toml-serialize.ts +109 -0
  18. package/src/config/trustStore.ts +84 -0
  19. package/src/config/updateCheckCache.ts +85 -0
  20. package/src/core/README.md +55 -0
  21. package/src/core/agent/loop.ts +464 -0
  22. package/src/core/agent/protocol.ts +345 -0
  23. package/src/core/agent/tools.ts +423 -0
  24. package/src/core/auth/codexAuth.ts +359 -0
  25. package/src/core/codex/codexExecArgs.ts +166 -0
  26. package/src/core/codex/codexLaunch.ts +163 -0
  27. package/src/core/codex/codexPrompt.ts +429 -0
  28. package/src/core/debug/inputDebug.ts +51 -0
  29. package/src/core/debug/localStreamDebug.ts +50 -0
  30. package/src/core/debug/modelStateDebug.ts +35 -0
  31. package/src/core/executables/antigravityExecutable.ts +48 -0
  32. package/src/core/executables/claudeExecutable.ts +63 -0
  33. package/src/core/executables/codexExecutable.ts +160 -0
  34. package/src/core/executables/executableResolver.ts +164 -0
  35. package/src/core/executables/geminiExecutable.ts +78 -0
  36. package/src/core/models/codexCapabilities.ts +97 -0
  37. package/src/core/models/codexModelCapabilities.ts +624 -0
  38. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  39. package/src/core/models/modelSpecs.ts +303 -0
  40. package/src/core/models/providerModelCache.ts +94 -0
  41. package/src/core/perf/profiler.ts +125 -0
  42. package/src/core/perf/renderDebug.ts +398 -0
  43. package/src/core/process/CommandRunner.ts +280 -0
  44. package/src/core/process/processValidation.ts +111 -0
  45. package/src/core/providerLauncher/launcher.ts +220 -0
  46. package/src/core/providerLauncher/registry.ts +354 -0
  47. package/src/core/providerLauncher/types.ts +95 -0
  48. package/src/core/providerLauncher/workspaceConfig.ts +487 -0
  49. package/src/core/providerRuntime/anthropic.ts +580 -0
  50. package/src/core/providerRuntime/antigravity.ts +500 -0
  51. package/src/core/providerRuntime/capabilityProfile.ts +383 -0
  52. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
  53. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  54. package/src/core/providerRuntime/codexaCupy.ts +97 -0
  55. package/src/core/providerRuntime/codexaNative.ts +425 -0
  56. package/src/core/providerRuntime/contextMetadata.ts +397 -0
  57. package/src/core/providerRuntime/gemini.ts +789 -0
  58. package/src/core/providerRuntime/lmstudio.ts +118 -0
  59. package/src/core/providerRuntime/local.ts +770 -0
  60. package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
  61. package/src/core/providerRuntime/localOutputBudget.ts +17 -0
  62. package/src/core/providerRuntime/mistralVibe.ts +667 -0
  63. package/src/core/providerRuntime/models.ts +175 -0
  64. package/src/core/providerRuntime/reasoning.ts +20 -0
  65. package/src/core/providerRuntime/registry.ts +286 -0
  66. package/src/core/providerRuntime/types.ts +142 -0
  67. package/src/core/providerRuntime/unsloth.ts +216 -0
  68. package/src/core/providers/codexJsonStream.ts +305 -0
  69. package/src/core/providers/codexSubprocess.ts +378 -0
  70. package/src/core/providers/codexTranscript.ts +695 -0
  71. package/src/core/providers/openaiNative.ts +13 -0
  72. package/src/core/providers/registry.ts +21 -0
  73. package/src/core/providers/types.ts +91 -0
  74. package/src/core/shared/attachments.ts +101 -0
  75. package/src/core/shared/cleanupFastFail.ts +67 -0
  76. package/src/core/shared/clipboard.ts +24 -0
  77. package/src/core/shared/clipboardImage.ts +111 -0
  78. package/src/core/shared/githubDiagnostics.ts +222 -0
  79. package/src/core/shared/hollowResponseFormat.ts +39 -0
  80. package/src/core/terminal/clearFrameBoundary.ts +852 -0
  81. package/src/core/terminal/frameLock.ts +110 -0
  82. package/src/core/terminal/inkRenderReset.ts +123 -0
  83. package/src/core/terminal/startupClear.ts +20 -0
  84. package/src/core/terminal/terminalCapabilities.ts +100 -0
  85. package/src/core/terminal/terminalControl.ts +169 -0
  86. package/src/core/terminal/terminalSanitize.ts +147 -0
  87. package/src/core/terminal/terminalTitle.ts +400 -0
  88. package/src/core/version/channel.ts +27 -0
  89. package/src/core/version/packageManager.ts +119 -0
  90. package/src/core/version/updateCheck.ts +203 -0
  91. package/src/core/workspace/appData.ts +107 -0
  92. package/src/core/workspace/conversationStore.ts +335 -0
  93. package/src/core/workspace/launchContext.ts +259 -0
  94. package/src/core/workspace/planStorage.ts +135 -0
  95. package/src/core/workspace/projectInstructions.ts +54 -0
  96. package/src/core/workspace/scratchDir.ts +64 -0
  97. package/src/core/workspace/workspaceActivity.ts +384 -0
  98. package/src/core/workspace/workspaceGuard.ts +377 -0
  99. package/src/core/workspace/workspaceRoot.ts +47 -0
  100. package/src/exec.ts +73 -0
  101. package/src/headless/execArgs.ts +296 -0
  102. package/src/headless/execRunner.ts +304 -0
  103. package/src/index.tsx +270 -0
  104. package/src/legacyEnvBootstrap.ts +5 -0
  105. package/src/session/appSession.ts +771 -0
  106. package/src/session/chatLifecycle.ts +994 -0
  107. package/src/session/conversation.ts +107 -0
  108. package/src/session/liveRenderScheduler.ts +214 -0
  109. package/src/session/persistedResponse.ts +93 -0
  110. package/src/session/planFlow.ts +159 -0
  111. package/src/session/planTranscript.ts +19 -0
  112. package/src/session/promptRunSchedule.ts +26 -0
  113. package/src/session/types.ts +234 -0
  114. package/src/test/runtimeTestUtils.ts +14 -0
  115. package/src/types/react-dom.d.ts +3 -0
  116. package/src/ui/chrome/ActivityBars.tsx +68 -0
  117. package/src/ui/chrome/ActivityIndicator.tsx +58 -0
  118. package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
  119. package/src/ui/chrome/AppShell.tsx +474 -0
  120. package/src/ui/chrome/BottomComposer.tsx +1135 -0
  121. package/src/ui/chrome/DashCard.tsx +82 -0
  122. package/src/ui/chrome/RunFooter.tsx +65 -0
  123. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  124. package/src/ui/chrome/Spinner.tsx +25 -0
  125. package/src/ui/chrome/TopHeader.tsx +439 -0
  126. package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
  127. package/src/ui/chrome/busyStatusAnimation.ts +11 -0
  128. package/src/ui/input/commandNormalize.ts +66 -0
  129. package/src/ui/input/focus.ts +73 -0
  130. package/src/ui/input/imageAttachments.ts +18 -0
  131. package/src/ui/input/inputBuffer.ts +203 -0
  132. package/src/ui/input/pastedContent.ts +75 -0
  133. package/src/ui/input/rawArrowKeys.ts +28 -0
  134. package/src/ui/input/slashCommands.ts +43 -0
  135. package/src/ui/input/useStdinRawModeLease.ts +23 -0
  136. package/src/ui/layout.ts +560 -0
  137. package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
  138. package/src/ui/panels/AuthPanel.tsx +149 -0
  139. package/src/ui/panels/BackendPicker.tsx +28 -0
  140. package/src/ui/panels/ModePicker.tsx +31 -0
  141. package/src/ui/panels/ModelPicker.tsx +31 -0
  142. package/src/ui/panels/ModelPickerScreen.tsx +761 -0
  143. package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
  144. package/src/ui/panels/Panel.tsx +51 -0
  145. package/src/ui/panels/PermissionsPanel.tsx +78 -0
  146. package/src/ui/panels/PlanActionPicker.tsx +187 -0
  147. package/src/ui/panels/ProviderPicker.tsx +753 -0
  148. package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
  149. package/src/ui/panels/ReasoningPicker.tsx +46 -0
  150. package/src/ui/panels/ResumePicker.tsx +90 -0
  151. package/src/ui/panels/SelectionPanel.tsx +138 -0
  152. package/src/ui/panels/SettingsPanel.tsx +156 -0
  153. package/src/ui/panels/TextEntryPanel.tsx +139 -0
  154. package/src/ui/panels/ThemePicker.tsx +32 -0
  155. package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
  156. package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
  157. package/src/ui/panels/responsivePickerViewport.ts +64 -0
  158. package/src/ui/render/Markdown.tsx +331 -0
  159. package/src/ui/render/diffRenderer.ts +116 -0
  160. package/src/ui/render/logoVariants.ts +113 -0
  161. package/src/ui/render/modeDisplay.ts +52 -0
  162. package/src/ui/render/outputPipeline.ts +64 -0
  163. package/src/ui/render/runtimeDisplay.ts +128 -0
  164. package/src/ui/render/terminalAnswerFormat.ts +128 -0
  165. package/src/ui/render/textLayout.ts +392 -0
  166. package/src/ui/theme.tsx +274 -0
  167. package/src/ui/themeFlow.ts +41 -0
  168. package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
  169. package/src/ui/timeline/AgentBlock.tsx +130 -0
  170. package/src/ui/timeline/StaticIntroItem.tsx +54 -0
  171. package/src/ui/timeline/ThinkingBlock.tsx +100 -0
  172. package/src/ui/timeline/Timeline.tsx +1410 -0
  173. package/src/ui/timeline/TranscriptShell.tsx +302 -0
  174. package/src/ui/timeline/TurnGroup.tsx +673 -0
  175. package/src/ui/timeline/layoutListWindow.ts +145 -0
  176. package/src/ui/timeline/liveViewportWindow.ts +68 -0
  177. package/src/ui/timeline/progressEntries.ts +156 -0
  178. package/src/ui/timeline/runActivityView.ts +37 -0
  179. package/src/ui/timeline/staticTranscriptCache.ts +174 -0
  180. package/src/ui/timeline/streamCoalesce.ts +53 -0
  181. package/src/ui/timeline/timelineMeasure.ts +3273 -0
  182. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,259 @@
1
+ import { statSync } from "fs";
2
+ import { basename, join, resolve } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { resolveWorkspacePath } from "./workspaceGuard.js";
5
+ import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
6
+ import { UBUME_CHANNEL_ENV, LOCAL_DEV_CHANNEL, isLocalDevChannel } from "../version/channel.js";
7
+
8
+ export type LaunchKind = "installed-bin" | "dev-run";
9
+
10
+ export interface LaunchContext {
11
+ workspaceRoot: string;
12
+ packageRoot: string;
13
+ launchKind: LaunchKind;
14
+ launcherScriptPath?: string;
15
+ relaunchExecutable: string;
16
+ relaunchArgs: string[];
17
+ }
18
+
19
+ export interface ResolveLaunchContextOptions {
20
+ env?: NodeJS.ProcessEnv;
21
+ workspaceRoot?: string;
22
+ packageRoot?: string;
23
+ execPath?: string;
24
+ hasBunRuntime?: boolean;
25
+ forwardArgs?: string[];
26
+ }
27
+
28
+ export interface WorkspaceCommandContext {
29
+ root: string;
30
+ summaryMessage: string;
31
+ }
32
+
33
+ export interface WorkspaceRelaunchPlan {
34
+ executable: string;
35
+ args: string[];
36
+ cwd: string;
37
+ env: NodeJS.ProcessEnv;
38
+ targetWorkspaceRoot: string;
39
+ }
40
+
41
+ export type WorkspaceRelaunchPlanResult =
42
+ | { ok: true; plan: WorkspaceRelaunchPlan }
43
+ | { ok: false; message: string };
44
+
45
+ const ENV_KEYS = {
46
+ workspaceRoot: "CODEX_WORKSPACE_ROOT",
47
+ launchKind: "UBUME_LAUNCH_KIND",
48
+ packageRoot: "UBUME_PACKAGE_ROOT",
49
+ launcherScript: "UBUME_LAUNCHER_SCRIPT",
50
+ relaunchExecutable: "UBUME_RELAUNCH_EXECUTABLE",
51
+ relaunchArgs: "UBUME_RELAUNCH_ARGS",
52
+ } as const;
53
+
54
+ function getDefaultPackageRoot(): string {
55
+ return normalizeWorkspaceRoot(resolve(fileURLToPath(new URL(".", import.meta.url)), "..", ".."));
56
+ }
57
+
58
+ function parseRelaunchArgs(raw: string | undefined): string[] | null {
59
+ const value = raw?.trim();
60
+ if (!value) return null;
61
+
62
+ try {
63
+ const parsed = JSON.parse(value);
64
+ if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string")) {
65
+ return null;
66
+ }
67
+
68
+ return parsed;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ function isBunExecutable(execPath: string): boolean {
75
+ return basename(execPath).toLowerCase().startsWith("bun");
76
+ }
77
+
78
+ function getDevRelaunchExecutable(execPath: string, hasBunRuntime: boolean): string {
79
+ if (hasBunRuntime || isBunExecutable(execPath)) {
80
+ return execPath;
81
+ }
82
+
83
+ return "bun";
84
+ }
85
+
86
+ function buildInstalledRelaunchArgs(launcherScriptPath: string, forwardArgs: readonly string[]): string[] {
87
+ return [launcherScriptPath, ...forwardArgs];
88
+ }
89
+
90
+ function buildDevRelaunchArgs(packageRoot: string, forwardArgs: readonly string[]): string[] {
91
+ const args = ["run", "--silent", join(packageRoot, "src", "index.tsx")];
92
+ if (forwardArgs.length > 0) {
93
+ args.push("--", ...forwardArgs);
94
+ }
95
+ return args;
96
+ }
97
+
98
+ export function resolveLaunchContext(options: ResolveLaunchContextOptions = {}): LaunchContext {
99
+ const env = options.env ?? process.env;
100
+ const workspaceRoot = normalizeWorkspaceRoot(
101
+ options.workspaceRoot ?? env[ENV_KEYS.workspaceRoot] ?? process.cwd(),
102
+ );
103
+ const packageRoot = normalizeWorkspaceRoot(
104
+ env[ENV_KEYS.packageRoot] ?? options.packageRoot ?? getDefaultPackageRoot(),
105
+ );
106
+ const launchKind = env[ENV_KEYS.launchKind] === "installed-bin" ? "installed-bin" : "dev-run";
107
+ const launcherScriptPath = env[ENV_KEYS.launcherScript]?.trim() || undefined;
108
+ const envRelaunchExecutable = env[ENV_KEYS.relaunchExecutable]?.trim();
109
+ const envRelaunchArgs = parseRelaunchArgs(env[ENV_KEYS.relaunchArgs]);
110
+ const execPath = options.execPath ?? process.execPath;
111
+ const hasBunRuntime = options.hasBunRuntime ?? Boolean(process.versions.bun);
112
+ const forwardArgs = options.forwardArgs ?? process.argv.slice(2);
113
+
114
+ if (launchKind === "installed-bin") {
115
+ const resolvedLauncherScript = launcherScriptPath ?? join(packageRoot, "bin", "ubume.js");
116
+
117
+ return {
118
+ workspaceRoot,
119
+ packageRoot,
120
+ launchKind,
121
+ launcherScriptPath: resolvedLauncherScript,
122
+ relaunchExecutable: envRelaunchExecutable || execPath,
123
+ relaunchArgs: envRelaunchArgs && envRelaunchArgs.length > 0
124
+ ? envRelaunchArgs
125
+ : buildInstalledRelaunchArgs(resolvedLauncherScript, forwardArgs),
126
+ };
127
+ }
128
+
129
+ return {
130
+ workspaceRoot,
131
+ packageRoot,
132
+ launchKind: "dev-run",
133
+ launcherScriptPath,
134
+ relaunchExecutable: envRelaunchExecutable || getDevRelaunchExecutable(execPath, hasBunRuntime),
135
+ relaunchArgs: envRelaunchArgs && envRelaunchArgs.length > 0
136
+ ? envRelaunchArgs
137
+ : buildDevRelaunchArgs(packageRoot, forwardArgs),
138
+ };
139
+ }
140
+
141
+ export function buildWorkspaceCommandContext(launchContext: LaunchContext): WorkspaceCommandContext {
142
+ return {
143
+ root: launchContext.workspaceRoot,
144
+ summaryMessage: buildWorkspaceStatusMessage(launchContext),
145
+ };
146
+ }
147
+
148
+ export function buildWorkspaceStatusMessage(launchContext: LaunchContext): string {
149
+ const lines = [
150
+ "Active workspace:",
151
+ ` ${launchContext.workspaceRoot}`,
152
+ "",
153
+ `Launch mode: ${launchContext.launchKind === "installed-bin" ? "installed ubume" : "dev/repo launch"}`,
154
+ "",
155
+ ];
156
+
157
+ if (launchContext.launchKind === "dev-run") {
158
+ lines.push(
159
+ "This session was started from a local dev launch.",
160
+ "Recommended setup:",
161
+ " npm run install:dev-bin",
162
+ " which ubume-dev",
163
+ " cd <target-folder>",
164
+ " ubume-dev",
165
+ "",
166
+ "Quick recovery from here:",
167
+ " /workspace relaunch <path>",
168
+ );
169
+ } else {
170
+ lines.push(
171
+ "This session is locked to the folder where you launched ubume.",
172
+ "Use /workspace relaunch <path> to restart into another folder from inside this UI.",
173
+ );
174
+ }
175
+
176
+ return lines.join("\n");
177
+ }
178
+
179
+ export function buildDevLaunchNotice(launchContext: LaunchContext): string | null {
180
+ if (launchContext.launchKind !== "dev-run") {
181
+ return null;
182
+ }
183
+
184
+ return [
185
+ "Ready. Type a prompt, run !shell, or use /command.",
186
+ "Tip: /workspace relaunch <path>",
187
+ ].join("\n");
188
+ }
189
+
190
+ export function guardWorkspaceRelaunch(busy: boolean): { allowed: boolean; message?: string } {
191
+ if (!busy) {
192
+ return { allowed: true };
193
+ }
194
+
195
+ return {
196
+ allowed: false,
197
+ message: "Finish the current run before relaunching into another workspace.",
198
+ };
199
+ }
200
+
201
+ function buildRelaunchEnv(
202
+ launchContext: LaunchContext,
203
+ targetWorkspaceRoot: string,
204
+ baseEnv: NodeJS.ProcessEnv,
205
+ ): NodeJS.ProcessEnv {
206
+ return {
207
+ ...baseEnv,
208
+ [ENV_KEYS.workspaceRoot]: targetWorkspaceRoot,
209
+ [ENV_KEYS.launchKind]: launchContext.launchKind,
210
+ [ENV_KEYS.packageRoot]: launchContext.packageRoot,
211
+ [ENV_KEYS.launcherScript]: launchContext.launcherScriptPath ?? "",
212
+ [ENV_KEYS.relaunchExecutable]: launchContext.relaunchExecutable,
213
+ [ENV_KEYS.relaunchArgs]: JSON.stringify(launchContext.relaunchArgs),
214
+ [UBUME_CHANNEL_ENV]: isLocalDevChannel(baseEnv) ? LOCAL_DEV_CHANNEL : baseEnv[UBUME_CHANNEL_ENV],
215
+ };
216
+ }
217
+
218
+ export function createWorkspaceRelaunchPlan(
219
+ targetInput: string,
220
+ launchContext: LaunchContext,
221
+ baseEnv: NodeJS.ProcessEnv = process.env,
222
+ ): WorkspaceRelaunchPlanResult {
223
+ const trimmedTarget = targetInput.trim();
224
+ if (!trimmedTarget) {
225
+ return { ok: false, message: "Usage: /workspace relaunch <path>" };
226
+ }
227
+
228
+ const targetWorkspaceRoot = normalizeWorkspaceRoot(
229
+ resolveWorkspacePath(trimmedTarget, launchContext.workspaceRoot),
230
+ );
231
+
232
+ let stats: ReturnType<typeof statSync>;
233
+ try {
234
+ stats = statSync(targetWorkspaceRoot);
235
+ } catch {
236
+ return {
237
+ ok: false,
238
+ message: `Workspace relaunch failed: ${targetWorkspaceRoot} does not exist.`,
239
+ };
240
+ }
241
+
242
+ if (!stats.isDirectory()) {
243
+ return {
244
+ ok: false,
245
+ message: `Workspace relaunch failed: ${targetWorkspaceRoot} is not a directory.`,
246
+ };
247
+ }
248
+
249
+ return {
250
+ ok: true,
251
+ plan: {
252
+ executable: launchContext.relaunchExecutable,
253
+ args: launchContext.relaunchArgs,
254
+ cwd: targetWorkspaceRoot,
255
+ env: buildRelaunchEnv(launchContext, targetWorkspaceRoot, baseEnv),
256
+ targetWorkspaceRoot,
257
+ },
258
+ };
259
+ }
@@ -0,0 +1,135 @@
1
+ import { createHash } from "crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ import { isNoiseLine } from "../providers/codexTranscript.js";
6
+ import { sanitizeTerminalOutput } from "../terminal/terminalSanitize.js";
7
+ import { resolveUbumeDataDir } from "./appData.js";
8
+
9
+ type Platform = "win32" | "darwin" | "linux" | string;
10
+
11
+ const SECTION_LINE_RE = /^\s*(?:#{1,3}\s+)?(?:\*\*)?([A-Za-z][A-Za-z0-9 /&-]{0,48})(?:\*\*)?:?\s*$/;
12
+ const ABSOLUTE_WINDOWS_PATH_RE = /[A-Za-z]:[\\/][^\s`),;\]]+/g;
13
+
14
+ function normalizePathSeparators(value: string): string {
15
+ return value.replace(/\\/g, "/");
16
+ }
17
+
18
+ function replaceAllLiteral(value: string, search: string, replacement: string): string {
19
+ if (!search) return value;
20
+ return value.split(search).join(replacement);
21
+ }
22
+
23
+ /**
24
+ * Strips absolute filesystem paths from plan text, replacing them with
25
+ * relative paths or truncated versions to protect user privacy.
26
+ */
27
+ export function hidePlanReviewFilesystemDetails(planText: string, workspaceRoot?: string | null): string {
28
+ let output = planText;
29
+ const normalizedRoot = workspaceRoot?.trim() ? normalizePathSeparators(workspaceRoot.trim()).replace(/\/+$/, "") : "";
30
+
31
+ if (normalizedRoot) {
32
+ output = replaceAllLiteral(output, workspaceRoot!.replace(/\\+$/, ""), "");
33
+ output = replaceAllLiteral(normalizePathSeparators(output), normalizedRoot, "");
34
+ output = output.replace(/(^|[\s(`])\/+([A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+)/g, "$1$2");
35
+ }
36
+
37
+ return output.replace(ABSOLUTE_WINDOWS_PATH_RE, (match) => {
38
+ const normalized = normalizePathSeparators(match);
39
+ const srcIndex = normalized.search(/(?:^|\/)(src|test|tests|docs|scripts|bin)\//);
40
+ if (srcIndex >= 0) {
41
+ return normalized.slice(normalized[srcIndex] === "/" ? srcIndex + 1 : srcIndex);
42
+ }
43
+ const parts = normalized.split("/").filter(Boolean);
44
+ return parts.slice(-2).join("/") || match;
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Normalizes plan markdown for consistent rendering, converting bold labels
50
+ * into proper headings and hiding filesystem details.
51
+ */
52
+ export function normalizePlanReviewMarkdown(planText: string, workspaceRoot?: string | null): string {
53
+ const sanitized = sanitizeTerminalOutput(hidePlanReviewFilesystemDetails(planText, workspaceRoot), {
54
+ preserveTabs: false,
55
+ tabSize: 2,
56
+ })
57
+ .replace(/\r\n/g, "\n")
58
+ .replace(/\r/g, "\n")
59
+ .replace(/\n{4,}/g, "\n\n\n")
60
+ .split("\n")
61
+ .filter((line) => !isNoiseLine(line))
62
+ .join("\n");
63
+
64
+ return sanitized
65
+ .split("\n")
66
+ .map((line) => {
67
+ const trimmed = line.trim();
68
+ const sectionMatch = SECTION_LINE_RE.exec(trimmed);
69
+ if (sectionMatch && !/^[-*]\s+/.test(trimmed) && !/^\d+\.\s+/.test(trimmed)) {
70
+ return `## ${sectionMatch[1]!.trim()}`;
71
+ }
72
+ return line;
73
+ })
74
+ .join("\n");
75
+ }
76
+
77
+ /**
78
+ * Resolve the directory where plan files are stored.
79
+ * Uses platform-appropriate app-data locations instead of the workspace.
80
+ */
81
+ export function resolvePlanDir(platformOverride?: Platform): string {
82
+ const envDir = process.env["UBUME_PLAN_DIR"] || process.env["CODEXA_PLAN_DIR"];
83
+ if (envDir) return envDir;
84
+
85
+ const platform = platformOverride ?? process.platform;
86
+
87
+ if (platform === "win32") {
88
+ const localAppData = process.env["LOCALAPPDATA"];
89
+ if (localAppData) return join(localAppData, "Ubume", "plans");
90
+ const appData = process.env["APPDATA"];
91
+ if (appData) return join(appData, "Ubume", "plans");
92
+ return join(homedir(), "AppData", "Local", "Ubume", "plans");
93
+ }
94
+
95
+ return join(resolveUbumeDataDir(platform), "plans");
96
+ }
97
+
98
+ // SHA-256 of the workspace path ensures filename uniqueness across projects with the same name.
99
+ function workspaceHash(workspaceRoot: string): string {
100
+ return createHash("sha256").update(workspaceRoot).digest("hex").slice(0, 8);
101
+ }
102
+
103
+ function safeTimestamp(): string {
104
+ return new Date().toISOString().replace(/[:.]/g, "-");
105
+ }
106
+
107
+ /**
108
+ * Save plan content to the app-data plan directory.
109
+ * Returns the written file path, or null on failure.
110
+ */
111
+ export function savePlan(content: string, workspaceRoot: string): string | null {
112
+ try {
113
+ const dir = resolvePlanDir();
114
+ mkdirSync(dir, { recursive: true });
115
+ const filename = `plan-${safeTimestamp()}-${workspaceHash(workspaceRoot)}.md`;
116
+ const filePath = join(dir, filename);
117
+ writeFileSync(filePath, content, "utf-8");
118
+ return filePath;
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Read plan content from a file path.
126
+ * Returns the content string, or null on failure.
127
+ */
128
+ export function readPlan(filePath: string): string | null {
129
+ try {
130
+ if (!existsSync(filePath)) return null;
131
+ return readFileSync(filePath, "utf-8");
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
@@ -0,0 +1,54 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ export interface ProjectInstructions {
5
+ path: string;
6
+ content: string;
7
+ }
8
+
9
+ export type ProjectInstructionsLoadResult =
10
+ | { status: "loaded"; instructions: ProjectInstructions }
11
+ | { status: "missing" }
12
+ | { status: "error"; path: string; message: string };
13
+
14
+ const PROJECT_INSTRUCTION_CANDIDATES = [
15
+ "AGENTS.md",
16
+ join(".codex", "AGENTS.md"),
17
+ ] as const;
18
+
19
+ export function loadProjectInstructions(workspaceRoot: string): ProjectInstructionsLoadResult {
20
+ let firstError: Extract<ProjectInstructionsLoadResult, { status: "error" }> | null = null;
21
+
22
+ for (const relativePath of PROJECT_INSTRUCTION_CANDIDATES) {
23
+ const candidatePath = join(workspaceRoot, relativePath);
24
+ if (!existsSync(candidatePath)) {
25
+ continue;
26
+ }
27
+
28
+ try {
29
+ const content = readFileSync(candidatePath, "utf8").trim();
30
+ if (!content) {
31
+ continue;
32
+ }
33
+ return {
34
+ status: "loaded",
35
+ instructions: {
36
+ path: candidatePath,
37
+ content,
38
+ },
39
+ };
40
+ } catch (error) {
41
+ firstError ??= {
42
+ status: "error",
43
+ path: candidatePath,
44
+ message: error instanceof Error ? error.message : String(error),
45
+ };
46
+ }
47
+ }
48
+
49
+ if (firstError) {
50
+ return firstError;
51
+ }
52
+
53
+ return { status: "missing" };
54
+ }
@@ -0,0 +1,64 @@
1
+ import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ /** Workspace-relative home for agent throwaway files (test harnesses, probe scripts, logs). */
5
+ export const SCRATCH_RELATIVE_DIR = ".ubume/scratch";
6
+ export const LEGACY_SCRATCH_RELATIVE_DIR = ".codexa/scratch";
7
+
8
+ const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
9
+ const SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
10
+
11
+ export interface SessionScratchDir {
12
+ absolutePath: string;
13
+ relativePath: string;
14
+ }
15
+
16
+ export function resolveScratchRoot(workspaceRoot: string): string {
17
+ return join(workspaceRoot, ".ubume", "scratch");
18
+ }
19
+
20
+ export function resolveLegacyScratchRoot(workspaceRoot: string): string {
21
+ return join(workspaceRoot, ".codexa", "scratch");
22
+ }
23
+
24
+ /**
25
+ * Create `.ubume/scratch/<sessionId>` inside the workspace. The scratch root
26
+ * carries its own `*` .gitignore so git ignores it without touching the
27
+ * project's ignore rules. It lives in the workspace (not the OS temp dir)
28
+ * because sandboxed shells only get a persistent writable workspace.
29
+ */
30
+ export function ensureSessionScratchDir(workspaceRoot: string, sessionId: string): SessionScratchDir {
31
+ if (!SAFE_SESSION_ID.test(sessionId)) throw new Error(`Unsafe scratch session id: ${sessionId}`);
32
+ const root = resolveScratchRoot(workspaceRoot);
33
+ mkdirSync(root, { recursive: true });
34
+ const gitignore = join(root, ".gitignore");
35
+ if (!existsSync(gitignore)) writeFileSync(gitignore, "*\n", "utf8");
36
+ const absolutePath = join(root, sessionId);
37
+ mkdirSync(absolutePath, { recursive: true });
38
+ return { absolutePath, relativePath: `${SCRATCH_RELATIVE_DIR}/${sessionId}` };
39
+ }
40
+
41
+ /** Best-effort removal of session scratch folders older than `maxAgeMs`. */
42
+ export function pruneStaleScratchDirs(
43
+ workspaceRoot: string,
44
+ options: { keep?: string; maxAgeMs?: number; now?: number } = {},
45
+ ): void {
46
+ for (const root of [resolveScratchRoot(workspaceRoot), resolveLegacyScratchRoot(workspaceRoot)]) {
47
+ const cutoff = (options.now ?? Date.now()) - (options.maxAgeMs ?? DEFAULT_MAX_AGE_MS);
48
+ let entries;
49
+ try {
50
+ entries = readdirSync(root, { withFileTypes: true });
51
+ } catch {
52
+ continue;
53
+ }
54
+ for (const entry of entries) {
55
+ if (!entry.isDirectory() || entry.name === options.keep) continue;
56
+ const dir = join(root, entry.name);
57
+ try {
58
+ if (statSync(dir).mtimeMs < cutoff) rmSync(dir, { recursive: true, force: true });
59
+ } catch {
60
+ // A locked or vanished folder must not break the run that triggered pruning.
61
+ }
62
+ }
63
+ }
64
+ }