@oh-my-pi/pi-coding-agent 14.5.14 → 14.6.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/CHANGELOG.md +39 -0
- package/package.json +7 -7
- package/src/autoresearch/command-resume.md +5 -8
- package/src/autoresearch/git.ts +41 -51
- package/src/autoresearch/helpers.ts +43 -359
- package/src/autoresearch/index.ts +281 -273
- package/src/autoresearch/prompt-setup.md +43 -0
- package/src/autoresearch/prompt.md +52 -193
- package/src/autoresearch/resume-message.md +2 -8
- package/src/autoresearch/state.ts +59 -166
- package/src/autoresearch/storage.ts +687 -0
- package/src/autoresearch/tools/init-experiment.ts +201 -290
- package/src/autoresearch/tools/log-experiment.ts +304 -517
- package/src/autoresearch/tools/run-experiment.ts +117 -296
- package/src/autoresearch/tools/update-notes.ts +116 -0
- package/src/autoresearch/types.ts +16 -66
- package/src/config/settings-schema.ts +1 -1
- package/src/config/settings.ts +20 -1
- package/src/cursor.ts +1 -1
- package/src/edit/index.ts +9 -31
- package/src/edit/line-hash.ts +70 -43
- package/src/edit/modes/hashline.lark +26 -0
- package/src/edit/modes/hashline.ts +898 -1099
- package/src/edit/modes/patch.ts +0 -7
- package/src/edit/modes/replace.ts +0 -4
- package/src/edit/renderer.ts +22 -20
- package/src/edit/streaming.ts +8 -28
- package/src/eval/eval.lark +24 -30
- package/src/eval/js/context-manager.ts +5 -162
- package/src/eval/js/prelude.txt +0 -12
- package/src/eval/parse.ts +129 -129
- package/src/eval/py/prelude.py +1 -219
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +2 -2
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/modes/components/session-observer-overlay.ts +5 -2
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tree-selector.ts +4 -5
- package/src/modes/components/welcome.ts +11 -1
- package/src/modes/controllers/command-controller.ts +2 -6
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/extension-ui-controller.ts +3 -15
- package/src/modes/controllers/input-controller.ts +0 -1
- package/src/modes/controllers/selector-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +5 -7
- package/src/prompts/system/system-prompt.md +14 -38
- package/src/prompts/tools/ast-edit.md +8 -8
- package/src/prompts/tools/ast-grep.md +10 -10
- package/src/prompts/tools/eval.md +13 -31
- package/src/prompts/tools/find.md +2 -1
- package/src/prompts/tools/hashline.md +66 -57
- package/src/prompts/tools/search.md +2 -2
- package/src/session/session-manager.ts +17 -13
- package/src/tools/ast-edit.ts +141 -44
- package/src/tools/ast-grep.ts +112 -36
- package/src/tools/eval.ts +2 -53
- package/src/tools/find.ts +16 -15
- package/src/tools/path-utils.ts +36 -196
- package/src/tools/search.ts +56 -35
- package/src/utils/edit-mode.ts +2 -11
- package/src/utils/file-display-mode.ts +1 -1
- package/src/utils/git.ts +17 -0
- package/src/utils/session-color.ts +0 -12
- package/src/utils/title-generator.ts +22 -38
- package/src/autoresearch/apply-contract-to-state.ts +0 -24
- package/src/autoresearch/contract.ts +0 -288
- package/src/edit/modes/atom.lark +0 -29
- package/src/edit/modes/atom.ts +0 -1773
- package/src/prompts/tools/atom.md +0 -150
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
* Generate session titles using a smol, fast model.
|
|
3
3
|
*/
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
|
|
6
|
-
import type
|
|
7
|
-
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
5
|
+
|
|
6
|
+
import { type Api, completeSimple, type Model } from "@oh-my-pi/pi-ai";
|
|
8
7
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
9
8
|
import type { ModelRegistry } from "../config/model-registry";
|
|
10
9
|
import { resolveRoleSelection } from "../config/model-resolver";
|
|
11
10
|
import type { Settings } from "../config/settings";
|
|
12
11
|
import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
|
|
13
|
-
import { toReasoningEffort } from "../thinking";
|
|
14
12
|
|
|
15
13
|
const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
|
|
16
14
|
|
|
@@ -19,22 +17,14 @@ const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
|
19
17
|
|
|
20
18
|
const MAX_INPUT_CHARS = 2000;
|
|
21
19
|
|
|
22
|
-
function getTitleModel(
|
|
23
|
-
registry: ModelRegistry,
|
|
24
|
-
settings: Settings,
|
|
25
|
-
currentModel?: Model<Api>,
|
|
26
|
-
): { model: Model<Api>; thinkingLevel?: ThinkingLevel } | undefined {
|
|
20
|
+
function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel?: Model<Api>): Model<Api> | undefined {
|
|
27
21
|
const availableModels = registry.getAvailable();
|
|
28
22
|
if (availableModels.length === 0) return undefined;
|
|
29
23
|
|
|
30
|
-
const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels, registry);
|
|
31
|
-
if (titleModel)
|
|
32
|
-
return { model: titleModel.model, thinkingLevel: titleModel.thinkingLevel };
|
|
33
|
-
}
|
|
24
|
+
const titleModel = resolveRoleSelection(["commit", "smol"], settings, availableModels, registry)?.model;
|
|
25
|
+
if (titleModel) return titleModel;
|
|
34
26
|
|
|
35
|
-
if (currentModel)
|
|
36
|
-
return { model: currentModel };
|
|
37
|
-
}
|
|
27
|
+
if (currentModel) return currentModel;
|
|
38
28
|
|
|
39
29
|
return undefined;
|
|
40
30
|
}
|
|
@@ -44,7 +34,7 @@ function getTitleModel(
|
|
|
44
34
|
*
|
|
45
35
|
* @param firstMessage The first user message
|
|
46
36
|
* @param registry Model registry
|
|
47
|
-
* @param settings Settings used to resolve the smol role
|
|
37
|
+
* @param settings Settings used to resolve the smol role
|
|
48
38
|
* @param sessionId Optional session id for sticky API key selection
|
|
49
39
|
*/
|
|
50
40
|
export async function generateSessionTitle(
|
|
@@ -54,8 +44,8 @@ export async function generateSessionTitle(
|
|
|
54
44
|
sessionId?: string,
|
|
55
45
|
currentModel?: Model<Api>,
|
|
56
46
|
): Promise<string | null> {
|
|
57
|
-
const
|
|
58
|
-
if (!
|
|
47
|
+
const model = getTitleModel(registry, settings, currentModel);
|
|
48
|
+
if (!model) {
|
|
59
49
|
logger.debug("title-generator: no title model found");
|
|
60
50
|
return null;
|
|
61
51
|
}
|
|
@@ -67,17 +57,20 @@ export async function generateSessionTitle(
|
|
|
67
57
|
${truncatedMessage}
|
|
68
58
|
</user-message>`;
|
|
69
59
|
|
|
70
|
-
const apiKey = await registry.getApiKey(
|
|
60
|
+
const apiKey = await registry.getApiKey(model, sessionId);
|
|
71
61
|
if (!apiKey) {
|
|
72
62
|
logger.debug("title-generator: no API key for smol model", {
|
|
73
|
-
provider:
|
|
74
|
-
id:
|
|
63
|
+
provider: model.provider,
|
|
64
|
+
id: model.id,
|
|
75
65
|
});
|
|
76
66
|
return null;
|
|
77
67
|
}
|
|
78
68
|
|
|
69
|
+
// Title generation is a 3-6 word task; force reasoning off so reasoning models
|
|
70
|
+
// don't burn the entire output budget on internal thinking and return an empty
|
|
71
|
+
// string. With reasoning disabled, 30 tokens of output is plenty.
|
|
79
72
|
const request = {
|
|
80
|
-
model: `${
|
|
73
|
+
model: `${model.provider}/${model.id}`,
|
|
81
74
|
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
82
75
|
userMessage,
|
|
83
76
|
maxTokens: 30,
|
|
@@ -86,7 +79,7 @@ ${truncatedMessage}
|
|
|
86
79
|
|
|
87
80
|
try {
|
|
88
81
|
const response = await completeSimple(
|
|
89
|
-
|
|
82
|
+
model,
|
|
90
83
|
{
|
|
91
84
|
systemPrompt: request.systemPrompt,
|
|
92
85
|
messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
|
|
@@ -94,7 +87,7 @@ ${truncatedMessage}
|
|
|
94
87
|
{
|
|
95
88
|
apiKey,
|
|
96
89
|
maxTokens: 30,
|
|
97
|
-
|
|
90
|
+
disableReasoning: true,
|
|
98
91
|
},
|
|
99
92
|
);
|
|
100
93
|
|
|
@@ -153,13 +146,8 @@ function getFallbackTerminalTitle(cwd: string | undefined): string | undefined {
|
|
|
153
146
|
return sanitizeTerminalTitlePart(baseName);
|
|
154
147
|
}
|
|
155
148
|
|
|
156
|
-
export function formatSessionTerminalTitle(
|
|
157
|
-
sessionName
|
|
158
|
-
cwd?: string,
|
|
159
|
-
titleSource?: "auto" | "user" | undefined,
|
|
160
|
-
): string {
|
|
161
|
-
const label =
|
|
162
|
-
sanitizeTerminalTitlePart(titleSource === "auto" ? undefined : sessionName) ?? getFallbackTerminalTitle(cwd);
|
|
149
|
+
export function formatSessionTerminalTitle(sessionName: string | undefined, cwd?: string): string {
|
|
150
|
+
const label = sanitizeTerminalTitlePart(sessionName) ?? getFallbackTerminalTitle(cwd);
|
|
163
151
|
return label ? `${DEFAULT_TERMINAL_TITLE}: ${label}` : DEFAULT_TERMINAL_TITLE;
|
|
164
152
|
}
|
|
165
153
|
|
|
@@ -170,12 +158,8 @@ export function setTerminalTitle(title: string): void {
|
|
|
170
158
|
process.stdout.write(`\x1b]0;${sanitizeTerminalTitlePart(title) ?? DEFAULT_TERMINAL_TITLE}\x07`);
|
|
171
159
|
}
|
|
172
160
|
|
|
173
|
-
export function setSessionTerminalTitle(
|
|
174
|
-
sessionName
|
|
175
|
-
cwd?: string,
|
|
176
|
-
titleSource?: "auto" | "user" | undefined,
|
|
177
|
-
): void {
|
|
178
|
-
setTerminalTitle(formatSessionTerminalTitle(sessionName, cwd, titleSource));
|
|
161
|
+
export function setSessionTerminalTitle(sessionName: string | undefined, cwd?: string): void {
|
|
162
|
+
setTerminalTitle(formatSessionTerminalTitle(sessionName, cwd));
|
|
179
163
|
}
|
|
180
164
|
|
|
181
165
|
/**
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { inferMetricUnitFromName } from "./helpers";
|
|
2
|
-
import type { AutoresearchContract, ExperimentState } from "./types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Updates session fields from a validated `autoresearch.md` parse (same fields as `init_experiment`).
|
|
6
|
-
* Does not touch `name`, `currentSegment`, `results`, `bestMetric`, `confidence`, or `maxExperiments`.
|
|
7
|
-
*/
|
|
8
|
-
export function applyAutoresearchContractToExperimentState(
|
|
9
|
-
contract: AutoresearchContract,
|
|
10
|
-
state: ExperimentState,
|
|
11
|
-
): void {
|
|
12
|
-
const benchmarkContract = contract.benchmark;
|
|
13
|
-
state.metricName = benchmarkContract.primaryMetric ?? state.metricName;
|
|
14
|
-
state.metricUnit = benchmarkContract.metricUnit;
|
|
15
|
-
state.bestDirection = benchmarkContract.direction ?? "lower";
|
|
16
|
-
state.secondaryMetrics = benchmarkContract.secondaryMetrics.map(name => ({
|
|
17
|
-
name,
|
|
18
|
-
unit: inferMetricUnitFromName(name),
|
|
19
|
-
}));
|
|
20
|
-
state.benchmarkCommand = benchmarkContract.command?.trim() ?? state.benchmarkCommand;
|
|
21
|
-
state.scopePaths = [...contract.scopePaths];
|
|
22
|
-
state.offLimits = [...contract.offLimits];
|
|
23
|
-
state.constraints = [...contract.constraints];
|
|
24
|
-
}
|
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import type { AutoresearchBenchmarkContract, AutoresearchContract, MetricDirection } from "./types";
|
|
4
|
-
|
|
5
|
-
export interface AutoresearchContractLoadResult {
|
|
6
|
-
contract: AutoresearchContract;
|
|
7
|
-
errors: string[];
|
|
8
|
-
path: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface AutoresearchScriptSnapshot {
|
|
12
|
-
benchmarkScript: string;
|
|
13
|
-
benchmarkScriptPath: string;
|
|
14
|
-
checksScript: string | null;
|
|
15
|
-
checksScriptPath: string;
|
|
16
|
-
errors: string[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const HEADING_REGEX = /^##\s+(.+?)\s*$/;
|
|
20
|
-
const LIST_ITEM_REGEX = /^\s*[-*]\s+(.*)$/;
|
|
21
|
-
const KEY_VALUE_REGEX = /^\s*[-*]\s+([^:]+):\s*(.*)$/;
|
|
22
|
-
|
|
23
|
-
export function readAutoresearchContract(workDir: string): AutoresearchContractLoadResult {
|
|
24
|
-
const contractPath = path.join(workDir, "autoresearch.md");
|
|
25
|
-
let content = "";
|
|
26
|
-
try {
|
|
27
|
-
content = fs.readFileSync(contractPath, "utf8");
|
|
28
|
-
} catch {
|
|
29
|
-
return {
|
|
30
|
-
contract: createEmptyAutoresearchContract(),
|
|
31
|
-
errors: [`${contractPath} does not exist. Create it before initializing autoresearch.`],
|
|
32
|
-
path: contractPath,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const contract = parseAutoresearchContract(content);
|
|
37
|
-
const errors = validateAutoresearchContract(contract);
|
|
38
|
-
return { contract, errors, path: contractPath };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function parseAutoresearchContract(markdown: string): AutoresearchContract {
|
|
42
|
-
const sections = extractSections(markdown);
|
|
43
|
-
return {
|
|
44
|
-
benchmark: parseBenchmarkSection(sections.get("benchmark") ?? ""),
|
|
45
|
-
scopePaths: parseListSection(sections.get("files in scope") ?? "", normalizeContractPathSpec),
|
|
46
|
-
offLimits: parseListSection(sections.get("off limits") ?? "", normalizeContractPathSpec),
|
|
47
|
-
constraints: parseListSection(sections.get("constraints") ?? ""),
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function validateAutoresearchContract(contract: AutoresearchContract): string[] {
|
|
52
|
-
const errors: string[] = [];
|
|
53
|
-
if (!contract.benchmark.command) {
|
|
54
|
-
errors.push("Benchmark.command is required in autoresearch.md.");
|
|
55
|
-
}
|
|
56
|
-
if (!contract.benchmark.primaryMetric) {
|
|
57
|
-
errors.push("Benchmark.primary metric is required in autoresearch.md.");
|
|
58
|
-
}
|
|
59
|
-
if (!contract.benchmark.direction) {
|
|
60
|
-
errors.push("Benchmark.direction must be `lower` or `higher` in autoresearch.md.");
|
|
61
|
-
}
|
|
62
|
-
if (contract.scopePaths.length === 0) {
|
|
63
|
-
errors.push("Files in Scope must contain at least one path in autoresearch.md.");
|
|
64
|
-
}
|
|
65
|
-
for (const scopePath of contract.scopePaths) {
|
|
66
|
-
if (isUnsafeContractPathSpec(scopePath)) {
|
|
67
|
-
errors.push(`Files in Scope contains an invalid path: ${scopePath}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
for (const offLimitsPath of contract.offLimits) {
|
|
71
|
-
if (isUnsafeContractPathSpec(offLimitsPath)) {
|
|
72
|
-
errors.push(`Off Limits contains an invalid path: ${offLimitsPath}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return errors;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function loadAutoresearchScriptSnapshot(workDir: string): AutoresearchScriptSnapshot {
|
|
79
|
-
const benchmarkScriptPath = path.join(workDir, "autoresearch.sh");
|
|
80
|
-
const checksScriptPath = path.join(workDir, "autoresearch.checks.sh");
|
|
81
|
-
const errors: string[] = [];
|
|
82
|
-
|
|
83
|
-
let benchmarkScript = "";
|
|
84
|
-
try {
|
|
85
|
-
benchmarkScript = fs.readFileSync(benchmarkScriptPath, "utf8");
|
|
86
|
-
} catch {
|
|
87
|
-
errors.push(`${benchmarkScriptPath} does not exist. Create it before initializing autoresearch.`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let checksScript: string | null = null;
|
|
91
|
-
try {
|
|
92
|
-
checksScript = fs.readFileSync(checksScriptPath, "utf8");
|
|
93
|
-
} catch {
|
|
94
|
-
checksScript = null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
benchmarkScript,
|
|
99
|
-
benchmarkScriptPath,
|
|
100
|
-
checksScript,
|
|
101
|
-
checksScriptPath,
|
|
102
|
-
errors,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function normalizeAutoresearchList(values: readonly string[]): string[] {
|
|
107
|
-
const normalized: string[] = [];
|
|
108
|
-
const seen = new Set<string>();
|
|
109
|
-
for (const value of values) {
|
|
110
|
-
const trimmed = value.trim();
|
|
111
|
-
if (trimmed.length === 0) continue;
|
|
112
|
-
if (seen.has(trimmed)) continue;
|
|
113
|
-
seen.add(trimmed);
|
|
114
|
-
normalized.push(trimmed);
|
|
115
|
-
}
|
|
116
|
-
return normalized;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function normalizeContractPathSpec(value: string): string {
|
|
120
|
-
const normalized = path.posix.normalize(value.trim().replaceAll("\\", "/"));
|
|
121
|
-
if (normalized === "." || normalized === "./") return ".";
|
|
122
|
-
return normalized.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function pathMatchesContractPath(pathValue: string, specValue: string): boolean {
|
|
126
|
-
const normalizedPath = normalizeContractPathSpec(pathValue);
|
|
127
|
-
const normalizedSpec = normalizeContractPathSpec(specValue);
|
|
128
|
-
if (normalizedSpec === ".") return true;
|
|
129
|
-
return normalizedPath === normalizedSpec || normalizedPath.startsWith(`${normalizedSpec}/`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function contractListsEqual(left: readonly string[], right: readonly string[]): boolean {
|
|
133
|
-
const normalizedLeft = normalizeAutoresearchList(left);
|
|
134
|
-
const normalizedRight = normalizeAutoresearchList(right);
|
|
135
|
-
if (normalizedLeft.length !== normalizedRight.length) return false;
|
|
136
|
-
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function contractPathListsEqual(left: readonly string[], right: readonly string[]): boolean {
|
|
140
|
-
const normalizedLeft = normalizeContractPathList(left);
|
|
141
|
-
const normalizedRight = normalizeContractPathList(right);
|
|
142
|
-
if (normalizedLeft.length !== normalizedRight.length) return false;
|
|
143
|
-
return normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function createEmptyAutoresearchContract(): AutoresearchContract {
|
|
147
|
-
return {
|
|
148
|
-
benchmark: {
|
|
149
|
-
command: null,
|
|
150
|
-
primaryMetric: null,
|
|
151
|
-
metricUnit: "",
|
|
152
|
-
direction: null,
|
|
153
|
-
secondaryMetrics: [],
|
|
154
|
-
},
|
|
155
|
-
scopePaths: [],
|
|
156
|
-
offLimits: [],
|
|
157
|
-
constraints: [],
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function normalizeContractPathList(values: readonly string[]): string[] {
|
|
162
|
-
return normalizeAutoresearchList(values.map(normalizeContractPathSpec)).sort((left, right) =>
|
|
163
|
-
left.localeCompare(right),
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function extractSections(markdown: string): Map<string, string> {
|
|
168
|
-
const sections = new Map<string, string>();
|
|
169
|
-
const lines = markdown.split("\n");
|
|
170
|
-
let currentHeading: string | null = null;
|
|
171
|
-
let currentLines: string[] = [];
|
|
172
|
-
|
|
173
|
-
for (const line of lines) {
|
|
174
|
-
const headingMatch = line.match(HEADING_REGEX);
|
|
175
|
-
if (headingMatch) {
|
|
176
|
-
if (currentHeading) {
|
|
177
|
-
sections.set(currentHeading, currentLines.join("\n").trim());
|
|
178
|
-
}
|
|
179
|
-
currentHeading = headingMatch[1]?.trim().toLowerCase() ?? null;
|
|
180
|
-
currentLines = [];
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (currentHeading) {
|
|
184
|
-
currentLines.push(line);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (currentHeading) {
|
|
189
|
-
sections.set(currentHeading, currentLines.join("\n").trim());
|
|
190
|
-
}
|
|
191
|
-
return sections;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function parseBenchmarkSection(section: string): AutoresearchBenchmarkContract {
|
|
195
|
-
const entries = new Map<string, string>();
|
|
196
|
-
const lines = section.split("\n");
|
|
197
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
198
|
-
const rawLine = lines[index] ?? "";
|
|
199
|
-
const match = rawLine.match(KEY_VALUE_REGEX);
|
|
200
|
-
if (!match) continue;
|
|
201
|
-
const key = normalizeKey(match[1] ?? "");
|
|
202
|
-
let value = (match[2] ?? "").trim();
|
|
203
|
-
if (key === "secondarymetrics") {
|
|
204
|
-
const nestedItems: string[] = [];
|
|
205
|
-
for (let nestedIndex = index + 1; nestedIndex < lines.length; nestedIndex += 1) {
|
|
206
|
-
const nestedLine = lines[nestedIndex] ?? "";
|
|
207
|
-
if (nestedLine.match(KEY_VALUE_REGEX)) break;
|
|
208
|
-
const nestedMatch = nestedLine.match(/^\s{2,}[-*]\s+(.*)$/);
|
|
209
|
-
if (!nestedMatch) {
|
|
210
|
-
if (nestedLine.trim().length > 0) break;
|
|
211
|
-
continue;
|
|
212
|
-
}
|
|
213
|
-
nestedItems.push((nestedMatch[1] ?? "").trim());
|
|
214
|
-
index = nestedIndex;
|
|
215
|
-
}
|
|
216
|
-
if (nestedItems.length > 0) {
|
|
217
|
-
value = [value, ...nestedItems].filter(Boolean).join(", ");
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
entries.set(key, value);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const direction = parseDirection(entries.get("direction"));
|
|
224
|
-
return {
|
|
225
|
-
command: readNullableEntry(entries.get("command")),
|
|
226
|
-
primaryMetric: readNullableEntry(entries.get("primarymetric")),
|
|
227
|
-
metricUnit: entries.get("metricunit")?.trim() ?? "",
|
|
228
|
-
direction,
|
|
229
|
-
secondaryMetrics: parseSecondaryMetrics(entries.get("secondarymetrics")),
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function parseListSection(section: string, normalizeItem?: (value: string) => string): string[] {
|
|
234
|
-
const items: string[] = [];
|
|
235
|
-
let activeItem: string | null = null;
|
|
236
|
-
for (const rawLine of section.split("\n")) {
|
|
237
|
-
const line = rawLine.trimEnd();
|
|
238
|
-
if (line.trim().length === 0) continue;
|
|
239
|
-
const match = rawLine.match(LIST_ITEM_REGEX);
|
|
240
|
-
if (match) {
|
|
241
|
-
if (activeItem) items.push(activeItem);
|
|
242
|
-
activeItem = (match[1] ?? "").trim();
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
if (activeItem && /^\s{2,}\S/.test(rawLine)) {
|
|
246
|
-
activeItem = `${activeItem} ${line.trim()}`;
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
if (activeItem) {
|
|
250
|
-
items.push(activeItem);
|
|
251
|
-
activeItem = null;
|
|
252
|
-
}
|
|
253
|
-
items.push(line.trim());
|
|
254
|
-
}
|
|
255
|
-
if (activeItem) {
|
|
256
|
-
items.push(activeItem);
|
|
257
|
-
}
|
|
258
|
-
const normalizedItems = normalizeAutoresearchList(items);
|
|
259
|
-
return normalizeItem ? normalizedItems.map(normalizeItem) : normalizedItems;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function normalizeKey(value: string): string {
|
|
263
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function parseDirection(value: string | undefined): MetricDirection | null {
|
|
267
|
-
if (value === "lower" || value === "higher") return value;
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function readNullableEntry(value: string | undefined): string | null {
|
|
272
|
-
const trimmed = value?.trim() ?? "";
|
|
273
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function parseSecondaryMetrics(value: string | undefined): string[] {
|
|
277
|
-
if (!value) return [];
|
|
278
|
-
return normalizeAutoresearchList(
|
|
279
|
-
value
|
|
280
|
-
.split(",")
|
|
281
|
-
.map(entry => entry.trim())
|
|
282
|
-
.filter(Boolean),
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function isUnsafeContractPathSpec(value: string): boolean {
|
|
287
|
-
return path.posix.isAbsolute(value) || value === ".." || value.startsWith("../");
|
|
288
|
-
}
|
package/src/edit/modes/atom.lark
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
%import common.LF
|
|
2
|
-
|
|
3
|
-
start: file_section+
|
|
4
|
-
|
|
5
|
-
file_section: file_header (line_change | whole_file_change)
|
|
6
|
-
file_header: "---" filename LF
|
|
7
|
-
|
|
8
|
-
filename: /(.+)/
|
|
9
|
-
|
|
10
|
-
line_change: line* mutation_line line*
|
|
11
|
-
line: insert_line | delete_line | set_block | move_line | blank
|
|
12
|
-
mutation_line: insert_line | delete_line | set_block
|
|
13
|
-
|
|
14
|
-
whole_file_change: blank* whole_file_line blank*
|
|
15
|
-
whole_file_line: remove_file | move_file
|
|
16
|
-
remove_file: "!rm" LF
|
|
17
|
-
move_file: "!mv" WS destination LF
|
|
18
|
-
destination: /(?:[^ \t\r\n]+|"[^"\r\n]+"|'[^'\r\n]+')/
|
|
19
|
-
|
|
20
|
-
insert_line: "+" /(.*)/ LF
|
|
21
|
-
delete_line: "-" (LID ".." LID | LID) LF
|
|
22
|
-
set_line: (LID ".." LID | LID) WS? "=" /(.*)/ LF
|
|
23
|
-
set_block: set_line continuation_line*
|
|
24
|
-
continuation_line: "\\" /(.*)/ LF
|
|
25
|
-
move_line: ("@" LID | "^" LID | "^" | "$") LF
|
|
26
|
-
|
|
27
|
-
LID: /[1-9][0-9]*[a-z]{2}/
|
|
28
|
-
WS: /[ \t]+/
|
|
29
|
-
blank: LF
|