memorix 1.4.6 → 1.5.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/CHANGELOG.md +38 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +225 -70
- package/dist/cli/memcode.js +0 -0
- package/dist/index.js +225 -70
- package/dist/maintenance-runner.js +183 -38
- package/dist/media-audio-runner.js +183 -38
- package/dist/media-video-runner.js +183 -38
- package/dist/memcode-runtime/CHANGELOG.md +38 -0
- package/dist/sdk.js +224 -69
- package/dist/types.d.ts +2 -0
- package/dist/vector-backfill-runner.js +183 -38
- package/docs/CONFIGURATION.md +12 -0
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/package.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +54 -54
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +82 -82
- package/src/cli/commands/message.ts +104 -104
- package/src/cli/commands/poll.ts +58 -58
- package/src/cli/commands/reasoning.ts +118 -118
- package/src/cli/commands/skills.ts +114 -114
- package/src/cli/commands/task.ts +167 -167
- package/src/cli/commands/transfer.ts +47 -47
- package/src/cli/index.ts +406 -406
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/config.ts +23 -2
- package/src/dashboard/project-classification.ts +64 -64
- package/src/embedding/api-provider.ts +105 -22
- package/src/embedding/provider.ts +2 -0
- package/src/git/hooks-path.ts +85 -85
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +17 -8
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/disclosure-policy.ts +135 -135
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +6 -3
- package/src/memory/formation/index.ts +2 -1
- package/src/memory/formation/resolve.ts +10 -6
- package/src/memory/formation/types.ts +3 -1
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/long-term.ts +7 -1
- package/src/memory/refs.ts +94 -94
- package/src/memory/secret-filter.ts +79 -79
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +343 -343
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/project/aliases.ts +374 -374
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/syncer.ts +235 -235
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/server.ts +14 -10
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/orama-store.ts +6 -4
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/team/event-bus.ts +76 -76
- package/src/team/handoff.ts +161 -161
- package/src/team/poll.ts +132 -132
- package/src/timeout.ts +67 -0
- package/src/types.ts +2 -0
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project classification helpers — shared by /api/projects and /api/identity.
|
|
3
|
-
*
|
|
4
|
-
* Three kinds:
|
|
5
|
-
* - 'real': genuine user projects (e.g. AVIDS2/memorix, github.com/org/repo)
|
|
6
|
-
* - 'temporary': test/demo/smoke/e2e scratch projects (local/task-*, local/smoke-*, etc.)
|
|
7
|
-
* - 'placeholder': unresolved / obviously broken IDs (placeholder/*, __unresolved__, System32)
|
|
8
|
-
*
|
|
9
|
-
* A 'dirty' project is one with a clearly broken canonical ID (System32 etc.).
|
|
10
|
-
* 'dirty' and 'temporary' are orthogonal axes:
|
|
11
|
-
* - local/task-abc → temporary, NOT dirty
|
|
12
|
-
* - placeholder/xxx → placeholder, dirty
|
|
13
|
-
* - System32\something → real-looking location but dirty (broken ID)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export type ProjectKind = 'real' | 'temporary' | 'placeholder';
|
|
17
|
-
|
|
18
|
-
/** Regex list — anything matching is temporary (scratch projects) */
|
|
19
|
-
const TEMPORARY_PATTERNS: RegExp[] = [
|
|
20
|
-
/^local\/task-/i,
|
|
21
|
-
/^local\/smoke-/i,
|
|
22
|
-
/^local\/release-smoke-/i,
|
|
23
|
-
/^local\/memorix-e2e-/i,
|
|
24
|
-
/^local\/orchestrate-/i,
|
|
25
|
-
/^local\/scratch-/i,
|
|
26
|
-
/^local\/tmp-/i,
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
/** Regex list — anything matching is placeholder/unresolved */
|
|
30
|
-
const PLACEHOLDER_PATTERNS: RegExp[] = [
|
|
31
|
-
/^__unresolved__$/,
|
|
32
|
-
/^placeholder\//i,
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
/** Regex list — IDs that indicate a broken canonical ID (dirty). */
|
|
36
|
-
const DIRTY_PATTERNS: RegExp[] = [
|
|
37
|
-
/^placeholder\//i,
|
|
38
|
-
/System32/i,
|
|
39
|
-
/Microsoft VS Code/i,
|
|
40
|
-
/node_modules/i,
|
|
41
|
-
/\.vscode/i,
|
|
42
|
-
/^local\/[A-Z]:\\/i,
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
export function classifyProjectId(id: string): ProjectKind {
|
|
46
|
-
if (!id) return 'placeholder';
|
|
47
|
-
if (PLACEHOLDER_PATTERNS.some(p => p.test(id))) return 'placeholder';
|
|
48
|
-
if (TEMPORARY_PATTERNS.some(p => p.test(id))) return 'temporary';
|
|
49
|
-
return 'real';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function isDirtyProjectId(id: string): boolean {
|
|
53
|
-
if (!id) return false;
|
|
54
|
-
return DIRTY_PATTERNS.some(p => p.test(id));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Friendly label for UI badges */
|
|
58
|
-
export function projectKindLabel(kind: ProjectKind): string {
|
|
59
|
-
switch (kind) {
|
|
60
|
-
case 'real': return 'real';
|
|
61
|
-
case 'temporary': return 'temporary';
|
|
62
|
-
case 'placeholder': return 'placeholder';
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Project classification helpers — shared by /api/projects and /api/identity.
|
|
3
|
+
*
|
|
4
|
+
* Three kinds:
|
|
5
|
+
* - 'real': genuine user projects (e.g. AVIDS2/memorix, github.com/org/repo)
|
|
6
|
+
* - 'temporary': test/demo/smoke/e2e scratch projects (local/task-*, local/smoke-*, etc.)
|
|
7
|
+
* - 'placeholder': unresolved / obviously broken IDs (placeholder/*, __unresolved__, System32)
|
|
8
|
+
*
|
|
9
|
+
* A 'dirty' project is one with a clearly broken canonical ID (System32 etc.).
|
|
10
|
+
* 'dirty' and 'temporary' are orthogonal axes:
|
|
11
|
+
* - local/task-abc → temporary, NOT dirty
|
|
12
|
+
* - placeholder/xxx → placeholder, dirty
|
|
13
|
+
* - System32\something → real-looking location but dirty (broken ID)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export type ProjectKind = 'real' | 'temporary' | 'placeholder';
|
|
17
|
+
|
|
18
|
+
/** Regex list — anything matching is temporary (scratch projects) */
|
|
19
|
+
const TEMPORARY_PATTERNS: RegExp[] = [
|
|
20
|
+
/^local\/task-/i,
|
|
21
|
+
/^local\/smoke-/i,
|
|
22
|
+
/^local\/release-smoke-/i,
|
|
23
|
+
/^local\/memorix-e2e-/i,
|
|
24
|
+
/^local\/orchestrate-/i,
|
|
25
|
+
/^local\/scratch-/i,
|
|
26
|
+
/^local\/tmp-/i,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Regex list — anything matching is placeholder/unresolved */
|
|
30
|
+
const PLACEHOLDER_PATTERNS: RegExp[] = [
|
|
31
|
+
/^__unresolved__$/,
|
|
32
|
+
/^placeholder\//i,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** Regex list — IDs that indicate a broken canonical ID (dirty). */
|
|
36
|
+
const DIRTY_PATTERNS: RegExp[] = [
|
|
37
|
+
/^placeholder\//i,
|
|
38
|
+
/System32/i,
|
|
39
|
+
/Microsoft VS Code/i,
|
|
40
|
+
/node_modules/i,
|
|
41
|
+
/\.vscode/i,
|
|
42
|
+
/^local\/[A-Z]:\\/i,
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export function classifyProjectId(id: string): ProjectKind {
|
|
46
|
+
if (!id) return 'placeholder';
|
|
47
|
+
if (PLACEHOLDER_PATTERNS.some(p => p.test(id))) return 'placeholder';
|
|
48
|
+
if (TEMPORARY_PATTERNS.some(p => p.test(id))) return 'temporary';
|
|
49
|
+
return 'real';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isDirtyProjectId(id: string): boolean {
|
|
53
|
+
if (!id) return false;
|
|
54
|
+
return DIRTY_PATTERNS.some(p => p.test(id));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Friendly label for UI badges */
|
|
58
|
+
export function projectKindLabel(kind: ProjectKind): string {
|
|
59
|
+
switch (kind) {
|
|
60
|
+
case 'real': return 'real';
|
|
61
|
+
case 'temporary': return 'temporary';
|
|
62
|
+
case 'placeholder': return 'placeholder';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -406,6 +406,16 @@ interface APIEmbeddingConfig {
|
|
|
406
406
|
requestedDimensions: number | null;
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
+
class EmbeddingAPIError extends Error {
|
|
410
|
+
readonly status: number;
|
|
411
|
+
|
|
412
|
+
constructor(status: number, detail: string) {
|
|
413
|
+
super(`Embedding API error (${status}): ${detail}`);
|
|
414
|
+
this.name = 'EmbeddingAPIError';
|
|
415
|
+
this.status = status;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
409
419
|
export interface APIEmbeddingProviderCreateOptions {
|
|
410
420
|
/**
|
|
411
421
|
* When false, only previously persisted dimension metadata may initialize
|
|
@@ -432,6 +442,14 @@ function getPreferredBatchSize(config: APIEmbeddingConfig): number {
|
|
|
432
442
|
function parseBatchLimit(error: unknown): number | null {
|
|
433
443
|
if (!(error instanceof Error)) return null;
|
|
434
444
|
|
|
445
|
+
// Only a provider-side request-shape error may trigger recursive batch
|
|
446
|
+
// splitting. Billing, authentication, quota, and upstream errors must fail
|
|
447
|
+
// once; retrying them multiplies latency without changing the request.
|
|
448
|
+
const status = error instanceof EmbeddingAPIError
|
|
449
|
+
? error.status
|
|
450
|
+
: Number(error.message.match(/Embedding API error \((\d{3})\)/i)?.[1] ?? 0);
|
|
451
|
+
if (status > 0 && status !== 400) return null;
|
|
452
|
+
|
|
435
453
|
const explicit = error.message.match(/should not be larger than\s+(\d+)/i);
|
|
436
454
|
if (explicit) return parseInt(explicit[1], 10);
|
|
437
455
|
|
|
@@ -712,16 +730,14 @@ export class APIEmbeddingProvider implements EmbeddingProvider {
|
|
|
712
730
|
}
|
|
713
731
|
} catch (error) {
|
|
714
732
|
const providerLimit = parseBatchLimit(error);
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
if (chunkTexts.length > 1 && fallbackSize < chunkTexts.length) {
|
|
733
|
+
if (providerLimit !== null && chunkTexts.length > 1 && providerLimit < chunkTexts.length) {
|
|
718
734
|
console.error(
|
|
719
|
-
`[memorix] Embedding batch too large for provider, retrying in chunks of ${
|
|
735
|
+
`[memorix] Embedding batch too large for provider, retrying in chunks of ${providerLimit}`,
|
|
720
736
|
);
|
|
721
|
-
for (let start = 0; start < chunkTexts.length; start +=
|
|
737
|
+
for (let start = 0; start < chunkTexts.length; start += providerLimit) {
|
|
722
738
|
await processChunk(
|
|
723
|
-
chunkTexts.slice(start, start +
|
|
724
|
-
chunkIndices.slice(start, start +
|
|
739
|
+
chunkTexts.slice(start, start + providerLimit),
|
|
740
|
+
chunkIndices.slice(start, start + providerLimit),
|
|
725
741
|
);
|
|
726
742
|
}
|
|
727
743
|
return;
|
|
@@ -842,6 +858,9 @@ async function fetchWithRetry(
|
|
|
842
858
|
? Math.max(100, Math.min(Math.floor(options.timeoutMs), 60_000))
|
|
843
859
|
: 10_000;
|
|
844
860
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
861
|
+
const signal = options.signal
|
|
862
|
+
? AbortSignal.any([options.signal, controller.signal])
|
|
863
|
+
: controller.signal;
|
|
845
864
|
let response: Response;
|
|
846
865
|
try {
|
|
847
866
|
response = await fetch(url, {
|
|
@@ -850,30 +869,94 @@ async function fetchWithRetry(
|
|
|
850
869
|
? { 'Content-Type': 'application/json', 'x-goog-api-key': apiKey }
|
|
851
870
|
: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
852
871
|
body: JSON.stringify(body),
|
|
853
|
-
signal
|
|
872
|
+
signal,
|
|
854
873
|
});
|
|
855
874
|
} catch (err: unknown) {
|
|
856
875
|
clearTimeout(timeout);
|
|
857
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
876
|
+
if (err instanceof Error && (err.name === 'AbortError' || controller.signal.aborted || options.signal?.aborted)) {
|
|
858
877
|
throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
|
|
859
878
|
}
|
|
860
879
|
throw err;
|
|
861
880
|
}
|
|
862
|
-
clearTimeout(timeout);
|
|
863
881
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
882
|
+
try {
|
|
883
|
+
if (response.ok) {
|
|
884
|
+
return await raceWithAbort(
|
|
885
|
+
response.json() as Promise<EmbeddingAPIResponse>,
|
|
886
|
+
signal,
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES) {
|
|
891
|
+
const delay = BASE_DELAY_MS * Math.pow(2, attempt);
|
|
892
|
+
const retryAfter = response.headers.get('retry-after');
|
|
893
|
+
const parsedRetryAfter = retryAfter ? parseInt(retryAfter, 10) * 1000 : NaN;
|
|
894
|
+
const waitMs = Number.isFinite(parsedRetryAfter) ? parsedRetryAfter : delay;
|
|
895
|
+
console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES} in ${waitMs}ms`);
|
|
896
|
+
await waitForRetry(waitMs, options.signal);
|
|
897
|
+
return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
|
|
898
|
+
}
|
|
867
899
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
900
|
+
const errorText = await raceWithAbort(
|
|
901
|
+
response.text().catch(() => 'unknown error'),
|
|
902
|
+
signal,
|
|
903
|
+
);
|
|
904
|
+
throw new EmbeddingAPIError(response.status, errorText);
|
|
905
|
+
} catch (err: unknown) {
|
|
906
|
+
if (err instanceof Error && (err.name === 'AbortError' || controller.signal.aborted || options.signal?.aborted)) {
|
|
907
|
+
throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
|
|
908
|
+
}
|
|
909
|
+
throw err;
|
|
910
|
+
} finally {
|
|
911
|
+
clearTimeout(timeout);
|
|
875
912
|
}
|
|
913
|
+
}
|
|
876
914
|
|
|
877
|
-
|
|
878
|
-
|
|
915
|
+
function abortError(): Error {
|
|
916
|
+
const error = new Error('Embedding API request aborted');
|
|
917
|
+
error.name = 'AbortError';
|
|
918
|
+
return error;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function raceWithAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
922
|
+
if (signal.aborted) return Promise.reject(abortError());
|
|
923
|
+
return new Promise<T>((resolve, reject) => {
|
|
924
|
+
const onAbort = () => {
|
|
925
|
+
signal.removeEventListener('abort', onAbort);
|
|
926
|
+
reject(abortError());
|
|
927
|
+
};
|
|
928
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
929
|
+
promise.then(
|
|
930
|
+
(value) => {
|
|
931
|
+
signal.removeEventListener('abort', onAbort);
|
|
932
|
+
resolve(value);
|
|
933
|
+
},
|
|
934
|
+
(error) => {
|
|
935
|
+
signal.removeEventListener('abort', onAbort);
|
|
936
|
+
reject(error);
|
|
937
|
+
},
|
|
938
|
+
);
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function waitForRetry(waitMs: number, signal?: AbortSignal): Promise<void> {
|
|
943
|
+
if (!signal) {
|
|
944
|
+
await new Promise<void>((resolve) => setTimeout(resolve, waitMs));
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const abortSignal = signal;
|
|
948
|
+
if (abortSignal.aborted) throw new Error('Embedding API retry aborted');
|
|
949
|
+
await new Promise<void>((resolve, reject) => {
|
|
950
|
+
const timer = setTimeout(done, waitMs);
|
|
951
|
+
const onAbort = () => {
|
|
952
|
+
clearTimeout(timer);
|
|
953
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
954
|
+
reject(new Error('Embedding API retry aborted'));
|
|
955
|
+
};
|
|
956
|
+
function done() {
|
|
957
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
958
|
+
resolve();
|
|
959
|
+
}
|
|
960
|
+
abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
961
|
+
});
|
|
879
962
|
}
|
|
@@ -34,6 +34,8 @@ export interface EmbeddingRequestOptions {
|
|
|
34
34
|
timeoutMs?: number;
|
|
35
35
|
/** Disable provider retries for latency-sensitive best-effort work. */
|
|
36
36
|
retry?: boolean;
|
|
37
|
+
/** Abort the request and any retry wait when the caller's budget expires. */
|
|
38
|
+
signal?: AbortSignal;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export type EmbeddingModality = 'text' | 'image' | 'audio' | 'video' | 'document';
|
package/src/git/hooks-path.ts
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Git hooks path resolver — worktree-safe.
|
|
3
|
-
*
|
|
4
|
-
* In a normal repo: .git is a directory → hooks at .git/hooks/
|
|
5
|
-
* In a git worktree: .git is a FILE containing "gitdir: /path/to/actual/git/dir"
|
|
6
|
-
* → hooks at that resolved path + /hooks/
|
|
7
|
-
*
|
|
8
|
-
* This utility ensures all hook install/uninstall/check operations
|
|
9
|
-
* work correctly in both scenarios.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { existsSync, readFileSync, statSync, mkdirSync } from 'node:fs';
|
|
13
|
-
import path from 'node:path';
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Resolve the actual .git directory path, following worktree indirection.
|
|
17
|
-
*
|
|
18
|
-
* @param projectRoot - The root of the working tree (where .git lives)
|
|
19
|
-
* @returns Absolute path to the real git dir, or null if no .git found
|
|
20
|
-
*/
|
|
21
|
-
export function resolveGitDir(projectRoot: string): string | null {
|
|
22
|
-
const dotGit = path.join(projectRoot, '.git');
|
|
23
|
-
|
|
24
|
-
if (!existsSync(dotGit)) return null;
|
|
25
|
-
|
|
26
|
-
const stat = statSync(dotGit);
|
|
27
|
-
|
|
28
|
-
if (stat.isDirectory()) {
|
|
29
|
-
// Normal repo — .git is a directory
|
|
30
|
-
return dotGit;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (stat.isFile()) {
|
|
34
|
-
// Worktree — .git is a file: "gitdir: /path/to/actual/git/dir"
|
|
35
|
-
try {
|
|
36
|
-
const content = readFileSync(dotGit, 'utf-8').trim();
|
|
37
|
-
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
38
|
-
if (match) {
|
|
39
|
-
const gitdir = match[1].trim();
|
|
40
|
-
// Resolve relative paths against the project root
|
|
41
|
-
const resolved = path.isAbsolute(gitdir)
|
|
42
|
-
? gitdir
|
|
43
|
-
: path.resolve(projectRoot, gitdir);
|
|
44
|
-
if (existsSync(resolved)) {
|
|
45
|
-
return resolved;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
} catch {
|
|
49
|
-
// Unreadable .git file — fall through
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Resolve the hooks directory for a project (read-only — does NOT create directories).
|
|
58
|
-
* Handles both normal repos and git worktrees.
|
|
59
|
-
*
|
|
60
|
-
* @param projectRoot - The root of the working tree
|
|
61
|
-
* @returns Object with hooksDir and hookPath for post-commit, or null if no .git found
|
|
62
|
-
*/
|
|
63
|
-
export function resolveHooksDir(projectRoot: string): { hooksDir: string; hookPath: string } | null {
|
|
64
|
-
const gitDir = resolveGitDir(projectRoot);
|
|
65
|
-
if (!gitDir) return null;
|
|
66
|
-
|
|
67
|
-
const hooksDir = path.join(gitDir, 'hooks');
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
hooksDir,
|
|
71
|
-
hookPath: path.join(hooksDir, 'post-commit'),
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Resolve hooks directory AND ensure it exists on disk.
|
|
77
|
-
* Use this only in write paths (hook install / autoHook).
|
|
78
|
-
*/
|
|
79
|
-
export function ensureHooksDir(projectRoot: string): { hooksDir: string; hookPath: string } | null {
|
|
80
|
-
const resolved = resolveHooksDir(projectRoot);
|
|
81
|
-
if (!resolved) return null;
|
|
82
|
-
|
|
83
|
-
mkdirSync(resolved.hooksDir, { recursive: true });
|
|
84
|
-
return resolved;
|
|
85
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Git hooks path resolver — worktree-safe.
|
|
3
|
+
*
|
|
4
|
+
* In a normal repo: .git is a directory → hooks at .git/hooks/
|
|
5
|
+
* In a git worktree: .git is a FILE containing "gitdir: /path/to/actual/git/dir"
|
|
6
|
+
* → hooks at that resolved path + /hooks/
|
|
7
|
+
*
|
|
8
|
+
* This utility ensures all hook install/uninstall/check operations
|
|
9
|
+
* work correctly in both scenarios.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, statSync, mkdirSync } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the actual .git directory path, following worktree indirection.
|
|
17
|
+
*
|
|
18
|
+
* @param projectRoot - The root of the working tree (where .git lives)
|
|
19
|
+
* @returns Absolute path to the real git dir, or null if no .git found
|
|
20
|
+
*/
|
|
21
|
+
export function resolveGitDir(projectRoot: string): string | null {
|
|
22
|
+
const dotGit = path.join(projectRoot, '.git');
|
|
23
|
+
|
|
24
|
+
if (!existsSync(dotGit)) return null;
|
|
25
|
+
|
|
26
|
+
const stat = statSync(dotGit);
|
|
27
|
+
|
|
28
|
+
if (stat.isDirectory()) {
|
|
29
|
+
// Normal repo — .git is a directory
|
|
30
|
+
return dotGit;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (stat.isFile()) {
|
|
34
|
+
// Worktree — .git is a file: "gitdir: /path/to/actual/git/dir"
|
|
35
|
+
try {
|
|
36
|
+
const content = readFileSync(dotGit, 'utf-8').trim();
|
|
37
|
+
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
38
|
+
if (match) {
|
|
39
|
+
const gitdir = match[1].trim();
|
|
40
|
+
// Resolve relative paths against the project root
|
|
41
|
+
const resolved = path.isAbsolute(gitdir)
|
|
42
|
+
? gitdir
|
|
43
|
+
: path.resolve(projectRoot, gitdir);
|
|
44
|
+
if (existsSync(resolved)) {
|
|
45
|
+
return resolved;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
// Unreadable .git file — fall through
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the hooks directory for a project (read-only — does NOT create directories).
|
|
58
|
+
* Handles both normal repos and git worktrees.
|
|
59
|
+
*
|
|
60
|
+
* @param projectRoot - The root of the working tree
|
|
61
|
+
* @returns Object with hooksDir and hookPath for post-commit, or null if no .git found
|
|
62
|
+
*/
|
|
63
|
+
export function resolveHooksDir(projectRoot: string): { hooksDir: string; hookPath: string } | null {
|
|
64
|
+
const gitDir = resolveGitDir(projectRoot);
|
|
65
|
+
if (!gitDir) return null;
|
|
66
|
+
|
|
67
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
hooksDir,
|
|
71
|
+
hookPath: path.join(hooksDir, 'post-commit'),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Resolve hooks directory AND ensure it exists on disk.
|
|
77
|
+
* Use this only in write paths (hook install / autoHook).
|
|
78
|
+
*/
|
|
79
|
+
export function ensureHooksDir(projectRoot: string): { hooksDir: string; hookPath: string } | null {
|
|
80
|
+
const resolved = resolveHooksDir(projectRoot);
|
|
81
|
+
if (!resolved) return null;
|
|
82
|
+
|
|
83
|
+
mkdirSync(resolved.hooksDir, { recursive: true });
|
|
84
|
+
return resolved;
|
|
85
|
+
}
|