praxis-agent 0.30.0 → 0.32.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/dist/application/claude-project-purge.js +18 -5
- package/dist/application/project-memory.d.ts +141 -0
- package/dist/application/project-memory.js +607 -0
- package/dist/application/session-service.d.ts +24 -0
- package/dist/application/session-service.js +412 -44
- package/dist/application/subagent-service.js +6 -0
- package/dist/cli/interactive.d.ts +3 -1
- package/dist/cli/interactive.js +37 -4
- package/dist/cli/protocol.d.ts +1 -0
- package/dist/cli/protocol.js +24 -0
- package/dist/cli/tui/hook-settings.js +14 -1
- package/dist/cli/tui/memory-files.d.ts +2 -1
- package/dist/cli/tui/memory-files.js +30 -30
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +144 -24
- package/dist/compatibility/claude/context.d.ts +6 -1
- package/dist/compatibility/claude/context.js +16 -2
- package/dist/compatibility/claude/dynamic-context.js +2 -1
- package/dist/compatibility/claude/paths.js +2 -13
- package/dist/compatibility/claude/shared-resources.d.ts +4 -4
- package/dist/compatibility/claude/shared-resources.js +14 -35
- package/dist/core/context-budget.d.ts +9 -8
- package/dist/core/context-budget.js +16 -21
- package/dist/core/project-memory.d.ts +19 -0
- package/dist/core/project-memory.js +58 -0
- package/dist/core/runtime.d.ts +10 -1
- package/dist/core/runtime.js +47 -3
- package/dist/hooks/claude-file-change-watcher.d.ts +43 -0
- package/dist/hooks/claude-file-change-watcher.js +185 -0
- package/dist/hooks/claude-hook-tools.d.ts +2 -0
- package/dist/hooks/claude-hook-tools.js +45 -6
- package/dist/hooks/claude-hooks.d.ts +14 -2
- package/dist/hooks/claude-hooks.js +230 -5
- package/dist/hooks/claude-session-environment.d.ts +16 -0
- package/dist/hooks/claude-session-environment.js +112 -0
- package/dist/maintenance/doctor.js +29 -5
- package/dist/mcp/claude-mcp-management.js +2 -0
- package/dist/persistence/native-resources.d.ts +3 -1
- package/dist/persistence/native-resources.js +19 -6
- package/dist/platform/path-containment.d.ts +2 -0
- package/dist/platform/path-containment.js +7 -0
- package/dist/platform/project-identity.d.ts +6 -0
- package/dist/platform/project-identity.js +66 -0
- package/dist/platform/project-memory-paths.d.ts +13 -0
- package/dist/platform/project-memory-paths.js +28 -0
- package/dist/platform/project-path-key.d.ts +2 -0
- package/dist/platform/project-path-key.js +16 -0
- package/dist/providers/fallback-provider.js +3 -1
- package/dist/tools/claude-task-tools.d.ts +5 -0
- package/dist/tools/claude-task-tools.js +15 -1
- package/dist/tools/local-tools.d.ts +2 -0
- package/dist/tools/local-tools.js +11 -4
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ClaudeEnvironmentHookEvent = 'Setup' | 'SessionStart' | 'CwdChanged' | 'FileChanged';
|
|
2
|
+
export interface ClaudeSessionEnvironmentOptions {
|
|
3
|
+
stateRoot: string;
|
|
4
|
+
warn?(message: string): void;
|
|
5
|
+
}
|
|
6
|
+
export declare class ClaudeSessionEnvironment {
|
|
7
|
+
private readonly options;
|
|
8
|
+
private readonly cache;
|
|
9
|
+
constructor(options: ClaudeSessionEnvironmentOptions);
|
|
10
|
+
hookFile(sessionId: string, event: ClaudeEnvironmentHookEvent, hookIndex: number): Promise<string | undefined>;
|
|
11
|
+
invalidate(sessionId: string): void;
|
|
12
|
+
clearCwdFiles(sessionId: string): Promise<void>;
|
|
13
|
+
environment(sessionId: string): Promise<Readonly<Record<string, string>> | undefined>;
|
|
14
|
+
private directory;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=claude-session-environment.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const ENV_FILE = /^(setup|sessionstart|cwdchanged|filechanged)-hook-(\d+)\.sh$/u;
|
|
4
|
+
const PRIORITY = {
|
|
5
|
+
setup: 0,
|
|
6
|
+
sessionstart: 1,
|
|
7
|
+
cwdchanged: 2,
|
|
8
|
+
filechanged: 3,
|
|
9
|
+
};
|
|
10
|
+
export class ClaudeSessionEnvironment {
|
|
11
|
+
options;
|
|
12
|
+
cache = new Map();
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
async hookFile(sessionId, event, hookIndex) {
|
|
17
|
+
if (process.platform === 'win32')
|
|
18
|
+
return undefined;
|
|
19
|
+
const directory = this.directory(sessionId);
|
|
20
|
+
await mkdir(directory, { recursive: true });
|
|
21
|
+
this.invalidate(sessionId);
|
|
22
|
+
return join(directory, `${event.toLowerCase()}-hook-${hookIndex}.sh`);
|
|
23
|
+
}
|
|
24
|
+
invalidate(sessionId) {
|
|
25
|
+
this.cache.delete(sessionId);
|
|
26
|
+
}
|
|
27
|
+
async clearCwdFiles(sessionId) {
|
|
28
|
+
const directory = this.directory(sessionId);
|
|
29
|
+
try {
|
|
30
|
+
const files = await readdir(directory);
|
|
31
|
+
await Promise.all(files
|
|
32
|
+
.filter((file) => ENV_FILE.test(file) &&
|
|
33
|
+
(file.startsWith('cwdchanged-') ||
|
|
34
|
+
file.startsWith('filechanged-')))
|
|
35
|
+
.map((file) => writeFile(join(directory, file), '')));
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code !== 'ENOENT') {
|
|
39
|
+
this.options.warn?.(`Could not clear hook environment: ${error instanceof Error ? error.message : String(error)}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.invalidate(sessionId);
|
|
43
|
+
}
|
|
44
|
+
async environment(sessionId) {
|
|
45
|
+
if (process.platform === 'win32')
|
|
46
|
+
return undefined;
|
|
47
|
+
const cached = this.cache.get(sessionId);
|
|
48
|
+
if (cached !== undefined)
|
|
49
|
+
return cached ?? undefined;
|
|
50
|
+
const directory = this.directory(sessionId);
|
|
51
|
+
const environment = Object.create(null);
|
|
52
|
+
try {
|
|
53
|
+
const files = (await readdir(directory))
|
|
54
|
+
.filter((file) => ENV_FILE.test(file))
|
|
55
|
+
.sort((left, right) => {
|
|
56
|
+
const leftMatch = left.match(ENV_FILE);
|
|
57
|
+
const rightMatch = right.match(ENV_FILE);
|
|
58
|
+
const priority = (PRIORITY[leftMatch?.[1] ?? ''] ?? 99) -
|
|
59
|
+
(PRIORITY[rightMatch?.[1] ?? ''] ?? 99);
|
|
60
|
+
if (priority !== 0)
|
|
61
|
+
return priority;
|
|
62
|
+
return Number(leftMatch?.[2] ?? 0) - Number(rightMatch?.[2] ?? 0);
|
|
63
|
+
});
|
|
64
|
+
for (const file of files) {
|
|
65
|
+
const path = join(directory, file);
|
|
66
|
+
const content = await readFile(path, 'utf8');
|
|
67
|
+
for (const [index, line] of content.split(/\r?\n/u).entries()) {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
70
|
+
continue;
|
|
71
|
+
const match = trimmed.match(/^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u);
|
|
72
|
+
const name = match?.[1];
|
|
73
|
+
const source = match?.[2];
|
|
74
|
+
const value = source === undefined ? undefined : literalExportValue(source);
|
|
75
|
+
if (!name || value === undefined) {
|
|
76
|
+
this.options.warn?.(`Ignored unsafe CLAUDE_ENV_FILE entry at ${path}:${index + 1}`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
environment[name] = value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code !== 'ENOENT') {
|
|
85
|
+
this.options.warn?.(`Could not load hook environment: ${error instanceof Error ? error.message : String(error)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const result = Object.keys(environment).length > 0 ? { ...environment } : null;
|
|
89
|
+
this.cache.set(sessionId, result);
|
|
90
|
+
return result ?? undefined;
|
|
91
|
+
}
|
|
92
|
+
directory(sessionId) {
|
|
93
|
+
return join(this.options.stateRoot, 'session-env', sessionId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function literalExportValue(source) {
|
|
97
|
+
const value = source.trim();
|
|
98
|
+
if (value === '')
|
|
99
|
+
return '';
|
|
100
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
101
|
+
const literal = value.slice(1, -1);
|
|
102
|
+
return literal.includes("'") ? undefined : literal;
|
|
103
|
+
}
|
|
104
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
105
|
+
const literal = value.slice(1, -1);
|
|
106
|
+
if (literal.includes('$') || literal.includes('`'))
|
|
107
|
+
return undefined;
|
|
108
|
+
return literal.replace(/\\(["\\])/gu, '$1');
|
|
109
|
+
}
|
|
110
|
+
return /^[A-Za-z0-9_./:@%+,=-]*$/u.test(value) ? value : undefined;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=claude-session-environment.js.map
|
|
@@ -17,6 +17,7 @@ import { ModelPricingRegistry } from '../core/usage.js';
|
|
|
17
17
|
import { detectInstalledClaudeVersion, } from '../platform/claude-version.js';
|
|
18
18
|
import { redactSensitiveText, sensitiveEnvironmentValues, } from '../platform/sensitive-data.js';
|
|
19
19
|
import { collectDoctorLocalDiagnostics, resolveDoctorUpdates, } from './doctor-diagnostic.js';
|
|
20
|
+
import { resolveProjectMemoryPolicy } from '../core/project-memory.js';
|
|
20
21
|
function isRecord(value) {
|
|
21
22
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
23
|
}
|
|
@@ -148,23 +149,36 @@ export async function runDoctor(options) {
|
|
|
148
149
|
const updatesPromise = resolveDoctorUpdates(diagnosticOptions, local.updates);
|
|
149
150
|
let settings;
|
|
150
151
|
let nativeResources;
|
|
152
|
+
const settingsResources = async () => {
|
|
153
|
+
settings ??=
|
|
154
|
+
dataPlane === 'claude'
|
|
155
|
+
? await loadClaudeSettings({ configRoot, cwd })
|
|
156
|
+
: await loadNativeSettings({ root: configRoot, cwd });
|
|
157
|
+
return settings;
|
|
158
|
+
};
|
|
159
|
+
const projectMemoryEnabled = async () => resolveProjectMemoryPolicy({
|
|
160
|
+
dataPlane,
|
|
161
|
+
settings: await settingsResources(),
|
|
162
|
+
environment: options.environment,
|
|
163
|
+
}).enabled;
|
|
151
164
|
const sharedResources = async () => {
|
|
165
|
+
const includeProjectMemory = await projectMemoryEnabled();
|
|
152
166
|
if (dataPlane === 'claude') {
|
|
153
167
|
return loadClaudeSharedResources({
|
|
154
168
|
configRoot,
|
|
155
169
|
cwd,
|
|
156
170
|
claudeStatePath: options.claudeStatePath,
|
|
171
|
+
includeProjectMemory,
|
|
157
172
|
});
|
|
158
173
|
}
|
|
159
174
|
nativeResources ??= await loadNativeSharedResources({
|
|
160
175
|
root: configRoot,
|
|
161
176
|
cwd,
|
|
177
|
+
environment: options.environment,
|
|
178
|
+
includeProjectMemory,
|
|
162
179
|
});
|
|
163
180
|
return nativeResources;
|
|
164
181
|
};
|
|
165
|
-
const settingsResources = async () => dataPlane === 'claude'
|
|
166
|
-
? loadClaudeSettings({ configRoot, cwd })
|
|
167
|
-
: loadNativeSettings({ root: configRoot, cwd });
|
|
168
182
|
const checks = [];
|
|
169
183
|
checks.push(await capture('installation', async () => {
|
|
170
184
|
const executablePath = await canonicalExecutable(options.executablePath);
|
|
@@ -350,11 +364,21 @@ export async function runDoctor(options) {
|
|
|
350
364
|
}));
|
|
351
365
|
checks.push(await capture('resources', async () => {
|
|
352
366
|
const resources = await sharedResources();
|
|
367
|
+
const includeProjectMemory = await projectMemoryEnabled();
|
|
353
368
|
if (dataPlane === 'native') {
|
|
354
|
-
await loadNativeContextResources({
|
|
369
|
+
await loadNativeContextResources({
|
|
370
|
+
root: configRoot,
|
|
371
|
+
cwd,
|
|
372
|
+
environment: options.environment,
|
|
373
|
+
includeProjectMemory,
|
|
374
|
+
});
|
|
355
375
|
}
|
|
356
376
|
else {
|
|
357
|
-
await loadClaudeContextResources({
|
|
377
|
+
await loadClaudeContextResources({
|
|
378
|
+
configRoot,
|
|
379
|
+
cwd,
|
|
380
|
+
includeProjectMemory,
|
|
381
|
+
});
|
|
358
382
|
}
|
|
359
383
|
validateClaudeExtensions([
|
|
360
384
|
...resources.commands,
|
|
@@ -191,11 +191,13 @@ export class ClaudeMcpManagement {
|
|
|
191
191
|
? await loadNativeSharedResources({
|
|
192
192
|
root: this.configRoot,
|
|
193
193
|
cwd: this.cwd,
|
|
194
|
+
includeProjectMemory: false,
|
|
194
195
|
})
|
|
195
196
|
: await loadClaudeSharedResources({
|
|
196
197
|
configRoot: this.configRoot,
|
|
197
198
|
cwd: this.cwd,
|
|
198
199
|
settingSources: ['user', 'project', 'local'],
|
|
200
|
+
includeProjectMemory: false,
|
|
199
201
|
});
|
|
200
202
|
const records = new Map();
|
|
201
203
|
for (const resource of resources.mcp) {
|
|
@@ -2,8 +2,10 @@ import type { ClaudeContextResources, ClaudeJsonResource, ClaudeSharedResources
|
|
|
2
2
|
export interface LoadNativeResourcesOptions {
|
|
3
3
|
root: string;
|
|
4
4
|
cwd: string;
|
|
5
|
+
environment?: Readonly<Record<string, string | undefined>>;
|
|
6
|
+
includeProjectMemory?: boolean;
|
|
5
7
|
}
|
|
6
8
|
export declare function loadNativeSettings({ root, cwd, }: LoadNativeResourcesOptions): Promise<ClaudeJsonResource[]>;
|
|
7
|
-
export declare function loadNativeSharedResources({ root, cwd, }: LoadNativeResourcesOptions): Promise<ClaudeSharedResources>;
|
|
9
|
+
export declare function loadNativeSharedResources({ root, cwd, environment, includeProjectMemory, }: LoadNativeResourcesOptions): Promise<ClaudeSharedResources>;
|
|
8
10
|
export declare function loadNativeContextResources(options: LoadNativeResourcesOptions): Promise<ClaudeContextResources>;
|
|
9
11
|
//# sourceMappingURL=native-resources.d.ts.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { resolveProjectMemoryPolicy } from '../core/project-memory.js';
|
|
4
|
+
import { resolveProjectMemoryDirectory } from '../platform/project-memory-paths.js';
|
|
4
5
|
async function text(path, scope) {
|
|
5
6
|
try {
|
|
6
7
|
return { path, scope, content: await readFile(path, 'utf8') };
|
|
@@ -56,13 +57,26 @@ export async function loadNativeSettings({ root, cwd, }) {
|
|
|
56
57
|
]);
|
|
57
58
|
return resources.filter((resource) => resource !== null);
|
|
58
59
|
}
|
|
59
|
-
export async function loadNativeSharedResources({ root, cwd, }) {
|
|
60
|
+
export async function loadNativeSharedResources({ root, cwd, environment = process.env, includeProjectMemory = true, }) {
|
|
60
61
|
const project = resolve(cwd, '.praxis');
|
|
61
|
-
const memoryRoot =
|
|
62
|
-
|
|
62
|
+
const memoryRoot = await resolveProjectMemoryDirectory({
|
|
63
|
+
dataPlane: 'native',
|
|
64
|
+
configRoot: root,
|
|
65
|
+
cwd,
|
|
66
|
+
});
|
|
67
|
+
const settings = await loadNativeSettings({ root, cwd });
|
|
68
|
+
const memoryEnabled = includeProjectMemory &&
|
|
69
|
+
resolveProjectMemoryPolicy({
|
|
70
|
+
dataPlane: 'native',
|
|
71
|
+
settings,
|
|
72
|
+
environment,
|
|
73
|
+
}).enabled;
|
|
74
|
+
const [userInstruction, projectInstruction, memory, skills, commands, agents, userMcp, projectMcp, localMcp,] = await Promise.all([
|
|
63
75
|
text(join(root, 'PRAXIS.md'), 'user'),
|
|
64
76
|
text(join(project, 'PRAXIS.md'), 'project'),
|
|
65
|
-
|
|
77
|
+
memoryEnabled
|
|
78
|
+
? files(memoryRoot, 'project', (name) => name.endsWith('.md'))
|
|
79
|
+
: Promise.resolve([]),
|
|
66
80
|
Promise.all([
|
|
67
81
|
files(join(root, 'skills'), 'user', (name) => name === 'SKILL.md'),
|
|
68
82
|
files(join(project, 'skills'), 'project', (name) => name === 'SKILL.md'),
|
|
@@ -75,7 +89,6 @@ export async function loadNativeSharedResources({ root, cwd, }) {
|
|
|
75
89
|
files(join(root, 'agents'), 'user', (name) => name.endsWith('.md')),
|
|
76
90
|
files(join(project, 'agents'), 'project', (name) => name.endsWith('.md')),
|
|
77
91
|
]).then(([user, local]) => [...user, ...local]),
|
|
78
|
-
loadNativeSettings({ root, cwd }),
|
|
79
92
|
json(join(root, 'mcp.json'), 'user'),
|
|
80
93
|
json(join(project, 'mcp.json'), 'project'),
|
|
81
94
|
json(join(project, 'mcp.local.json'), 'local'),
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
2
|
+
export function isPathWithin(root, candidate) {
|
|
3
|
+
const pathFromRoot = relative(resolve(root), resolve(candidate));
|
|
4
|
+
return (pathFromRoot === '' ||
|
|
5
|
+
(!pathFromRoot.startsWith('..') && !isAbsolute(pathFromRoot)));
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=path-containment.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the stable local identity for a project. Linked worktrees share the
|
|
3
|
+
* main repository identity; non-git directories use their canonical path.
|
|
4
|
+
*/
|
|
5
|
+
export declare function resolveProjectIdentity(cwd: string): Promise<string>;
|
|
6
|
+
//# sourceMappingURL=project-identity.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFile, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
async function canonicalPath(path) {
|
|
4
|
+
try {
|
|
5
|
+
return await realpath(path);
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error.code === 'ENOENT')
|
|
9
|
+
return resolve(path);
|
|
10
|
+
throw error;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function exists(path) {
|
|
14
|
+
try {
|
|
15
|
+
await stat(path);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === 'ENOENT')
|
|
20
|
+
return false;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function findGitRoot(cwd) {
|
|
25
|
+
let current = await canonicalPath(cwd);
|
|
26
|
+
while (!(await exists(join(current, '.git')))) {
|
|
27
|
+
const parent = dirname(current);
|
|
28
|
+
if (parent === current)
|
|
29
|
+
return null;
|
|
30
|
+
current = parent;
|
|
31
|
+
}
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
34
|
+
async function optionalFile(path) {
|
|
35
|
+
try {
|
|
36
|
+
return await readFile(path, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code === 'ENOENT')
|
|
40
|
+
return null;
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolves the stable local identity for a project. Linked worktrees share the
|
|
46
|
+
* main repository identity; non-git directories use their canonical path.
|
|
47
|
+
*/
|
|
48
|
+
export async function resolveProjectIdentity(cwd) {
|
|
49
|
+
const canonicalCwd = await canonicalPath(cwd);
|
|
50
|
+
const gitRoot = await findGitRoot(canonicalCwd);
|
|
51
|
+
if (!gitRoot)
|
|
52
|
+
return canonicalCwd;
|
|
53
|
+
const marker = join(gitRoot, '.git');
|
|
54
|
+
if ((await stat(marker)).isDirectory())
|
|
55
|
+
return gitRoot;
|
|
56
|
+
const gitFile = await readFile(marker, 'utf8');
|
|
57
|
+
const gitDirectoryValue = /^gitdir:\s*(.+)\s*$/mu.exec(gitFile)?.[1];
|
|
58
|
+
if (!gitDirectoryValue)
|
|
59
|
+
return gitRoot;
|
|
60
|
+
const gitDirectory = resolve(gitRoot, gitDirectoryValue);
|
|
61
|
+
const commonDirectory = await optionalFile(join(gitDirectory, 'commondir'));
|
|
62
|
+
if (!commonDirectory)
|
|
63
|
+
return gitRoot;
|
|
64
|
+
return dirname(await canonicalPath(resolve(gitDirectory, commonDirectory.trim())));
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=project-identity.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProjectMemoryDataPlane } from '../core/project-memory.js';
|
|
2
|
+
export interface ResolveProjectMemoryDirectoryOptions {
|
|
3
|
+
dataPlane: ProjectMemoryDataPlane;
|
|
4
|
+
configRoot: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveProjectMemoryDirectory({ dataPlane, configRoot, cwd, }: ResolveProjectMemoryDirectoryOptions): Promise<string>;
|
|
8
|
+
export declare function resolveProjectMemoryStatePath(options: {
|
|
9
|
+
dataPlane: ProjectMemoryDataPlane;
|
|
10
|
+
configRoot: string;
|
|
11
|
+
memoryDirectory: string;
|
|
12
|
+
}): string;
|
|
13
|
+
//# sourceMappingURL=project-memory-paths.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
|
+
import { resolveProjectIdentity } from './project-identity.js';
|
|
4
|
+
import { sanitizeProjectPath } from './project-path-key.js';
|
|
5
|
+
async function canonicalPath(path) {
|
|
6
|
+
try {
|
|
7
|
+
return await realpath(path);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error.code === 'ENOENT')
|
|
11
|
+
return resolve(path);
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function resolveProjectMemoryDirectory({ dataPlane, configRoot, cwd, }) {
|
|
16
|
+
const [root, identity] = await Promise.all([
|
|
17
|
+
canonicalPath(configRoot),
|
|
18
|
+
resolveProjectIdentity(cwd),
|
|
19
|
+
]);
|
|
20
|
+
const key = sanitizeProjectPath(identity);
|
|
21
|
+
return dataPlane === 'native'
|
|
22
|
+
? join(root, 'memory', key)
|
|
23
|
+
: join(root, 'projects', key, 'memory');
|
|
24
|
+
}
|
|
25
|
+
export function resolveProjectMemoryStatePath(options) {
|
|
26
|
+
return join(resolve(options.configRoot), options.dataPlane === 'native' ? 'state' : 'praxis', 'project-memory', basename(options.memoryDirectory), 'cursor.json');
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=project-memory-paths.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const MAX_SANITIZED_PROJECT_PATH_LENGTH = 200;
|
|
2
|
+
function stablePathHash(value) {
|
|
3
|
+
let hash = 0;
|
|
4
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
5
|
+
hash = Math.imul(hash, 31) + value.charCodeAt(index);
|
|
6
|
+
hash |= 0;
|
|
7
|
+
}
|
|
8
|
+
return Math.abs(hash);
|
|
9
|
+
}
|
|
10
|
+
export function sanitizeProjectPath(path) {
|
|
11
|
+
const sanitized = path.replace(/[^a-zA-Z0-9]/g, '-');
|
|
12
|
+
if (sanitized.length <= MAX_SANITIZED_PROJECT_PATH_LENGTH)
|
|
13
|
+
return sanitized;
|
|
14
|
+
return `${sanitized.slice(0, MAX_SANITIZED_PROJECT_PATH_LENGTH)}-${stablePathHash(path).toString(36)}`;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=project-path-key.js.map
|
|
@@ -2,7 +2,9 @@ import { modelProviderErrorKind, ModelProviderError, } from '../core/runtime.js'
|
|
|
2
2
|
const MAX_ATTEMPTS_PER_MODEL = 3;
|
|
3
3
|
const RETRY_DELAYS_MS = [500, 1_000];
|
|
4
4
|
function retryable(error) {
|
|
5
|
-
return error instanceof ModelProviderError &&
|
|
5
|
+
return (error instanceof ModelProviderError &&
|
|
6
|
+
error.retryable &&
|
|
7
|
+
modelProviderErrorKind(error) !== 'prompt_too_long');
|
|
6
8
|
}
|
|
7
9
|
/** Routes each complete request through Claude's retry-then-fallback policy. */
|
|
8
10
|
export class FallbackModelProvider {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ModelToolCall, ModelToolDefinition, ToolExecutionContext, ToolExecutionResult, ToolRegistry, RuntimeEventSink } from '../core/runtime.js';
|
|
2
2
|
import { type BackgroundBashSnapshot, type BackgroundBashToolResult } from '../application/background-bash-manager.js';
|
|
3
|
+
import { type ClaudeTask } from '../persistence/claude-task-store.js';
|
|
3
4
|
export interface ClaudeTaskToolRegistryOptions {
|
|
4
5
|
base: ToolRegistry;
|
|
5
6
|
cwd: string;
|
|
@@ -10,6 +11,10 @@ export interface ClaudeTaskToolRegistryOptions {
|
|
|
10
11
|
enabledTools?: readonly string[];
|
|
11
12
|
maxOutputBytes?: number;
|
|
12
13
|
eventSink?: RuntimeEventSink;
|
|
14
|
+
taskHooks?: {
|
|
15
|
+
created(task: Readonly<ClaudeTask>, signal?: AbortSignal): Promise<void>;
|
|
16
|
+
completed(task: Readonly<ClaudeTask>, signal?: AbortSignal): Promise<void>;
|
|
17
|
+
};
|
|
13
18
|
}
|
|
14
19
|
export declare class ClaudeTaskToolRegistry implements ToolRegistry {
|
|
15
20
|
private readonly options;
|
|
@@ -342,6 +342,13 @@ export class ClaudeTaskToolRegistry {
|
|
|
342
342
|
switch (call.name) {
|
|
343
343
|
case 'TaskCreate': {
|
|
344
344
|
const task = await this.store.create(this.createInput(call.input));
|
|
345
|
+
try {
|
|
346
|
+
await this.options.taskHooks?.created(task, context.signal);
|
|
347
|
+
}
|
|
348
|
+
catch (error) {
|
|
349
|
+
await this.store.update(task.id, { status: 'deleted' });
|
|
350
|
+
throw error;
|
|
351
|
+
}
|
|
345
352
|
return {
|
|
346
353
|
content: `Task #${task.id} created successfully: ${task.subject}`,
|
|
347
354
|
isError: false,
|
|
@@ -374,7 +381,14 @@ export class ClaudeTaskToolRegistry {
|
|
|
374
381
|
}
|
|
375
382
|
case 'TaskUpdate': {
|
|
376
383
|
const id = stringField(call.input, 'taskId');
|
|
377
|
-
const
|
|
384
|
+
const update = this.updateInput(call.input);
|
|
385
|
+
if (update.status === 'completed') {
|
|
386
|
+
const task = await this.store.get(id);
|
|
387
|
+
if (task && task.status !== 'completed') {
|
|
388
|
+
await this.options.taskHooks?.completed(task, context.signal);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const result = await this.store.update(id, update);
|
|
378
392
|
const missing = {
|
|
379
393
|
success: false,
|
|
380
394
|
taskId: id,
|
|
@@ -11,6 +11,7 @@ export interface LocalToolRegistryOptions {
|
|
|
11
11
|
maxShellTimeoutMs?: number;
|
|
12
12
|
enableReportFindings?: boolean;
|
|
13
13
|
environment?: Readonly<Record<string, string>>;
|
|
14
|
+
sessionEnvironment?: (sessionId: string) => Promise<Readonly<Record<string, string>> | undefined>;
|
|
14
15
|
sandbox?: BashSandboxRuntime;
|
|
15
16
|
homeDirectory?: string;
|
|
16
17
|
configRoot?: string;
|
|
@@ -44,6 +45,7 @@ export declare class LocalToolRegistry implements ToolRegistry {
|
|
|
44
45
|
private readonly processRunner;
|
|
45
46
|
private readonly enableReportFindings;
|
|
46
47
|
private readonly environment;
|
|
48
|
+
private readonly sessionEnvironment;
|
|
47
49
|
private readonly sandbox;
|
|
48
50
|
private readonly homeDirectory;
|
|
49
51
|
private readonly configRoot;
|
|
@@ -509,6 +509,7 @@ export class LocalToolRegistry {
|
|
|
509
509
|
processRunner;
|
|
510
510
|
enableReportFindings;
|
|
511
511
|
environment;
|
|
512
|
+
sessionEnvironment;
|
|
512
513
|
sandbox;
|
|
513
514
|
homeDirectory;
|
|
514
515
|
configRoot;
|
|
@@ -526,6 +527,7 @@ export class LocalToolRegistry {
|
|
|
526
527
|
this.maxShellTimeoutMs = options.maxShellTimeoutMs ?? 120_000;
|
|
527
528
|
this.enableReportFindings = options.enableReportFindings ?? false;
|
|
528
529
|
this.environment = options.environment;
|
|
530
|
+
this.sessionEnvironment = options.sessionEnvironment;
|
|
529
531
|
this.sandbox = options.sandbox;
|
|
530
532
|
this.homeDirectory = options.homeDirectory
|
|
531
533
|
? resolve(options.homeDirectory)
|
|
@@ -1258,18 +1260,21 @@ export class LocalToolRegistry {
|
|
|
1258
1260
|
throw new Error('Prepared Bash call has no timeout');
|
|
1259
1261
|
const rawCommand = stringInput(call.input, 'command');
|
|
1260
1262
|
this.assertProtectedBashCommand(rawCommand, context);
|
|
1261
|
-
const
|
|
1263
|
+
const sandboxPolicyInput = {
|
|
1262
1264
|
command: rawCommand,
|
|
1263
1265
|
...(call.input.dangerouslyDisableSandbox === true
|
|
1264
1266
|
? { dangerouslyDisableSandbox: true }
|
|
1265
1267
|
: {}),
|
|
1266
1268
|
};
|
|
1267
|
-
const sandboxed = this.sandbox?.shouldUseSandbox(
|
|
1269
|
+
const sandboxed = this.sandbox?.shouldUseSandbox(sandboxPolicyInput) ?? false;
|
|
1268
1270
|
const shell = commandShell();
|
|
1269
1271
|
let result;
|
|
1270
1272
|
try {
|
|
1273
|
+
const sessionEnvironment = context.sessionId
|
|
1274
|
+
? await this.sessionEnvironment?.(context.sessionId)
|
|
1275
|
+
: undefined;
|
|
1271
1276
|
const command = sandboxed
|
|
1272
|
-
? await this.sandbox?.wrapCommand(
|
|
1277
|
+
? await this.sandbox?.wrapCommand(sandboxPolicyInput, {
|
|
1273
1278
|
shell,
|
|
1274
1279
|
...(context.signal ? { signal: context.signal } : {}),
|
|
1275
1280
|
commandId: call.id,
|
|
@@ -1280,7 +1285,9 @@ export class LocalToolRegistry {
|
|
|
1280
1285
|
args: commandShellArguments(command ?? rawCommand),
|
|
1281
1286
|
timeoutMs: timeout,
|
|
1282
1287
|
cwd: this.currentCwd(context),
|
|
1283
|
-
...(this.environment
|
|
1288
|
+
...(this.environment || sessionEnvironment
|
|
1289
|
+
? { env: { ...this.environment, ...sessionEnvironment } }
|
|
1290
|
+
: {}),
|
|
1284
1291
|
...(context.signal ? { signal: context.signal } : {}),
|
|
1285
1292
|
});
|
|
1286
1293
|
}
|