ubume 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.
package/bin/codexa.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ubume",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Terminal UI for Ubume — TypeScript + Bun + Ink",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
],
|
|
48
48
|
"repository": {
|
|
49
49
|
"type": "git",
|
|
50
|
-
"url": "git+https://github.com/golba98/
|
|
50
|
+
"url": "git+https://github.com/golba98/Ubume.git"
|
|
51
51
|
},
|
|
52
52
|
"bugs": {
|
|
53
|
-
"url": "https://github.com/golba98/
|
|
53
|
+
"url": "https://github.com/golba98/Ubume/issues"
|
|
54
54
|
},
|
|
55
|
-
"homepage": "https://github.com/golba98/
|
|
55
|
+
"homepage": "https://github.com/golba98/Ubume#readme",
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"registry": "https://registry.npmjs.org"
|
|
58
58
|
},
|
package/src/config/buildInfo.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by scripts/gen-build-info.mjs — do not edit manually.
|
|
2
|
-
export const BUILD_COMMIT = "
|
|
3
|
-
export const APP_VERSION = "0.1.
|
|
2
|
+
export const BUILD_COMMIT = "cd0bbd0c106c85ce0610674130853ca0404b8967" as const;
|
|
3
|
+
export const APP_VERSION = "0.1.1" as const;
|
|
@@ -12,7 +12,13 @@ import { resolveDefaultMaxOutputTokens } from "../localOutputBudget.js";
|
|
|
12
12
|
import type { LocalHarnessSessionMetadata } from "../../workspace/conversationStore.js";
|
|
13
13
|
import { resolveUbumeWorkspaceDataDir } from "../../workspace/appData.js";
|
|
14
14
|
import { getShellWorkspaceGuardMessage, isPathInsideAllowedRoots } from "../../workspace/workspaceGuard.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
describeSessionScratchDir,
|
|
17
|
+
ensureSessionScratchDir,
|
|
18
|
+
mentionsScratchDir,
|
|
19
|
+
pruneStaleScratchDirs,
|
|
20
|
+
removeUnusedSessionScratchDir,
|
|
21
|
+
} from "../../workspace/scratchDir.js";
|
|
16
22
|
import { isDangerousShellCommand } from "../../agent/tools.js";
|
|
17
23
|
import { traceLocalStream } from "../../debug/localStreamDebug.js";
|
|
18
24
|
import {
|
|
@@ -229,7 +235,8 @@ function resolveDshBin(): string {
|
|
|
229
235
|
function prepareSessionScratch(request: ProviderChatRequest, sessionId: string, resumed: boolean): string | null {
|
|
230
236
|
if (resolveHarnessSandboxMode(request) === "read-only") return null;
|
|
231
237
|
try {
|
|
232
|
-
|
|
238
|
+
// Only name the folder here; the tool/policy bridge creates it once a tool targets it.
|
|
239
|
+
const scratch = describeSessionScratchDir(request.workspaceRoot, sessionId);
|
|
233
240
|
if (!resumed) pruneStaleScratchDirs(request.workspaceRoot, { keep: sessionId });
|
|
234
241
|
return `Scratch directory for this session: ${scratch.relativePath}/ (put every temporary test, debug, or probe file there, not in the project).`;
|
|
235
242
|
} catch (error) {
|
|
@@ -817,6 +824,13 @@ export class LocalHarnessProcess implements LocalHarnessRunner {
|
|
|
817
824
|
return { kind: "deny", reason: `Path is outside the active workspace: ${candidatePath}` };
|
|
818
825
|
}
|
|
819
826
|
}
|
|
827
|
+
if ([command, ...pathsFrom(args)].some(mentionsScratchDir)) {
|
|
828
|
+
try {
|
|
829
|
+
ensureSessionScratchDir(state.request.workspaceRoot, state.sessionId);
|
|
830
|
+
} catch (error) {
|
|
831
|
+
traceLocalStream("harness.scratch.unavailable", { sessionId: state.sessionId, error: error instanceof Error ? error.message : String(error) });
|
|
832
|
+
}
|
|
833
|
+
}
|
|
820
834
|
const signature = `${tool}:${command || pathsFrom(args).join(",")}`;
|
|
821
835
|
if (state.approvals.has(signature)) return { kind: "allow" };
|
|
822
836
|
if (state.request.runtime.policy.approvalPolicy === "on-request") return { kind: "ask", reason: `Allow ${commandFrom(tool, args)}?` };
|
|
@@ -963,6 +977,7 @@ export class LocalHarnessProcess implements LocalHarnessRunner {
|
|
|
963
977
|
state.settled = true;
|
|
964
978
|
state.abortCleanup();
|
|
965
979
|
this.active = null;
|
|
980
|
+
if (state.request?.workspaceRoot) removeUnusedSessionScratchDir(state.request.workspaceRoot, state.sessionId);
|
|
966
981
|
const completedMessages = [
|
|
967
982
|
...(state.request.conversationHistory ?? []),
|
|
968
983
|
{ role: "user", content: state.request.prompt },
|
|
@@ -985,6 +1000,8 @@ export class LocalHarnessProcess implements LocalHarnessRunner {
|
|
|
985
1000
|
state.settled = true;
|
|
986
1001
|
state.abortCleanup();
|
|
987
1002
|
this.active = null;
|
|
1003
|
+
// Best-effort cleanup must never keep a failed run from settling.
|
|
1004
|
+
if (state.request?.workspaceRoot) removeUnusedSessionScratchDir(state.request.workspaceRoot, state.sessionId);
|
|
988
1005
|
state.handlers.onLocalHarnessSession?.(null, state.sessionId);
|
|
989
1006
|
const child = this.child;
|
|
990
1007
|
const transport = this.transport;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, rmdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
/** Workspace-relative home for agent throwaway files (test harnesses, probe scripts, logs). */
|
|
@@ -7,6 +7,7 @@ export const LEGACY_SCRATCH_RELATIVE_DIR = ".codexa/scratch";
|
|
|
7
7
|
|
|
8
8
|
const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
9
|
const SAFE_SESSION_ID = /^[A-Za-z0-9_-]+$/;
|
|
10
|
+
const SCRATCH_GITIGNORE = ".gitignore";
|
|
10
11
|
|
|
11
12
|
export interface SessionScratchDir {
|
|
12
13
|
absolutePath: string;
|
|
@@ -21,21 +22,67 @@ export function resolveLegacyScratchRoot(workspaceRoot: string): string {
|
|
|
21
22
|
return join(workspaceRoot, ".codexa", "scratch");
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
/** Name the session scratch folder without touching the filesystem. */
|
|
26
|
+
export function describeSessionScratchDir(workspaceRoot: string, sessionId: string): SessionScratchDir {
|
|
27
|
+
if (!SAFE_SESSION_ID.test(sessionId)) throw new Error(`Unsafe scratch session id: ${sessionId}`);
|
|
28
|
+
return {
|
|
29
|
+
absolutePath: join(resolveScratchRoot(workspaceRoot), sessionId),
|
|
30
|
+
relativePath: `${SCRATCH_RELATIVE_DIR}/${sessionId}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True when a tool path or shell command targets the workspace scratch folder. */
|
|
35
|
+
export function mentionsScratchDir(value: string): boolean {
|
|
36
|
+
return value.replace(/\\/g, "/").includes(SCRATCH_RELATIVE_DIR);
|
|
37
|
+
}
|
|
38
|
+
|
|
24
39
|
/**
|
|
25
40
|
* Create `.ubume/scratch/<sessionId>` inside the workspace. The scratch root
|
|
26
41
|
* carries its own `*` .gitignore so git ignores it without touching the
|
|
27
42
|
* project's ignore rules. It lives in the workspace (not the OS temp dir)
|
|
28
|
-
* because sandboxed shells only get a persistent writable workspace.
|
|
43
|
+
* because sandboxed shells only get a persistent writable workspace. Call it
|
|
44
|
+
* only once a tool actually targets the folder, so unused sessions leave no
|
|
45
|
+
* trace in the project.
|
|
29
46
|
*/
|
|
30
47
|
export function ensureSessionScratchDir(workspaceRoot: string, sessionId: string): SessionScratchDir {
|
|
31
|
-
|
|
48
|
+
const scratch = describeSessionScratchDir(workspaceRoot, sessionId);
|
|
32
49
|
const root = resolveScratchRoot(workspaceRoot);
|
|
33
50
|
mkdirSync(root, { recursive: true });
|
|
34
|
-
const gitignore = join(root,
|
|
51
|
+
const gitignore = join(root, SCRATCH_GITIGNORE);
|
|
35
52
|
if (!existsSync(gitignore)) writeFileSync(gitignore, "*\n", "utf8");
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
mkdirSync(scratch.absolutePath, { recursive: true });
|
|
54
|
+
return scratch;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function removeDirIfEmpty(dir: string, ignoredEntries: readonly string[] = []): boolean {
|
|
58
|
+
let entries: string[];
|
|
59
|
+
try {
|
|
60
|
+
entries = readdirSync(dir);
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (entries.some((entry) => !ignoredEntries.includes(entry))) return false;
|
|
65
|
+
for (const entry of entries) rmSync(join(dir, entry), { force: true });
|
|
66
|
+
rmdirSync(dir);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Best-effort removal of an empty session scratch folder, then of the
|
|
72
|
+
* `.ubume/scratch` root (when only its .gitignore remains) and `.ubume`
|
|
73
|
+
* (when nothing else lives there), so a session that never used scratch
|
|
74
|
+
* leaves the project untouched.
|
|
75
|
+
*/
|
|
76
|
+
export function removeUnusedSessionScratchDir(workspaceRoot: string, sessionId: string): void {
|
|
77
|
+
try {
|
|
78
|
+
const scratch = describeSessionScratchDir(workspaceRoot, sessionId);
|
|
79
|
+
if (existsSync(scratch.absolutePath) && !removeDirIfEmpty(scratch.absolutePath)) return;
|
|
80
|
+
const root = resolveScratchRoot(workspaceRoot);
|
|
81
|
+
if (!removeDirIfEmpty(root, [SCRATCH_GITIGNORE])) return;
|
|
82
|
+
removeDirIfEmpty(join(workspaceRoot, ".ubume"));
|
|
83
|
+
} catch {
|
|
84
|
+
// A locked or vanished folder must not break the run that triggered cleanup.
|
|
85
|
+
}
|
|
39
86
|
}
|
|
40
87
|
|
|
41
88
|
/** Best-effort removal of session scratch folders older than `maxAgeMs`. */
|