@pi-ohm/references 0.6.4-dev.27705031416.1.1d8069f
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 +76 -0
- package/SPEC.md +186 -0
- package/dist/cache.d.ts +26 -0
- package/dist/cache.js +261 -0
- package/dist/config.d.ts +71 -0
- package/dist/config.js +119 -0
- package/dist/extension.d.ts +32 -0
- package/dist/extension.js +414 -0
- package/dist/references.d.ts +54 -0
- package/dist/references.js +180 -0
- package/dist/repository.d.ts +40 -0
- package/dist/repository.js +165 -0
- package/package.json +63 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { Result } from "better-result";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { loadConfig, pickConfig, registerConfig } from "@pi-ohm/core/config";
|
|
5
|
+
|
|
6
|
+
//#region src/config.ts
|
|
7
|
+
const LocalReferenceConfigSchema = Type.Object({
|
|
8
|
+
path: Type.String({ minLength: 1 }),
|
|
9
|
+
description: Type.Optional(Type.String()),
|
|
10
|
+
hidden: Type.Optional(Type.Boolean())
|
|
11
|
+
}, { additionalProperties: false });
|
|
12
|
+
const GitReferenceConfigSchema = Type.Object({
|
|
13
|
+
repository: Type.String({ minLength: 1 }),
|
|
14
|
+
branch: Type.Optional(Type.String()),
|
|
15
|
+
description: Type.Optional(Type.String()),
|
|
16
|
+
hidden: Type.Optional(Type.Boolean())
|
|
17
|
+
}, { additionalProperties: false });
|
|
18
|
+
const ReferenceEntryConfigSchema = Type.Union([
|
|
19
|
+
Type.String({ minLength: 1 }),
|
|
20
|
+
LocalReferenceConfigSchema,
|
|
21
|
+
GitReferenceConfigSchema
|
|
22
|
+
]);
|
|
23
|
+
const ReferencesConfigSchema = Type.Record(Type.String({ minLength: 1 }), ReferenceEntryConfigSchema);
|
|
24
|
+
const DEFAULT_REFERENCES_CONFIG = {};
|
|
25
|
+
const referencesConfigModule = registerConfig({
|
|
26
|
+
namespace: "references",
|
|
27
|
+
schema: ReferencesConfigSchema,
|
|
28
|
+
defaults: DEFAULT_REFERENCES_CONFIG,
|
|
29
|
+
merge(base, patch) {
|
|
30
|
+
return Result.ok(mergeReferencesConfig(base, patch));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
function isJsonMap(value) {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
function trimString(value) {
|
|
37
|
+
const trimmed = value.trim();
|
|
38
|
+
if (trimmed.length === 0) return undefined;
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
function optionalString(value) {
|
|
42
|
+
if (value === undefined) return undefined;
|
|
43
|
+
return trimString(value);
|
|
44
|
+
}
|
|
45
|
+
function normalizeEntry(entry) {
|
|
46
|
+
if (typeof entry === "string") return trimString(entry);
|
|
47
|
+
if ("path" in entry) {
|
|
48
|
+
const resolved = trimString(entry.path);
|
|
49
|
+
if (!resolved) return undefined;
|
|
50
|
+
return {
|
|
51
|
+
path: resolved,
|
|
52
|
+
...optionalString(entry.description) ? { description: optionalString(entry.description) } : {},
|
|
53
|
+
...entry.hidden !== undefined ? { hidden: entry.hidden } : {}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const repository = trimString(entry.repository);
|
|
57
|
+
if (!repository) return undefined;
|
|
58
|
+
return {
|
|
59
|
+
repository,
|
|
60
|
+
...optionalString(entry.branch) ? { branch: optionalString(entry.branch) } : {},
|
|
61
|
+
...optionalString(entry.description) ? { description: optionalString(entry.description) } : {},
|
|
62
|
+
...entry.hidden !== undefined ? { hidden: entry.hidden } : {}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function validReferenceAlias(name) {
|
|
66
|
+
return name.length > 0 && !/[/\s`,]/.test(name);
|
|
67
|
+
}
|
|
68
|
+
function mergeReferencesConfig(current, patch) {
|
|
69
|
+
const entries = Object.entries(patch).reduce((next, [rawName, rawEntry]) => {
|
|
70
|
+
const name = rawName.trim();
|
|
71
|
+
if (!validReferenceAlias(name)) return next;
|
|
72
|
+
const entry = normalizeEntry(rawEntry);
|
|
73
|
+
if (!entry) return next;
|
|
74
|
+
return {
|
|
75
|
+
...next,
|
|
76
|
+
[name]: entry
|
|
77
|
+
};
|
|
78
|
+
}, { ...current ?? DEFAULT_REFERENCES_CONFIG });
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
81
|
+
function isLocalReferenceConfig(value) {
|
|
82
|
+
if (!isJsonMap(value)) return false;
|
|
83
|
+
if (typeof value.path !== "string") return false;
|
|
84
|
+
if (value.description !== undefined && typeof value.description !== "string") return false;
|
|
85
|
+
if (value.hidden !== undefined && typeof value.hidden !== "boolean") return false;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function isGitReferenceConfig(value) {
|
|
89
|
+
if (!isJsonMap(value)) return false;
|
|
90
|
+
if (typeof value.repository !== "string") return false;
|
|
91
|
+
if (value.branch !== undefined && typeof value.branch !== "string") return false;
|
|
92
|
+
if (value.description !== undefined && typeof value.description !== "string") return false;
|
|
93
|
+
if (value.hidden !== undefined && typeof value.hidden !== "boolean") return false;
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
function isReferencesRuntimeConfig(value) {
|
|
97
|
+
if (!isJsonMap(value)) return false;
|
|
98
|
+
return Object.values(value).every((entry) => typeof entry === "string" || isLocalReferenceConfig(entry) || isGitReferenceConfig(entry));
|
|
99
|
+
}
|
|
100
|
+
async function loadReferencesConfig(cwd) {
|
|
101
|
+
const loaded = await loadConfig({
|
|
102
|
+
cwd: path.resolve(cwd),
|
|
103
|
+
modules: [referencesConfigModule]
|
|
104
|
+
});
|
|
105
|
+
if (Result.isError(loaded)) return Result.err(loaded.error);
|
|
106
|
+
const config = pickConfig({
|
|
107
|
+
loaded: loaded.value,
|
|
108
|
+
module: referencesConfigModule,
|
|
109
|
+
is: isReferencesRuntimeConfig
|
|
110
|
+
});
|
|
111
|
+
if (Result.isError(config)) return Result.err(config.error);
|
|
112
|
+
return Result.ok({
|
|
113
|
+
loaded: loaded.value,
|
|
114
|
+
config: config.value
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
export { GitReferenceConfigSchema, LocalReferenceConfigSchema, ReferenceEntryConfigSchema, ReferencesConfigSchema, isJsonMap, isReferencesRuntimeConfig, loadReferencesConfig, mergeReferencesConfig, referencesConfigModule, validReferenceAlias };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FileRepositoryReference, ReferenceErrorCode, ReferencesError, RemoteRepositoryReference, RepositoryReference, parseRemoteRepositoryReference, parseRepositoryReference, repositoryCacheIdentity, repositoryCachePath, sameRepositoryReference, validateBranch } from "./repository.js";
|
|
2
|
+
import { EnsureRepositoryInput, RepositoryCacheResult, defaultReferencesCacheRoot, ensureRepository } from "./cache.js";
|
|
3
|
+
import { GitReferenceConfig, GitReferenceConfigSchema, LocalReferenceConfig, LocalReferenceConfigSchema, ReferenceEntryConfig, ReferenceEntryConfigSchema, ReferencesConfigSchema, ReferencesRuntimeConfig, isJsonMap, isReferencesRuntimeConfig, loadReferencesConfig, mergeReferencesConfig, referencesConfigModule, validReferenceAlias } from "./config.js";
|
|
4
|
+
import { GitReferenceSource, LocalReferenceSource, MaterializeReferencesResult, ReferenceDiagnostic, ReferenceInfo, ReferenceSource, ResolvedReferences, materializeGitReferences, renderReferenceGuidance, resolveConfiguredReferences } from "./references.js";
|
|
5
|
+
import { AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
6
|
+
import { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
|
|
8
|
+
//#region src/extension.d.ts
|
|
9
|
+
interface ReferenceInvocation {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly token: string;
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly rootPath: string;
|
|
14
|
+
readonly relativePath: string | undefined;
|
|
15
|
+
readonly description: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
type AddAutocompleteProvider = ExtensionContext["ui"]["addAutocompleteProvider"];
|
|
18
|
+
interface AutocompleteHost {
|
|
19
|
+
addAutocompleteProvider: AddAutocompleteProvider;
|
|
20
|
+
}
|
|
21
|
+
declare function resolveReferenceToken(token: string, references: readonly ReferenceInfo[]): ReferenceInvocation | undefined;
|
|
22
|
+
declare function findReferenceInvocations(text: string, references: readonly ReferenceInfo[]): readonly ReferenceInvocation[];
|
|
23
|
+
declare function renderReferenceInvocation(invocations: readonly ReferenceInvocation[]): string | undefined;
|
|
24
|
+
declare function rewriteReferencePath(value: string | undefined, references: readonly ReferenceInfo[]): string | undefined;
|
|
25
|
+
declare function createReferencesAutocompleteProvider(current: AutocompleteProvider, getReferences: () => readonly ReferenceInfo[]): AutocompleteProvider;
|
|
26
|
+
declare function bridgeReferencesAutocomplete(input: {
|
|
27
|
+
readonly host: AutocompleteHost;
|
|
28
|
+
readonly getReferences: () => readonly ReferenceInfo[];
|
|
29
|
+
}): AddAutocompleteProvider;
|
|
30
|
+
declare function registerReferencesExtension(pi: ExtensionAPI): void;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { EnsureRepositoryInput, FileRepositoryReference, GitReferenceConfig, GitReferenceConfigSchema, GitReferenceSource, LocalReferenceConfig, LocalReferenceConfigSchema, LocalReferenceSource, MaterializeReferencesResult, ReferenceDiagnostic, ReferenceEntryConfig, ReferenceEntryConfigSchema, ReferenceErrorCode, ReferenceInfo, ReferenceInvocation, ReferenceSource, ReferencesConfigSchema, ReferencesError, ReferencesRuntimeConfig, RemoteRepositoryReference, RepositoryCacheResult, RepositoryReference, ResolvedReferences, bridgeReferencesAutocomplete, createReferencesAutocompleteProvider, registerReferencesExtension as default, defaultReferencesCacheRoot, ensureRepository, findReferenceInvocations, isJsonMap, isReferencesRuntimeConfig, loadReferencesConfig, materializeGitReferences, mergeReferencesConfig, parseRemoteRepositoryReference, parseRepositoryReference, referencesConfigModule, renderReferenceGuidance, renderReferenceInvocation, repositoryCacheIdentity, repositoryCachePath, resolveConfiguredReferences, resolveReferenceToken, rewriteReferencePath, sameRepositoryReference, validReferenceAlias, validateBranch };
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { GitReferenceConfigSchema, LocalReferenceConfigSchema, ReferenceEntryConfigSchema, ReferencesConfigSchema, isJsonMap, isReferencesRuntimeConfig, loadReferencesConfig, mergeReferencesConfig, referencesConfigModule, validReferenceAlias } from "./config.js";
|
|
2
|
+
import { ReferencesError, parseRemoteRepositoryReference, parseRepositoryReference, repositoryCacheIdentity, repositoryCachePath, sameRepositoryReference, validateBranch } from "./repository.js";
|
|
3
|
+
import { defaultReferencesCacheRoot, ensureRepository } from "./cache.js";
|
|
4
|
+
import { materializeGitReferences, renderReferenceGuidance, resolveConfiguredReferences } from "./references.js";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { Result } from "better-result";
|
|
8
|
+
import { Box, Text } from "@earendil-works/pi-tui";
|
|
9
|
+
import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
//#region src/extension.ts
|
|
12
|
+
const STATUS_KEY = "ohm-references";
|
|
13
|
+
const MAX_AUTOCOMPLETE_ITEMS = 30;
|
|
14
|
+
const AUTOCOMPLETE_DESCRIPTION_TAG = "[Ω:REF]";
|
|
15
|
+
const REFERENCE_MESSAGE_TYPE = "ohm-reference";
|
|
16
|
+
const REFERENCE_INVOCATION_BLURB = "The user inserted this project reference with @ autocomplete. Use the resolved path when reading or searching this referenced project.";
|
|
17
|
+
const EMPTY_REFERENCES_STATE = {
|
|
18
|
+
references: [],
|
|
19
|
+
diagnostics: []
|
|
20
|
+
};
|
|
21
|
+
const BRIDGED_HOSTS = new WeakMap();
|
|
22
|
+
function aliasValue(reference, relative) {
|
|
23
|
+
const suffix = relative ? `/${relative.replaceAll("\\", "/")}` : "";
|
|
24
|
+
return `@${reference.name}${suffix}`;
|
|
25
|
+
}
|
|
26
|
+
function extractReferenceToken(textBeforeCursor) {
|
|
27
|
+
const match = textBeforeCursor.match(/(?:^|[ \t])@([^\s@]*)$/);
|
|
28
|
+
return match?.[1];
|
|
29
|
+
}
|
|
30
|
+
function visibleReferences(references) {
|
|
31
|
+
return references.filter((reference) => reference.hidden !== true);
|
|
32
|
+
}
|
|
33
|
+
function taggedDescription(description) {
|
|
34
|
+
return `${AUTOCOMPLETE_DESCRIPTION_TAG} ${description}`;
|
|
35
|
+
}
|
|
36
|
+
function aliasItems(references, token) {
|
|
37
|
+
const normalized = token.toLowerCase();
|
|
38
|
+
return [...visibleReferences(references)].filter((reference) => reference.name.toLowerCase().includes(normalized)).sort((left, right) => left.name.localeCompare(right.name)).slice(0, MAX_AUTOCOMPLETE_ITEMS).map((reference) => ({
|
|
39
|
+
value: aliasValue(reference),
|
|
40
|
+
label: `@${reference.name}`,
|
|
41
|
+
description: reference.source.type === "git" ? taggedDescription(reference.source.repository) : taggedDescription(reference.source.path)
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
async function childItems(input) {
|
|
45
|
+
const parentTail = input.tail.endsWith("/") ? input.tail : path.dirname(input.tail);
|
|
46
|
+
const query = input.tail.endsWith("/") ? "" : path.basename(input.tail);
|
|
47
|
+
const parent = parentTail === "." ? input.reference.path : path.join(input.reference.path, parentTail);
|
|
48
|
+
const entries = await fs.readdir(parent, { withFileTypes: true }).then((items) => items, () => []);
|
|
49
|
+
return entries.filter((entry) => entry.name.toLowerCase().startsWith(query.toLowerCase())).sort((left, right) => {
|
|
50
|
+
if (left.isDirectory() && !right.isDirectory()) return -1;
|
|
51
|
+
if (!left.isDirectory() && right.isDirectory()) return 1;
|
|
52
|
+
return left.name.localeCompare(right.name);
|
|
53
|
+
}).slice(0, MAX_AUTOCOMPLETE_ITEMS).map((entry) => {
|
|
54
|
+
const relative = parentTail === "." ? entry.name : path.join(parentTail, entry.name);
|
|
55
|
+
const display = `@${input.reference.name}/${relative.replaceAll("\\", "/")}${entry.isDirectory() ? "/" : ""}`;
|
|
56
|
+
const relativeDisplay = `${relative.replaceAll("\\", "/")}${entry.isDirectory() ? "/" : ""}`;
|
|
57
|
+
return {
|
|
58
|
+
value: aliasValue(input.reference, `${relative}${entry.isDirectory() ? "/" : ""}`),
|
|
59
|
+
label: display,
|
|
60
|
+
description: taggedDescription(relativeDisplay)
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function escapeXmlAttribute(value) {
|
|
65
|
+
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
66
|
+
}
|
|
67
|
+
function escapeXmlText(value) {
|
|
68
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
69
|
+
}
|
|
70
|
+
function resolveReferenceTarget(input) {
|
|
71
|
+
if (!input.suffix) return {
|
|
72
|
+
path: input.reference.path,
|
|
73
|
+
relativePath: undefined
|
|
74
|
+
};
|
|
75
|
+
const withoutLeadingSlash = input.suffix.startsWith("/") ? input.suffix.slice(1) : input.suffix;
|
|
76
|
+
if (!withoutLeadingSlash) return {
|
|
77
|
+
path: input.reference.path,
|
|
78
|
+
relativePath: undefined
|
|
79
|
+
};
|
|
80
|
+
if (withoutLeadingSlash.includes("\0") || withoutLeadingSlash.includes("\\")) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const normalized = path.normalize(withoutLeadingSlash);
|
|
84
|
+
if (normalized === "." || path.isAbsolute(normalized)) return undefined;
|
|
85
|
+
const target = path.join(input.reference.path, normalized);
|
|
86
|
+
const relative = path.relative(input.reference.path, target);
|
|
87
|
+
if (relative === "") return {
|
|
88
|
+
path: target,
|
|
89
|
+
relativePath: undefined
|
|
90
|
+
};
|
|
91
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
path: target,
|
|
96
|
+
relativePath: normalized.replaceAll("\\", "/")
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function resolveReferenceToken(token, references) {
|
|
100
|
+
if (!token.startsWith("@")) return undefined;
|
|
101
|
+
const slashIndex = token.indexOf("/");
|
|
102
|
+
const name = slashIndex === -1 ? token.slice(1) : token.slice(1, slashIndex);
|
|
103
|
+
const suffix = slashIndex === -1 ? undefined : token.slice(slashIndex + 1);
|
|
104
|
+
const reference = references.find((candidate) => candidate.name === name);
|
|
105
|
+
if (!reference) return undefined;
|
|
106
|
+
const target = resolveReferenceTarget({
|
|
107
|
+
reference,
|
|
108
|
+
suffix
|
|
109
|
+
});
|
|
110
|
+
if (!target) return undefined;
|
|
111
|
+
return {
|
|
112
|
+
name: reference.name,
|
|
113
|
+
token,
|
|
114
|
+
path: target.path,
|
|
115
|
+
rootPath: reference.path,
|
|
116
|
+
relativePath: target.relativePath,
|
|
117
|
+
description: reference.description
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function resolveReferenceTokenInText(token, references) {
|
|
121
|
+
const direct = resolveReferenceToken(token, references);
|
|
122
|
+
if (direct) return direct;
|
|
123
|
+
const trimmed = token.replace(/[.:;!?)}\]]+$/u, "");
|
|
124
|
+
if (trimmed === token) return undefined;
|
|
125
|
+
return resolveReferenceToken(trimmed, references);
|
|
126
|
+
}
|
|
127
|
+
function findReferenceInvocations(text, references) {
|
|
128
|
+
const matches = text.matchAll(/(?:^|[\s([{])(@([^/\s`,]+)(?:\/([^\s`,]*))?)/g);
|
|
129
|
+
const invocations = Array.from(matches).map((match) => resolveReferenceTokenInText(match[1] ?? "", references)).filter((invocation) => invocation !== undefined);
|
|
130
|
+
return [...new Map(invocations.map((invocation) => [invocation.token, invocation])).values()].sort((left, right) => left.token.localeCompare(right.token));
|
|
131
|
+
}
|
|
132
|
+
function renderReferenceInvocation(invocations) {
|
|
133
|
+
if (invocations.length === 0) return undefined;
|
|
134
|
+
const groups = Array.from(invocations.reduce((state, invocation) => {
|
|
135
|
+
const previous = state.get(invocation.name) ?? [];
|
|
136
|
+
return new Map([...state, [invocation.name, [...previous, invocation]]]);
|
|
137
|
+
}, new Map()).values()).sort((left, right) => (left[0]?.name ?? "").localeCompare(right[0]?.name ?? ""));
|
|
138
|
+
return groups.map((group) => {
|
|
139
|
+
const reference = group[0];
|
|
140
|
+
if (!reference) return "";
|
|
141
|
+
const description = reference.description ? escapeXmlText(reference.description) : "";
|
|
142
|
+
const files = group.filter((invocation) => invocation.relativePath !== undefined).sort((left, right) => left.relativePath?.localeCompare(right.relativePath ?? "") ?? 0);
|
|
143
|
+
return [
|
|
144
|
+
`<reference name="${escapeXmlAttribute(reference.name)}" token="${escapeXmlAttribute(`@${reference.name}`)}" path="${escapeXmlAttribute(reference.rootPath)}">`,
|
|
145
|
+
escapeXmlText(REFERENCE_INVOCATION_BLURB),
|
|
146
|
+
...description ? ["", description] : [],
|
|
147
|
+
...files.length > 0 ? [
|
|
148
|
+
"",
|
|
149
|
+
"<reference_files>",
|
|
150
|
+
...files.map((file) => ` <file token="${escapeXmlAttribute(file.token)}" relative_path="${escapeXmlAttribute(file.relativePath ?? "")}" path="${escapeXmlAttribute(file.path)}" />`),
|
|
151
|
+
"</reference_files>"
|
|
152
|
+
] : [],
|
|
153
|
+
"</reference>"
|
|
154
|
+
].join("\n");
|
|
155
|
+
}).join("\n\n");
|
|
156
|
+
}
|
|
157
|
+
function rewriteReferencePath(value, references) {
|
|
158
|
+
if (!value?.startsWith("@")) return value;
|
|
159
|
+
return resolveReferenceToken(value, references)?.path ?? value;
|
|
160
|
+
}
|
|
161
|
+
function createReferenceMessage(invocations) {
|
|
162
|
+
const content = renderReferenceInvocation(invocations);
|
|
163
|
+
if (!content) return undefined;
|
|
164
|
+
return {
|
|
165
|
+
customType: REFERENCE_MESSAGE_TYPE,
|
|
166
|
+
content,
|
|
167
|
+
display: true,
|
|
168
|
+
details: { references: invocations }
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function renderReferenceMessage(details, expanded, color) {
|
|
172
|
+
const references = details?.references ?? [];
|
|
173
|
+
const groups = Array.from(references.reduce((state, reference) => {
|
|
174
|
+
const previous = state.get(reference.name) ?? [];
|
|
175
|
+
return new Map([...state, [reference.name, [...previous, reference]]]);
|
|
176
|
+
}, new Map())).sort((left, right) => left[0].localeCompare(right[0]));
|
|
177
|
+
const labels = groups.length > 0 ? groups.map(([name, group]) => {
|
|
178
|
+
const pathCount = group.filter((reference) => reference.relativePath !== undefined).length;
|
|
179
|
+
return pathCount > 0 ? `@${name} (${pathCount} paths)` : `@${name}`;
|
|
180
|
+
}).join(" ") : "references";
|
|
181
|
+
const box = new Box(1, 1, (text) => color.bg("customMessageBg", text));
|
|
182
|
+
const label = color.fg("customMessageLabel", "\x1B[1m[ref]\x1B[22m");
|
|
183
|
+
if (!expanded) {
|
|
184
|
+
box.addChild(new Text(`${label} ${color.fg("customMessageText", labels)} ${color.fg("dim", "(ctrl+o to expand)")}`, 0, 0));
|
|
185
|
+
return box;
|
|
186
|
+
}
|
|
187
|
+
box.addChild(new Text(`${label} ${color.fg("customMessageText", labels)}`, 0, 0));
|
|
188
|
+
box.addChild(new Text(color.fg("customMessageText", references.map((reference) => {
|
|
189
|
+
const description = reference.description ? `\n${reference.description}` : "";
|
|
190
|
+
const relative = reference.relativePath ? `\nrelative: ${reference.relativePath}` : "";
|
|
191
|
+
return `${reference.token}${relative}\npath: ${reference.path}${description}`;
|
|
192
|
+
}).join("\n\n")), 0, 0));
|
|
193
|
+
return box;
|
|
194
|
+
}
|
|
195
|
+
function uniqueItems(items) {
|
|
196
|
+
return Array.from(items.reduce((state, item) => {
|
|
197
|
+
const key = `${item.value}\u0000${item.label}`;
|
|
198
|
+
if (state.has(key)) return state;
|
|
199
|
+
return new Map([...state, [key, item]]);
|
|
200
|
+
}, new Map()).values());
|
|
201
|
+
}
|
|
202
|
+
function mergeSuggestions(input) {
|
|
203
|
+
if (!input.references) return input.current;
|
|
204
|
+
if (!input.current) return input.references;
|
|
205
|
+
if (input.references.prefix !== input.current.prefix) return input.references;
|
|
206
|
+
return {
|
|
207
|
+
prefix: input.references.prefix,
|
|
208
|
+
items: [...uniqueItems([...input.references.items, ...input.current.items])]
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async function referenceSuggestions(input) {
|
|
212
|
+
const slashIndex = input.token.indexOf("/");
|
|
213
|
+
if (slashIndex === -1) {
|
|
214
|
+
const items = aliasItems(input.references, input.token);
|
|
215
|
+
if (items.length === 0) return null;
|
|
216
|
+
return {
|
|
217
|
+
items: [...items],
|
|
218
|
+
prefix: `@${input.token}`
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const alias = input.token.slice(0, slashIndex);
|
|
222
|
+
const tail = input.token.slice(slashIndex + 1);
|
|
223
|
+
const reference = visibleReferences(input.references).find((candidate) => candidate.name === alias);
|
|
224
|
+
if (!reference) return null;
|
|
225
|
+
const items = await childItems({
|
|
226
|
+
reference,
|
|
227
|
+
token: input.token,
|
|
228
|
+
tail
|
|
229
|
+
});
|
|
230
|
+
if (items.length === 0) return null;
|
|
231
|
+
return {
|
|
232
|
+
items: [...items],
|
|
233
|
+
prefix: `@${input.token}`
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function createReferencesAutocompleteProvider(current, getReferences) {
|
|
237
|
+
return {
|
|
238
|
+
triggerCharacters: ["@"],
|
|
239
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
240
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
241
|
+
const textBeforeCursor = currentLine.slice(0, cursorCol);
|
|
242
|
+
const token = extractReferenceToken(textBeforeCursor);
|
|
243
|
+
if (token === undefined) return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
244
|
+
const [references, currentSuggestions] = await Promise.all([referenceSuggestions({
|
|
245
|
+
references: getReferences(),
|
|
246
|
+
token
|
|
247
|
+
}), current.getSuggestions(lines, cursorLine, cursorCol, options)]);
|
|
248
|
+
if (options.signal.aborted) return null;
|
|
249
|
+
return mergeSuggestions({
|
|
250
|
+
references,
|
|
251
|
+
current: currentSuggestions
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
255
|
+
return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
256
|
+
},
|
|
257
|
+
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
258
|
+
return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function bridgeReferencesAutocomplete(input) {
|
|
263
|
+
const existing = BRIDGED_HOSTS.get(input.host);
|
|
264
|
+
if (existing) return existing;
|
|
265
|
+
const add = input.host.addAutocompleteProvider.bind(input.host);
|
|
266
|
+
input.host.addAutocompleteProvider = (factory) => {
|
|
267
|
+
add((current) => createReferencesAutocompleteProvider(factory(current), input.getReferences));
|
|
268
|
+
};
|
|
269
|
+
BRIDGED_HOSTS.set(input.host, add);
|
|
270
|
+
return add;
|
|
271
|
+
}
|
|
272
|
+
async function loadResolvedReferences(cwd) {
|
|
273
|
+
const config = await loadReferencesConfig(cwd);
|
|
274
|
+
if (Result.isError(config)) return Result.err(config.error);
|
|
275
|
+
return Result.ok({
|
|
276
|
+
loaded: config.value.loaded,
|
|
277
|
+
resolved: resolveConfiguredReferences({
|
|
278
|
+
cwd,
|
|
279
|
+
config: config.value.config
|
|
280
|
+
})
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function renderReferences(references) {
|
|
284
|
+
if (references.length === 0) return "No references configured.";
|
|
285
|
+
return references.slice().sort((left, right) => left.name.localeCompare(right.name)).map((reference) => {
|
|
286
|
+
const source = reference.source.type === "git" ? `${reference.source.repository}${reference.source.branch ? `#${reference.source.branch}` : ""}` : reference.source.path;
|
|
287
|
+
const flags = [reference.description ? "described" : "no description", reference.hidden ? "hidden" : "visible"];
|
|
288
|
+
return [
|
|
289
|
+
`@${reference.name}`,
|
|
290
|
+
` path: ${reference.path}`,
|
|
291
|
+
` source: ${source}`,
|
|
292
|
+
` ${flags.join(" · ")}`
|
|
293
|
+
].join("\n");
|
|
294
|
+
}).join("\n\n");
|
|
295
|
+
}
|
|
296
|
+
function startMaterialization(input) {
|
|
297
|
+
void materializeGitReferences({
|
|
298
|
+
pi: input.pi,
|
|
299
|
+
references: input.references,
|
|
300
|
+
...input.ctx.signal ? { signal: input.ctx.signal } : {}
|
|
301
|
+
}).then((result) => {
|
|
302
|
+
if (!input.ctx.hasUI) return;
|
|
303
|
+
if (result.diagnostics.length > 0) {
|
|
304
|
+
input.ctx.ui.notify(`references: ${result.diagnostics.length} git materialization issue(s)`, "error");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (result.results.length > 0) {
|
|
308
|
+
input.ctx.ui.setStatus(STATUS_KEY, `refs:${input.references.length}`);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function registerReferencesExtension(pi) {
|
|
313
|
+
const states = new Map();
|
|
314
|
+
pi.registerMessageRenderer(REFERENCE_MESSAGE_TYPE, (message, { expanded }, theme) => renderReferenceMessage(message.details, expanded, theme));
|
|
315
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
316
|
+
const key = ctx.sessionManager.getSessionFile() ?? ctx.cwd;
|
|
317
|
+
const getReferences = () => states.get(key)?.references ?? EMPTY_REFERENCES_STATE.references;
|
|
318
|
+
const addAutocomplete = ctx.hasUI ? bridgeReferencesAutocomplete({
|
|
319
|
+
host: ctx.ui,
|
|
320
|
+
getReferences
|
|
321
|
+
}) : undefined;
|
|
322
|
+
const loaded = await loadResolvedReferences(ctx.cwd);
|
|
323
|
+
if (Result.isError(loaded)) return;
|
|
324
|
+
const references = loaded.value.resolved.references;
|
|
325
|
+
states.set(key, loaded.value.resolved);
|
|
326
|
+
if (ctx.hasUI) {
|
|
327
|
+
ctx.ui.setStatus(STATUS_KEY, `refs:${references.length}`);
|
|
328
|
+
addAutocomplete?.((current) => createReferencesAutocompleteProvider(current, getReferences));
|
|
329
|
+
}
|
|
330
|
+
startMaterialization({
|
|
331
|
+
pi,
|
|
332
|
+
ctx,
|
|
333
|
+
references
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
337
|
+
const loaded = await loadResolvedReferences(ctx.cwd);
|
|
338
|
+
if (Result.isError(loaded)) return;
|
|
339
|
+
const references = loaded.value.resolved.references;
|
|
340
|
+
const guidance = renderReferenceGuidance(references);
|
|
341
|
+
const invocations = findReferenceInvocations(event.prompt, references);
|
|
342
|
+
const message = createReferenceMessage(invocations);
|
|
343
|
+
if (!guidance && !message) return;
|
|
344
|
+
startMaterialization({
|
|
345
|
+
pi,
|
|
346
|
+
ctx,
|
|
347
|
+
references
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
...message ? { message } : {},
|
|
351
|
+
...guidance ? { systemPrompt: `${event.systemPrompt}\n\n${guidance}` } : {}
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
355
|
+
const loaded = await loadResolvedReferences(ctx.cwd);
|
|
356
|
+
if (Result.isError(loaded)) return;
|
|
357
|
+
const references = loaded.value.resolved.references;
|
|
358
|
+
if (isToolCallEventType("read", event)) {
|
|
359
|
+
event.input.path = rewriteReferencePath(event.input.path, references) ?? event.input.path;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (isToolCallEventType("ls", event)) {
|
|
363
|
+
event.input.path = rewriteReferencePath(event.input.path, references);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (isToolCallEventType("grep", event)) {
|
|
367
|
+
event.input.path = rewriteReferencePath(event.input.path, references);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (isToolCallEventType("find", event)) {
|
|
371
|
+
event.input.path = rewriteReferencePath(event.input.path, references);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (isToolCallEventType("edit", event) || isToolCallEventType("write", event)) {
|
|
375
|
+
const invocation = resolveReferenceToken(event.input.path, references);
|
|
376
|
+
if (invocation) {
|
|
377
|
+
return {
|
|
378
|
+
block: true,
|
|
379
|
+
reason: "Reference aliases are read-only. Use an absolute path if mutation is intentional."
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
pi.registerCommand("ohm-references", {
|
|
385
|
+
description: "Show resolved project references",
|
|
386
|
+
handler: async (_args, ctx) => {
|
|
387
|
+
const loaded = await loadResolvedReferences(ctx.cwd);
|
|
388
|
+
if (Result.isError(loaded)) {
|
|
389
|
+
console.log(loaded.error.message);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const text = [
|
|
393
|
+
"Pi OHM references",
|
|
394
|
+
"",
|
|
395
|
+
renderReferences(loaded.value.resolved.references),
|
|
396
|
+
"",
|
|
397
|
+
`loadedFrom: ${loaded.value.loaded.loadedFrom.length > 0 ? loaded.value.loaded.loadedFrom.join(", ") : "defaults"}`,
|
|
398
|
+
...loaded.value.resolved.diagnostics.length > 0 ? [
|
|
399
|
+
"",
|
|
400
|
+
"Diagnostics:",
|
|
401
|
+
...loaded.value.resolved.diagnostics.map((diagnostic) => `- ${diagnostic.name}: ${diagnostic.message}`)
|
|
402
|
+
] : []
|
|
403
|
+
].join("\n");
|
|
404
|
+
if (!ctx.hasUI) {
|
|
405
|
+
console.log(text);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
await ctx.ui.editor("pi-ohm references", text);
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
//#endregion
|
|
414
|
+
export { GitReferenceConfigSchema, LocalReferenceConfigSchema, ReferenceEntryConfigSchema, ReferencesConfigSchema, ReferencesError, bridgeReferencesAutocomplete, createReferencesAutocompleteProvider, registerReferencesExtension as default, defaultReferencesCacheRoot, ensureRepository, findReferenceInvocations, isJsonMap, isReferencesRuntimeConfig, loadReferencesConfig, materializeGitReferences, mergeReferencesConfig, parseRemoteRepositoryReference, parseRepositoryReference, referencesConfigModule, renderReferenceGuidance, renderReferenceInvocation, repositoryCacheIdentity, repositoryCachePath, resolveConfiguredReferences, resolveReferenceToken, rewriteReferencePath, sameRepositoryReference, validReferenceAlias, validateBranch };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ReferencesError } from "./repository.js";
|
|
2
|
+
import { RepositoryCacheResult } from "./cache.js";
|
|
3
|
+
import { ReferencesRuntimeConfig } from "./config.js";
|
|
4
|
+
import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
//#region src/references.d.ts
|
|
7
|
+
interface LocalReferenceSource {
|
|
8
|
+
readonly type: "local";
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly description?: string;
|
|
11
|
+
readonly hidden?: boolean;
|
|
12
|
+
}
|
|
13
|
+
interface GitReferenceSource {
|
|
14
|
+
readonly type: "git";
|
|
15
|
+
readonly repository: string;
|
|
16
|
+
readonly branch?: string;
|
|
17
|
+
readonly description?: string;
|
|
18
|
+
readonly hidden?: boolean;
|
|
19
|
+
}
|
|
20
|
+
type ReferenceSource = LocalReferenceSource | GitReferenceSource;
|
|
21
|
+
interface ReferenceInfo {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly description?: string;
|
|
25
|
+
readonly hidden?: boolean;
|
|
26
|
+
readonly source: ReferenceSource;
|
|
27
|
+
}
|
|
28
|
+
interface ReferenceDiagnostic {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly cause: ReferencesError;
|
|
32
|
+
}
|
|
33
|
+
interface ResolvedReferences {
|
|
34
|
+
readonly references: readonly ReferenceInfo[];
|
|
35
|
+
readonly diagnostics: readonly ReferenceDiagnostic[];
|
|
36
|
+
}
|
|
37
|
+
interface MaterializeReferencesResult {
|
|
38
|
+
readonly results: readonly RepositoryCacheResult[];
|
|
39
|
+
readonly diagnostics: readonly ReferenceDiagnostic[];
|
|
40
|
+
}
|
|
41
|
+
declare function resolveConfiguredReferences(input: {
|
|
42
|
+
readonly cwd: string;
|
|
43
|
+
readonly config: ReferencesRuntimeConfig;
|
|
44
|
+
readonly cacheRoot?: string;
|
|
45
|
+
}): ResolvedReferences;
|
|
46
|
+
declare function renderReferenceGuidance(references: readonly ReferenceInfo[]): string | undefined;
|
|
47
|
+
declare function materializeGitReferences(input: {
|
|
48
|
+
readonly pi: Pick<ExtensionAPI, "exec">;
|
|
49
|
+
readonly references: readonly ReferenceInfo[];
|
|
50
|
+
readonly cacheRoot?: string;
|
|
51
|
+
readonly signal?: AbortSignal;
|
|
52
|
+
}): Promise<MaterializeReferencesResult>;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { GitReferenceSource, LocalReferenceSource, MaterializeReferencesResult, ReferenceDiagnostic, ReferenceInfo, ReferenceSource, ResolvedReferences, materializeGitReferences, renderReferenceGuidance, resolveConfiguredReferences };
|