@gmickel/gno 1.25.1 → 1.26.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/README.md +10 -4
- package/assets/skill/SKILL.md +29 -17
- package/browser-extension/artifacts/{gno-browser-clipper-v1.25.1.zip → gno-browser-clipper-v1.26.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.26.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +146 -4
- package/spec/db/schema.sql +1 -1
- package/spec/output-schemas/project-profile-apply.schema.json +209 -0
- package/spec/output-schemas/project-profile-command.schema.json +160 -0
- package/spec/output-schemas/query-diagnose.schema.json +7 -1
- package/spec/output-schemas/setup-profile-result.schema.json +87 -0
- package/spec/project-profile.schema.json +301 -0
- package/src/cli/commands/collection/add.ts +39 -45
- package/src/cli/commands/collection/remove.ts +28 -28
- package/src/cli/commands/collection/rename.ts +55 -73
- package/src/cli/commands/context/add.ts +37 -26
- package/src/cli/commands/context/rm.ts +47 -20
- package/src/cli/commands/init.ts +55 -125
- package/src/cli/commands/models/use.ts +43 -38
- package/src/cli/commands/profile-apply.ts +334 -0
- package/src/cli/commands/profile.ts +409 -0
- package/src/cli/commands/setup-activation.ts +205 -54
- package/src/cli/commands/setup-profile.ts +223 -0
- package/src/cli/commands/setup.ts +3 -0
- package/src/cli/program.ts +110 -9
- package/src/config/index.ts +3 -0
- package/src/config/project-profile.ts +367 -0
- package/src/config/saver.ts +16 -7
- package/src/config/types.ts +42 -0
- package/src/core/config-mutation.ts +138 -76
- package/src/core/config-write-lock.ts +89 -0
- package/src/core/context-identity.ts +16 -0
- package/src/core/context-resolver.ts +2 -12
- package/src/core/folder-setup-planning.ts +6 -21
- package/src/core/folder-setup.ts +30 -2
- package/src/core/path-rules.ts +53 -0
- package/src/core/project-affinity-surface.ts +102 -7
- package/src/core/project-profile-apply-state.ts +268 -0
- package/src/core/project-profile-apply-validation.ts +95 -0
- package/src/core/project-profile-apply.ts +408 -0
- package/src/core/project-profile-canonical.ts +71 -0
- package/src/core/project-profile-diff.ts +302 -0
- package/src/core/project-profile-discovery.ts +519 -0
- package/src/core/project-profile-file.ts +37 -0
- package/src/core/project-profile-parser.ts +98 -0
- package/src/core/project-profile.ts +490 -0
- package/src/ingestion/walker.ts +85 -44
- package/src/llm/cache.ts +21 -0
- package/src/serve/config-sync.ts +2 -2
- package/src/serve/resident-runtime.ts +1 -0
- package/src/serve/routes/api.ts +1 -0
- package/src/store/migrations/021-multi-context-identity.ts +37 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +83 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.25.1.zip.sha256 +0 -1
package/src/cli/program.ts
CHANGED
|
@@ -1291,6 +1291,77 @@ function wireSearchCommands(program: Command): void {
|
|
|
1291
1291
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1292
1292
|
|
|
1293
1293
|
function wireOnboardingCommands(program: Command): void {
|
|
1294
|
+
const profileCmd = program
|
|
1295
|
+
.command("profile")
|
|
1296
|
+
.description("Inspect a project-local retrieval profile");
|
|
1297
|
+
|
|
1298
|
+
const wireProfileRead = (
|
|
1299
|
+
command: "check" | "show" | "diff",
|
|
1300
|
+
description: string
|
|
1301
|
+
): void => {
|
|
1302
|
+
profileCmd
|
|
1303
|
+
.command(`${command} [path]`)
|
|
1304
|
+
.description(description)
|
|
1305
|
+
.option("--json", "JSON output")
|
|
1306
|
+
.action(
|
|
1307
|
+
async (path: string | undefined, cmdOpts: Record<string, unknown>) => {
|
|
1308
|
+
const globals = getGlobals();
|
|
1309
|
+
const json = Boolean(cmdOpts.json) || globals.json;
|
|
1310
|
+
const { formatProjectProfileResult, runProjectProfileCommand } =
|
|
1311
|
+
await import("./commands/profile");
|
|
1312
|
+
const outcome = await runProjectProfileCommand({
|
|
1313
|
+
command,
|
|
1314
|
+
path,
|
|
1315
|
+
configPath: globals.config,
|
|
1316
|
+
offline: globals.offline,
|
|
1317
|
+
});
|
|
1318
|
+
process.stdout.write(
|
|
1319
|
+
`${formatProjectProfileResult(outcome.result, { json })}\n`
|
|
1320
|
+
);
|
|
1321
|
+
if (outcome.exitCode !== 0) {
|
|
1322
|
+
throw new CliError(
|
|
1323
|
+
outcome.exitCode === 1 ? "VALIDATION" : "RUNTIME",
|
|
1324
|
+
"Project profile inspection failed",
|
|
1325
|
+
{ silent: true }
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
);
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
wireProfileRead("check", "Validate a project-local retrieval profile");
|
|
1333
|
+
wireProfileRead("show", "Show normalized project-local retrieval state");
|
|
1334
|
+
wireProfileRead("diff", "Compare a project profile with local config");
|
|
1335
|
+
profileCmd
|
|
1336
|
+
.command("apply [path]")
|
|
1337
|
+
.description("Apply a project profile without implicit deletion")
|
|
1338
|
+
.option("--json", "JSON output")
|
|
1339
|
+
.action(
|
|
1340
|
+
async (path: string | undefined, cmdOpts: Record<string, unknown>) => {
|
|
1341
|
+
const globals = getGlobals();
|
|
1342
|
+
const json = Boolean(cmdOpts.json) || globals.json;
|
|
1343
|
+
const {
|
|
1344
|
+
formatProjectProfileApplyResult,
|
|
1345
|
+
runProjectProfileApplyCommand,
|
|
1346
|
+
} = await import("./commands/profile-apply");
|
|
1347
|
+
const outcome = await runProjectProfileApplyCommand({
|
|
1348
|
+
path,
|
|
1349
|
+
configPath: globals.config,
|
|
1350
|
+
indexName: globals.index,
|
|
1351
|
+
});
|
|
1352
|
+
process.stdout.write(
|
|
1353
|
+
`${formatProjectProfileApplyResult(outcome.result, { json })}\n`
|
|
1354
|
+
);
|
|
1355
|
+
if (outcome.exitCode !== 0) {
|
|
1356
|
+
throw new CliError(
|
|
1357
|
+
outcome.exitCode === 1 ? "VALIDATION" : "RUNTIME",
|
|
1358
|
+
"Project profile apply failed",
|
|
1359
|
+
{ silent: true }
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
);
|
|
1364
|
+
|
|
1294
1365
|
// init - Initialize GNO
|
|
1295
1366
|
program
|
|
1296
1367
|
.command("init [path]")
|
|
@@ -1320,6 +1391,7 @@ function wireOnboardingCommands(program: Command): void {
|
|
|
1320
1391
|
| undefined,
|
|
1321
1392
|
language: cmdOpts.language as string | undefined,
|
|
1322
1393
|
yes: globals.yes,
|
|
1394
|
+
configPath: globals.config,
|
|
1323
1395
|
});
|
|
1324
1396
|
|
|
1325
1397
|
if (!result.success) {
|
|
@@ -1367,13 +1439,20 @@ function wireOnboardingCommands(program: Command): void {
|
|
|
1367
1439
|
collectRepeatableValue,
|
|
1368
1440
|
[]
|
|
1369
1441
|
)
|
|
1442
|
+
.option(
|
|
1443
|
+
"--apply-profile",
|
|
1444
|
+
"apply a valid project profile before lexical setup"
|
|
1445
|
+
)
|
|
1370
1446
|
.option("--no-semantic", "skip background semantic indexing")
|
|
1371
1447
|
.option("--json", "JSON output")
|
|
1372
1448
|
.action(async (folder: string, cmdOpts: Record<string, unknown>) => {
|
|
1373
1449
|
const globals = getGlobals();
|
|
1374
1450
|
const json = Boolean(cmdOpts.json) || globals.json;
|
|
1375
|
-
const {
|
|
1376
|
-
|
|
1451
|
+
const {
|
|
1452
|
+
formatSetupOutputResult,
|
|
1453
|
+
formatSetupProfileAdvisory,
|
|
1454
|
+
setupWithActivation,
|
|
1455
|
+
} = await import("./commands/setup-activation");
|
|
1377
1456
|
const exclusions = cmdOpts.exclude as string[];
|
|
1378
1457
|
const outcome = await setupWithActivation({
|
|
1379
1458
|
folder,
|
|
@@ -1381,6 +1460,7 @@ function wireOnboardingCommands(program: Command): void {
|
|
|
1381
1460
|
exclude: exclusions.length > 0 ? exclusions : undefined,
|
|
1382
1461
|
authorizeSecretRisk: Boolean(cmdOpts.authorizeSecretRisk),
|
|
1383
1462
|
connectorIds: cmdOpts.connector as string[],
|
|
1463
|
+
applyProfile: Boolean(cmdOpts.applyProfile),
|
|
1384
1464
|
semantic: cmdOpts.semantic !== false,
|
|
1385
1465
|
indexName: globals.index,
|
|
1386
1466
|
configPath: globals.config,
|
|
@@ -1391,6 +1471,17 @@ function wireOnboardingCommands(program: Command): void {
|
|
|
1391
1471
|
progress: (stage) => {
|
|
1392
1472
|
process.stderr.write(`setup: ${stage}\n`);
|
|
1393
1473
|
},
|
|
1474
|
+
onProfileAdvisory: (profile) => {
|
|
1475
|
+
if (json || globals.quiet) return;
|
|
1476
|
+
const advisory = formatSetupProfileAdvisory(profile, {
|
|
1477
|
+
applyRequested: Boolean(cmdOpts.applyProfile),
|
|
1478
|
+
});
|
|
1479
|
+
if (advisory) process.stderr.write(`${advisory}\n`);
|
|
1480
|
+
},
|
|
1481
|
+
onProfileApply: (profile) => {
|
|
1482
|
+
if (json || globals.quiet) return;
|
|
1483
|
+
process.stderr.write(`setup: project profile ${profile.status}\n`);
|
|
1484
|
+
},
|
|
1394
1485
|
});
|
|
1395
1486
|
const output = formatSetupOutputResult(outcome.result, { json });
|
|
1396
1487
|
if (json || outcome.exitCode === 0) {
|
|
@@ -1958,6 +2049,7 @@ function wireManagementCommands(program: Command): void {
|
|
|
1958
2049
|
include: cmdOpts.include as string | undefined,
|
|
1959
2050
|
exclude: cmdOpts.exclude as string | undefined,
|
|
1960
2051
|
update: cmdOpts.update as string | undefined,
|
|
2052
|
+
configPath: getGlobals().config,
|
|
1961
2053
|
});
|
|
1962
2054
|
});
|
|
1963
2055
|
|
|
@@ -1982,7 +2074,7 @@ function wireManagementCommands(program: Command): void {
|
|
|
1982
2074
|
.description("Remove a collection")
|
|
1983
2075
|
.action(async (name: string) => {
|
|
1984
2076
|
const { collectionRemove } = await import("./commands/collection");
|
|
1985
|
-
await collectionRemove(name);
|
|
2077
|
+
await collectionRemove(name, { configPath: getGlobals().config });
|
|
1986
2078
|
});
|
|
1987
2079
|
|
|
1988
2080
|
collectionCmd
|
|
@@ -1990,7 +2082,9 @@ function wireManagementCommands(program: Command): void {
|
|
|
1990
2082
|
.description("Rename a collection")
|
|
1991
2083
|
.action(async (oldName: string, newName: string) => {
|
|
1992
2084
|
const { collectionRename } = await import("./commands/collection");
|
|
1993
|
-
await collectionRename(oldName, newName
|
|
2085
|
+
await collectionRename(oldName, newName, {
|
|
2086
|
+
configPath: getGlobals().config,
|
|
2087
|
+
});
|
|
1994
2088
|
});
|
|
1995
2089
|
|
|
1996
2090
|
collectionCmd
|
|
@@ -2017,9 +2111,11 @@ function wireManagementCommands(program: Command): void {
|
|
|
2017
2111
|
.description("Add context metadata for a scope")
|
|
2018
2112
|
.action(async (scope: string, text: string) => {
|
|
2019
2113
|
const { contextAdd } = await import("./commands/context");
|
|
2020
|
-
const exitCode = await contextAdd(scope, text
|
|
2114
|
+
const exitCode = await contextAdd(scope, text, {
|
|
2115
|
+
configPath: getGlobals().config,
|
|
2116
|
+
});
|
|
2021
2117
|
if (exitCode !== 0) {
|
|
2022
|
-
throw new CliError("
|
|
2118
|
+
throw new CliError("VALIDATION", "Failed to add context");
|
|
2023
2119
|
}
|
|
2024
2120
|
});
|
|
2025
2121
|
|
|
@@ -2301,11 +2397,16 @@ function wireManagementCommands(program: Command): void {
|
|
|
2301
2397
|
});
|
|
2302
2398
|
|
|
2303
2399
|
contextCmd
|
|
2304
|
-
.command("rm <
|
|
2400
|
+
.command("rm <scope> [text]")
|
|
2305
2401
|
.description("Remove context item")
|
|
2306
|
-
.action(async (
|
|
2402
|
+
.action(async (scope: string, text?: string) => {
|
|
2307
2403
|
const { contextRm } = await import("./commands/context");
|
|
2308
|
-
await contextRm(
|
|
2404
|
+
const exitCode = await contextRm(scope, text, {
|
|
2405
|
+
configPath: getGlobals().config,
|
|
2406
|
+
});
|
|
2407
|
+
if (exitCode !== 0) {
|
|
2408
|
+
throw new CliError("VALIDATION", "Failed to remove context");
|
|
2409
|
+
}
|
|
2309
2410
|
});
|
|
2310
2411
|
|
|
2311
2412
|
// models subcommands
|
package/src/config/index.ts
CHANGED
|
@@ -42,6 +42,7 @@ export {
|
|
|
42
42
|
type SaveResult,
|
|
43
43
|
saveConfig,
|
|
44
44
|
saveConfigToPath,
|
|
45
|
+
saveTextToPath,
|
|
45
46
|
} from "./saver";
|
|
46
47
|
// Types and schemas
|
|
47
48
|
export {
|
|
@@ -66,6 +67,8 @@ export {
|
|
|
66
67
|
getCollectionFromScope,
|
|
67
68
|
isValidLanguageHint,
|
|
68
69
|
parseScope,
|
|
70
|
+
type ProjectProfileBinding,
|
|
71
|
+
ProjectProfileBindingSchema,
|
|
69
72
|
type ScopeType,
|
|
70
73
|
ScopeTypeSchema,
|
|
71
74
|
} from "./types";
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import { NOTE_PRESETS } from "../core/note-presets";
|
|
4
|
+
import { hasLikelySecretPath } from "../core/path-rules";
|
|
5
|
+
import {
|
|
6
|
+
isValidLanguageHint,
|
|
7
|
+
PROJECT_AFFINITY_MAX_CONTRIBUTION,
|
|
8
|
+
} from "./types";
|
|
9
|
+
|
|
10
|
+
export const PROJECT_PROFILE_SCHEMA_VERSION = "1.0" as const;
|
|
11
|
+
export const PROJECT_PROFILE_FINGERPRINT_DOMAIN = "gno-project-profile\0v1.0\0";
|
|
12
|
+
export const PROJECT_PROFILE_FORCED_EXCLUDES = [".gno"] as const;
|
|
13
|
+
|
|
14
|
+
const COLLECTION_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
15
|
+
const REFERENCE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
16
|
+
const CAPABILITY_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
|
|
17
|
+
const WINDOWS_DRIVE_PATH_PATTERN = /^[a-z]:/i;
|
|
18
|
+
const WINDOWS_UNC_PATH_PATTERN = /^(?:\\\\|\/\/)/;
|
|
19
|
+
const ENV_EXPANSION_PATTERN =
|
|
20
|
+
/(?:\$\{[^}]*\}|\$[A-Za-z_][A-Za-z0-9_]*|%[A-Za-z_][A-Za-z0-9_]*%)/;
|
|
21
|
+
const GLOB_META_PATTERN = /[*?[\]{}]/;
|
|
22
|
+
const WINDOWS_INVALID_COMPONENT_PATTERN = /[<>:"|]/;
|
|
23
|
+
const WINDOWS_RESERVED_COMPONENT_PATTERN =
|
|
24
|
+
/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
|
|
25
|
+
const RUNTIME_PATH_PATTERN =
|
|
26
|
+
/(?:^|\/)\.gno(?:\/|$)|\.(?:db|sqlite|sqlite3|gguf|lock)$/i;
|
|
27
|
+
const NOTE_PRESET_IDS = new Set<string>(
|
|
28
|
+
NOTE_PRESETS.map((preset) => preset.id)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const globStructureIssue = (value: string): string | null => {
|
|
32
|
+
if (value.startsWith("!")) {
|
|
33
|
+
return "must not use whole-pattern negation";
|
|
34
|
+
}
|
|
35
|
+
let braceDepth = 0;
|
|
36
|
+
let bracketDepth = 0;
|
|
37
|
+
for (const character of value) {
|
|
38
|
+
if (character === "{") braceDepth += 1;
|
|
39
|
+
else if (character === "}") {
|
|
40
|
+
braceDepth -= 1;
|
|
41
|
+
if (braceDepth < 0) return "contains an unmatched closing brace";
|
|
42
|
+
} else if (character === "[") bracketDepth += 1;
|
|
43
|
+
else if (character === "]") {
|
|
44
|
+
bracketDepth -= 1;
|
|
45
|
+
if (bracketDepth < 0) return "contains an unmatched closing bracket";
|
|
46
|
+
}
|
|
47
|
+
if (braceDepth > 16 || bracketDepth > 16) {
|
|
48
|
+
return "contains excessively nested glob groups";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (braceDepth !== 0) return "contains an unmatched opening brace";
|
|
52
|
+
if (bracketDepth !== 0) return "contains an unmatched opening bracket";
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const windowsComponentIssue = (
|
|
57
|
+
normalized: string,
|
|
58
|
+
allowGlob: boolean
|
|
59
|
+
): string | null => {
|
|
60
|
+
const components: string[] = [];
|
|
61
|
+
let component = "";
|
|
62
|
+
let braceDepth = 0;
|
|
63
|
+
const pushComponent = (): void => {
|
|
64
|
+
if (component !== "") components.push(component);
|
|
65
|
+
component = "";
|
|
66
|
+
};
|
|
67
|
+
for (const character of normalized) {
|
|
68
|
+
const isBraceSeparator =
|
|
69
|
+
character === "{" ||
|
|
70
|
+
character === "}" ||
|
|
71
|
+
(character === "," && braceDepth > 0);
|
|
72
|
+
if (character === "/" || isBraceSeparator) {
|
|
73
|
+
pushComponent();
|
|
74
|
+
if (character === "{") braceDepth += 1;
|
|
75
|
+
else if (character === "}") braceDepth -= 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
component += character;
|
|
79
|
+
}
|
|
80
|
+
pushComponent();
|
|
81
|
+
|
|
82
|
+
for (const component of components) {
|
|
83
|
+
if (component === "." || component === "") continue;
|
|
84
|
+
let hasControlCharacter = false;
|
|
85
|
+
for (const character of component) {
|
|
86
|
+
if ((character.codePointAt(0) ?? 0) < 32) {
|
|
87
|
+
hasControlCharacter = true;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (
|
|
92
|
+
hasControlCharacter ||
|
|
93
|
+
WINDOWS_INVALID_COMPONENT_PATTERN.test(component)
|
|
94
|
+
) {
|
|
95
|
+
return "contains characters invalid in Windows path components";
|
|
96
|
+
}
|
|
97
|
+
if (component.endsWith(".") || component.endsWith(" ")) {
|
|
98
|
+
return "contains a component ending in a dot or space";
|
|
99
|
+
}
|
|
100
|
+
if (
|
|
101
|
+
(!allowGlob || !GLOB_META_PATTERN.test(component)) &&
|
|
102
|
+
WINDOWS_RESERVED_COMPONENT_PATTERN.test(component)
|
|
103
|
+
) {
|
|
104
|
+
return "contains a Windows-reserved path component";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const portablePathIssue = (
|
|
111
|
+
value: string,
|
|
112
|
+
options: { allowGlob: boolean; allowRuntimeExclusion: boolean }
|
|
113
|
+
): string | null => {
|
|
114
|
+
if (value.includes("\0")) return "contains a NUL byte";
|
|
115
|
+
if (
|
|
116
|
+
value.startsWith("/") ||
|
|
117
|
+
value.startsWith("\\") ||
|
|
118
|
+
WINDOWS_UNC_PATH_PATTERN.test(value) ||
|
|
119
|
+
WINDOWS_DRIVE_PATH_PATTERN.test(value) ||
|
|
120
|
+
value.startsWith("~")
|
|
121
|
+
) {
|
|
122
|
+
return "must be repository-relative on every supported platform";
|
|
123
|
+
}
|
|
124
|
+
if (ENV_EXPANSION_PATTERN.test(value)) {
|
|
125
|
+
return "must not contain environment expansion";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const normalized = value.replaceAll("\\", "/");
|
|
129
|
+
if (normalized.split("/").includes("..")) {
|
|
130
|
+
return "must not contain traversal segments";
|
|
131
|
+
}
|
|
132
|
+
const windowsIssue = windowsComponentIssue(normalized, options.allowGlob);
|
|
133
|
+
if (windowsIssue) return windowsIssue;
|
|
134
|
+
if (!options.allowGlob && GLOB_META_PATTERN.test(normalized)) {
|
|
135
|
+
return "must not contain glob metacharacters";
|
|
136
|
+
}
|
|
137
|
+
if (options.allowGlob) {
|
|
138
|
+
const globIssue = globStructureIssue(normalized);
|
|
139
|
+
if (globIssue) return globIssue;
|
|
140
|
+
if (normalized.includes("{") || normalized.includes("}")) {
|
|
141
|
+
return "must not use brace alternatives; use separate include or exclude entries";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!options.allowRuntimeExclusion && RUNTIME_PATH_PATTERN.test(normalized)) {
|
|
145
|
+
return "must not reference GNO runtime state";
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const portablePathSchema = (
|
|
151
|
+
options: { allowGlob?: boolean; allowRuntimeExclusion?: boolean } = {}
|
|
152
|
+
) =>
|
|
153
|
+
z
|
|
154
|
+
.string()
|
|
155
|
+
.min(1)
|
|
156
|
+
.max(512)
|
|
157
|
+
.superRefine((value, context) => {
|
|
158
|
+
const issue = portablePathIssue(value, {
|
|
159
|
+
allowGlob: options.allowGlob ?? false,
|
|
160
|
+
allowRuntimeExclusion: options.allowRuntimeExclusion ?? false,
|
|
161
|
+
});
|
|
162
|
+
if (issue) {
|
|
163
|
+
context.addIssue({
|
|
164
|
+
code: "custom",
|
|
165
|
+
message: `UNSAFE_PATH: ${issue}`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
export const ProjectProfileCollectionSchema = z
|
|
171
|
+
.object({
|
|
172
|
+
name: z.string().regex(COLLECTION_NAME_PATTERN),
|
|
173
|
+
root: portablePathSchema().default("."),
|
|
174
|
+
include: z
|
|
175
|
+
.array(portablePathSchema({ allowGlob: true }))
|
|
176
|
+
.min(1)
|
|
177
|
+
.max(128)
|
|
178
|
+
.superRefine((values, context) => {
|
|
179
|
+
const seen = new Set<string>();
|
|
180
|
+
for (const [index, value] of values.entries()) {
|
|
181
|
+
if (seen.has(value)) {
|
|
182
|
+
context.addIssue({
|
|
183
|
+
code: "custom",
|
|
184
|
+
path: [index],
|
|
185
|
+
message: "Duplicate include rule",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
seen.add(value);
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
.default(["**/*"]),
|
|
192
|
+
exclude: z
|
|
193
|
+
.array(
|
|
194
|
+
portablePathSchema({
|
|
195
|
+
allowGlob: true,
|
|
196
|
+
allowRuntimeExclusion: true,
|
|
197
|
+
})
|
|
198
|
+
)
|
|
199
|
+
.max(128)
|
|
200
|
+
.superRefine((values, context) => {
|
|
201
|
+
const seen = new Set<string>();
|
|
202
|
+
for (const [index, value] of values.entries()) {
|
|
203
|
+
if (seen.has(value)) {
|
|
204
|
+
context.addIssue({
|
|
205
|
+
code: "custom",
|
|
206
|
+
path: [index],
|
|
207
|
+
message: "Duplicate exclude rule",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
seen.add(value);
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
.default([]),
|
|
214
|
+
languageHint: z
|
|
215
|
+
.string()
|
|
216
|
+
.refine(isValidLanguageHint, "Invalid BCP-47 language hint")
|
|
217
|
+
.optional(),
|
|
218
|
+
modelPreset: z.string().regex(REFERENCE_PATTERN).optional(),
|
|
219
|
+
})
|
|
220
|
+
.strict();
|
|
221
|
+
|
|
222
|
+
const InlineProjectProfileContextSchema = z
|
|
223
|
+
.object({
|
|
224
|
+
text: z.string().min(1).max(65_536),
|
|
225
|
+
})
|
|
226
|
+
.strict();
|
|
227
|
+
|
|
228
|
+
const FileProjectProfileContextSchema = z
|
|
229
|
+
.object({
|
|
230
|
+
file: portablePathSchema().superRefine((value, context) => {
|
|
231
|
+
if (hasLikelySecretPath(value)) {
|
|
232
|
+
context.addIssue({
|
|
233
|
+
code: "custom",
|
|
234
|
+
message:
|
|
235
|
+
"UNSAFE_PATH: likely credential and secret files cannot be profile contexts",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}),
|
|
239
|
+
})
|
|
240
|
+
.strict();
|
|
241
|
+
|
|
242
|
+
export const ProjectProfileContextSchema = z.union([
|
|
243
|
+
InlineProjectProfileContextSchema,
|
|
244
|
+
FileProjectProfileContextSchema,
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
export const ProjectProfileContentTypeSchema = z
|
|
248
|
+
.object({
|
|
249
|
+
prefixes: z
|
|
250
|
+
.array(portablePathSchema())
|
|
251
|
+
.min(1)
|
|
252
|
+
.max(64)
|
|
253
|
+
.superRefine((values, context) => {
|
|
254
|
+
const seen = new Set<string>();
|
|
255
|
+
for (const [index, value] of values.entries()) {
|
|
256
|
+
if (seen.has(value)) {
|
|
257
|
+
context.addIssue({
|
|
258
|
+
code: "custom",
|
|
259
|
+
path: [index],
|
|
260
|
+
message: "Duplicate content type prefix",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
seen.add(value);
|
|
264
|
+
}
|
|
265
|
+
}),
|
|
266
|
+
preset: z
|
|
267
|
+
.string()
|
|
268
|
+
.min(1)
|
|
269
|
+
.refine((value) => NOTE_PRESET_IDS.has(value), "Unknown note preset"),
|
|
270
|
+
graphHints: z
|
|
271
|
+
.array(z.string().min(1).max(64))
|
|
272
|
+
.max(16)
|
|
273
|
+
.superRefine((values, context) => {
|
|
274
|
+
const seen = new Set<string>();
|
|
275
|
+
for (const [index, value] of values.entries()) {
|
|
276
|
+
if (seen.has(value)) {
|
|
277
|
+
context.addIssue({
|
|
278
|
+
code: "custom",
|
|
279
|
+
path: [index],
|
|
280
|
+
message: "Duplicate graph hint",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
seen.add(value);
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
.optional(),
|
|
287
|
+
searchBoost: z.number().finite().optional(),
|
|
288
|
+
temporal: z.boolean().optional(),
|
|
289
|
+
})
|
|
290
|
+
.strict();
|
|
291
|
+
|
|
292
|
+
export const ProjectProfileAffinityDefaultsSchema = z
|
|
293
|
+
.object({
|
|
294
|
+
enabled: z.boolean().default(true),
|
|
295
|
+
contribution: z
|
|
296
|
+
.number()
|
|
297
|
+
.finite()
|
|
298
|
+
.min(0)
|
|
299
|
+
.max(PROJECT_AFFINITY_MAX_CONTRIBUTION)
|
|
300
|
+
.default(PROJECT_AFFINITY_MAX_CONTRIBUTION),
|
|
301
|
+
})
|
|
302
|
+
.strict();
|
|
303
|
+
|
|
304
|
+
export const ProjectProfileSchema = z
|
|
305
|
+
.object({
|
|
306
|
+
schemaVersion: z.literal(PROJECT_PROFILE_SCHEMA_VERSION),
|
|
307
|
+
collection: ProjectProfileCollectionSchema,
|
|
308
|
+
contexts: z
|
|
309
|
+
.array(ProjectProfileContextSchema)
|
|
310
|
+
.max(64)
|
|
311
|
+
.superRefine((values, context) => {
|
|
312
|
+
const seen = new Set<string>();
|
|
313
|
+
for (const [index, value] of values.entries()) {
|
|
314
|
+
const key = JSON.stringify(value);
|
|
315
|
+
if (seen.has(key)) {
|
|
316
|
+
context.addIssue({
|
|
317
|
+
code: "custom",
|
|
318
|
+
path: [index],
|
|
319
|
+
message: "Duplicate context declaration",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
seen.add(key);
|
|
323
|
+
}
|
|
324
|
+
})
|
|
325
|
+
.default([]),
|
|
326
|
+
contentTypes: z
|
|
327
|
+
.record(
|
|
328
|
+
z.string().regex(REFERENCE_PATTERN),
|
|
329
|
+
ProjectProfileContentTypeSchema
|
|
330
|
+
)
|
|
331
|
+
.superRefine((rules, context) => {
|
|
332
|
+
if (Object.keys(rules).length > 64) {
|
|
333
|
+
context.addIssue({
|
|
334
|
+
code: "custom",
|
|
335
|
+
message: "Too many content type rules; maximum is 64",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
})
|
|
339
|
+
.default({}),
|
|
340
|
+
affinityDefaults: ProjectProfileAffinityDefaultsSchema.default({
|
|
341
|
+
enabled: true,
|
|
342
|
+
contribution: PROJECT_AFFINITY_MAX_CONTRIBUTION,
|
|
343
|
+
}),
|
|
344
|
+
recommendedCapabilities: z
|
|
345
|
+
.array(z.string().regex(CAPABILITY_PATTERN))
|
|
346
|
+
.max(32)
|
|
347
|
+
.superRefine((values, context) => {
|
|
348
|
+
const seen = new Set<string>();
|
|
349
|
+
for (const [index, value] of values.entries()) {
|
|
350
|
+
if (seen.has(value)) {
|
|
351
|
+
context.addIssue({
|
|
352
|
+
code: "custom",
|
|
353
|
+
path: [index],
|
|
354
|
+
message: "Duplicate recommended capability",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
seen.add(value);
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
.default([]),
|
|
361
|
+
})
|
|
362
|
+
.strict();
|
|
363
|
+
|
|
364
|
+
export type ProjectProfile = z.infer<typeof ProjectProfileSchema>;
|
|
365
|
+
export type ProjectProfileContentType = z.infer<
|
|
366
|
+
typeof ProjectProfileContentTypeSchema
|
|
367
|
+
>;
|
package/src/config/saver.ts
CHANGED
|
@@ -62,9 +62,18 @@ export async function saveConfigToPath(
|
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
return saveTextToPath(Bun.YAML.stringify(config), filePath);
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Atomically save deterministic text outside the config schema.
|
|
70
|
+
* Used for runtime receipts that need the same crash-safe write discipline as
|
|
71
|
+
* the user config.
|
|
72
|
+
*/
|
|
73
|
+
export async function saveTextToPath(
|
|
74
|
+
content: string,
|
|
75
|
+
filePath: string
|
|
76
|
+
): Promise<SaveResult> {
|
|
68
77
|
// Ensure parent directory exists
|
|
69
78
|
const dir = dirname(filePath);
|
|
70
79
|
try {
|
|
@@ -88,7 +97,7 @@ export async function saveConfigToPath(
|
|
|
88
97
|
);
|
|
89
98
|
|
|
90
99
|
try {
|
|
91
|
-
await Bun.write(tempPath,
|
|
100
|
+
await Bun.write(tempPath, content);
|
|
92
101
|
} catch (cause) {
|
|
93
102
|
return {
|
|
94
103
|
ok: false,
|
|
@@ -102,10 +111,10 @@ export async function saveConfigToPath(
|
|
|
102
111
|
|
|
103
112
|
// Rename temp to target (atomic on POSIX, needs unlink on Windows)
|
|
104
113
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
114
|
+
if (process.platform === "win32") {
|
|
115
|
+
// Windows rename fails if dest exists, so unlink first (ignore ENOENT).
|
|
116
|
+
await unlink(filePath).catch(() => undefined);
|
|
117
|
+
}
|
|
109
118
|
await rename(tempPath, filePath);
|
|
110
119
|
} catch (cause) {
|
|
111
120
|
// Clean up temp file on rename failure
|
package/src/config/types.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* @module src/config/types
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
// node:path provides platform-correct absolute-path validation; Bun has no path utilities.
|
|
9
|
+
import { isAbsolute } from "node:path";
|
|
8
10
|
import { z } from "zod";
|
|
9
11
|
|
|
10
12
|
import { URI_PREFIX } from "../app/constants";
|
|
@@ -123,6 +125,7 @@ export const TrustedProjectRootSourceSchema = z.enum([
|
|
|
123
125
|
"cli_cwd",
|
|
124
126
|
"cli_explicit",
|
|
125
127
|
"cli_worktree",
|
|
128
|
+
"project_profile",
|
|
126
129
|
]);
|
|
127
130
|
export type TrustedProjectRootSource = z.infer<
|
|
128
131
|
typeof TrustedProjectRootSourceSchema
|
|
@@ -183,6 +186,26 @@ export const ProjectAffinityConfigSchema = z.object({
|
|
|
183
186
|
});
|
|
184
187
|
export type ProjectAffinityConfig = z.infer<typeof ProjectAffinityConfigSchema>;
|
|
185
188
|
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
+
// Local Project Profile Bindings
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Machine-local provenance for a project profile projected into this config.
|
|
197
|
+
* Apply canonicalizes `path`; the schema rejects relative paths and malformed
|
|
198
|
+
* fingerprints without making the tracked profile machine-specific.
|
|
199
|
+
*/
|
|
200
|
+
export const ProjectProfileBindingSchema = z.object({
|
|
201
|
+
path: z.string().min(1).refine(isAbsolute, {
|
|
202
|
+
message: "Project profile binding path must be absolute",
|
|
203
|
+
}),
|
|
204
|
+
fingerprint: z.string().regex(SHA256_HEX_REGEX),
|
|
205
|
+
collection: z.string().regex(COLLECTION_NAME_REGEX),
|
|
206
|
+
});
|
|
207
|
+
export type ProjectProfileBinding = z.infer<typeof ProjectProfileBindingSchema>;
|
|
208
|
+
|
|
186
209
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
187
210
|
// Context Schema
|
|
188
211
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -413,6 +436,25 @@ export const ConfigSchema = z.object({
|
|
|
413
436
|
|
|
414
437
|
/** Bounded project-aware retrieval affinity. */
|
|
415
438
|
projectAffinity: ProjectAffinityConfigSchema.optional(),
|
|
439
|
+
|
|
440
|
+
/** Machine-local, timestamp-free project profile provenance. */
|
|
441
|
+
projectProfileBindings: z
|
|
442
|
+
.array(ProjectProfileBindingSchema)
|
|
443
|
+
.max(256)
|
|
444
|
+
.superRefine((bindings, ctx) => {
|
|
445
|
+
const paths = new Set<string>();
|
|
446
|
+
for (const [index, binding] of bindings.entries()) {
|
|
447
|
+
if (paths.has(binding.path)) {
|
|
448
|
+
ctx.addIssue({
|
|
449
|
+
code: "custom",
|
|
450
|
+
message: "Project profile binding paths must be unique",
|
|
451
|
+
path: [index, "path"],
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
paths.add(binding.path);
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
.optional(),
|
|
416
458
|
});
|
|
417
459
|
|
|
418
460
|
export type Config = Omit<z.infer<typeof ConfigSchema>, "contentTypes"> & {
|