@open-agent-toolkit/cli 0.3.36 → 0.3.37
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/provider-sync/pilot-with-a-team.md +6 -6
- package/assets/docs/provider-sync/providers.md +1 -0
- package/assets/docs/reference/what-oat-writes.md +14 -11
- package/assets/public-package-versions.json +4 -4
- package/dist/providers/codex/codec/config-merge.d.ts +5 -1
- package/dist/providers/codex/codec/config-merge.d.ts.map +1 -1
- package/dist/providers/codex/codec/config-merge.js +111 -62
- package/dist/providers/codex/codec/config-toml-edit.d.ts +8 -0
- package/dist/providers/codex/codec/config-toml-edit.d.ts.map +1 -0
- package/dist/providers/codex/codec/config-toml-edit.js +718 -0
- package/package.json +3 -3
|
@@ -177,12 +177,12 @@ inside the working tree. Remove it with `oat init --scope project --no-hook`.
|
|
|
177
177
|
|
|
178
178
|
## Mixed tools
|
|
179
179
|
|
|
180
|
-
| Provider | What OAT generates in the repository | What the team should know
|
|
181
|
-
| ----------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
182
|
-
| Claude Code | A relative link per skill in `.claude/skills/` and per agent in `.claude/agents/`; rules as generated copies in `.claude/rules/` | The only provider with skill views, so a new skill reaches Claude Code only after sync
|
|
183
|
-
| Cursor | No skill files: it reads `.agents/skills`. A link per agent in `.cursor/agents/`; rules as `.cursor/rules/*.mdc` files | Its `.cursor/skills/` folder is not managed; skills found there are reported as strays
|
|
184
|
-
| Codex | No skill files: it reads `.agents/skills`. A generated `.codex/agents/<name>.toml` per agent, registered in `.codex/config.toml` | Sync merges into an existing `.codex/config.toml`: it adds OAT's tables, turns on `multi_agent` and raises `max_depth` to at least 2, and keeps your other settings |
|
|
185
|
-
| Gemini | Nothing; it reads skills and agents from `.agents/` directly | Nothing to commit
|
|
180
|
+
| Provider | What OAT generates in the repository | What the team should know |
|
|
181
|
+
| ----------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
182
|
+
| Claude Code | A relative link per skill in `.claude/skills/` and per agent in `.claude/agents/`; rules as generated copies in `.claude/rules/` | The only provider with skill views, so a new skill reaches Claude Code only after sync |
|
|
183
|
+
| Cursor | No skill files: it reads `.agents/skills`. A link per agent in `.cursor/agents/`; rules as `.cursor/rules/*.mdc` files | Its `.cursor/skills/` folder is not managed; skills found there are reported as strays |
|
|
184
|
+
| Codex | No skill files: it reads `.agents/skills`. A generated `.codex/agents/<name>.toml` per agent, registered in `.codex/config.toml` | Sync merges into an existing `.codex/config.toml`: it adds OAT's tables, turns on `multi_agent` and raises `max_depth` to at least 2, and keeps your other settings and comments |
|
|
185
|
+
| Gemini | Nothing; it reads skills and agents from `.agents/` directly | Nothing to commit |
|
|
186
186
|
|
|
187
187
|
If OAT's own implementer and reviewer agents are installed in the repository,
|
|
188
188
|
sync also generates one file per supported model for each of them, in
|
|
@@ -58,6 +58,7 @@ description: 'Provider-specific path mappings for Claude, Cursor, Copilot, Gemin
|
|
|
58
58
|
- Canonical markdown agents in `.agents/agents/*.md` are exported to Codex runtime roles:
|
|
59
59
|
- `.codex/agents/<role>.toml`
|
|
60
60
|
- `.codex/config.toml` (`[features] multi_agent = true`, `[agents.<role>]` upserts)
|
|
61
|
+
- Sync and direct materialization edit `config.toml` in place. They change only `features.multi_agent`, `agents.max_depth`, and the `description` and `config_file` keys of managed `[agents.<role>]` tables, and remove stale managed role tables. Comments (including other tools' ownership markers), blank lines, table order, and other keys stay byte for byte, and the file is not rewritten when nothing managed changed. Removing a stale role deletes its whole table, including comments inside it; comments above or after the table stay. Dotted keys and inline tables are edited where they are defined. New keys and tables are never added between another tool's `# BEGIN <label>` and `# END <label>` comments: a key goes before the block, and a table that cannot sit next to its related tables outside the block goes at the end of the file. If `features`, `agents`, or a managed role is not a table, or a key can only be added inside such a block, sync stops with an error instead of rewriting the file.
|
|
61
62
|
- Codex role files include OAT managed provenance headers and are regenerated by `oat sync --scope project|user|all`
|
|
62
63
|
- Project sync writes and maintains the version-controlled supported catalogue: GPT-6.1 Sol and GPT-6 Luna, Sol, and Astra at `low`, `medium`, `high`, `xhigh`, and `max`; GPT-5.6 Luna and Terra through `xhigh`; and GPT-5.6 Sol through `max`. Both `oat-phase-implementer` and `oat-reviewer` receive each supported pair (66 pinned variants). Sync writes files; it does not create a Git commit.
|
|
63
64
|
- Custom targets follow configuration provenance: user-config targets write only under `~/.codex`; shared, repo-local, and active-project targets write under the project's tracked `.codex` view.
|
|
@@ -45,10 +45,13 @@ agents, templates and scripts that `oat tools install` adds.
|
|
|
45
45
|
## What each command writes
|
|
46
46
|
|
|
47
47
|
Where a file already existed, OAT appended its own marked block instead of
|
|
48
|
-
replacing it. This held for `.gitignore`, `.gitattributes
|
|
49
|
-
`pre-commit` hook
|
|
50
|
-
|
|
51
|
-
[
|
|
48
|
+
replacing it. This held for `.gitignore`, `.gitattributes` and an existing
|
|
49
|
+
`pre-commit` hook. From version 0.3.37, an existing `.codex/config.toml` is
|
|
50
|
+
edited in place: OAT changes only `multi_agent`, `max_depth` and its own
|
|
51
|
+
`[agents.<name>]` tables, and comments, blank lines, table order and your other
|
|
52
|
+
settings stay as they were (see the
|
|
53
|
+
[provider table](#syncing-provider-views)). Earlier versions rewrote the whole
|
|
54
|
+
file and dropped its comments. A pre-existing
|
|
52
55
|
`~/.claude/settings.json` was left untouched.
|
|
53
56
|
|
|
54
57
|
### Setting up
|
|
@@ -82,13 +85,13 @@ views OAT manages. A provider does not have to be enabled: an existing
|
|
|
82
85
|
`.claude/` directory in the repository, or `~/.claude/` in your home directory,
|
|
83
86
|
is enough for `oat sync` to create Claude views.
|
|
84
87
|
|
|
85
|
-
| Provider | Repository views created by `oat sync --scope project`
|
|
86
|
-
| -------- |
|
|
87
|
-
| Claude | `.claude/skills/<name>` and `.claude/agents/<name>.md`, relative symlinks into `.agents/`
|
|
88
|
-
| Cursor | `.cursor/agents/`: relative symlinks for agents, plus generated model-variant files marked `# oat-managed: true`
|
|
89
|
-
| Copilot | `.github/agents/`, relative symlinks
|
|
90
|
-
| Codex | `.codex/agents/*.toml` and `.codex/config.toml`, generated files. In an existing `config.toml`, OAT adds its `[agents.<name>]` tables and sets `[features] multi_agent = true` and `[agents] max_depth` to at least 2, changing those two values if you had set them lower; your other settings stay. |
|
|
91
|
-
| Gemini | Nothing
|
|
88
|
+
| Provider | Repository views created by `oat sync --scope project` |
|
|
89
|
+
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
90
|
+
| Claude | `.claude/skills/<name>` and `.claude/agents/<name>.md`, relative symlinks into `.agents/` |
|
|
91
|
+
| Cursor | `.cursor/agents/`: relative symlinks for agents, plus generated model-variant files marked `# oat-managed: true` |
|
|
92
|
+
| Copilot | `.github/agents/`, relative symlinks |
|
|
93
|
+
| Codex | `.codex/agents/*.toml` and `.codex/config.toml`, generated files. In an existing `config.toml`, OAT adds its `[agents.<name>]` tables and sets `[features] multi_agent = true` and `[agents] max_depth` to at least 2, changing those two values if you had set them lower; your other settings and comments stay byte for byte (0.3.37 and later). |
|
|
94
|
+
| Gemini | Nothing |
|
|
92
95
|
|
|
93
96
|
OAT creates skill views only for Claude. It reports the other providers' skill
|
|
94
97
|
support as `native-read` and creates no skill views for them. No generated
|
|
@@ -22,7 +22,11 @@ export interface CodexConfigMergeResult {
|
|
|
22
22
|
}
|
|
23
23
|
/** Reads a valid numeric agents.max_depth value from Codex TOML content. */
|
|
24
24
|
export declare function readCodexMaxDepth(content: string | null): number | null;
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Merges managed Codex roles and required shared agent configuration in place:
|
|
27
|
+
* other tools keep ownership markers in this file's comments, so unmanaged
|
|
28
|
+
* content must survive byte for byte.
|
|
29
|
+
*/
|
|
26
30
|
export declare function mergeCodexConfig({ existingContent, desiredRoles, staleManagedRoles, inheritedMaxDepth, }: CodexConfigMergeArgs): CodexConfigMergeResult;
|
|
27
31
|
/** Merges one managed Codex role using the shared configuration contract. */
|
|
28
32
|
export declare function mergeCodexConfigForRole({ existingContent, role, staleManagedRoles, inheritedMaxDepth, }: CodexSingleRoleConfigMergeArgs): CodexConfigMergeResult;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-merge.d.ts","sourceRoot":"","sources":["../../../../src/providers/codex/codec/config-merge.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config-merge.d.ts","sourceRoot":"","sources":["../../../../src/providers/codex/codec/config-merge.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,sBAAsB,EAAE,CAAC;IACvC,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,8BAA8B;IAC7C,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,EAAE,sBAAsB,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAqFD,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAIvE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,eAAe,EACf,YAAY,EACZ,iBAAsB,EACtB,iBAAiB,GAClB,EAAE,oBAAoB,GAAG,sBAAsB,CAkH/C;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,EACtC,eAAe,EACf,IAAI,EACJ,iBAAsB,EACtB,iBAAiB,GAClB,EAAE,8BAA8B,GAAG,sBAAsB,CAOzD"}
|
|
@@ -1,44 +1,6 @@
|
|
|
1
1
|
import { CliError } from '../../../errors/index.js';
|
|
2
2
|
import TOML from '@iarna/toml';
|
|
3
|
-
import {
|
|
4
|
-
function isEscapedBasicStringDelimiter(line, index) {
|
|
5
|
-
let backslashCount = 0;
|
|
6
|
-
for (let cursor = index - 1; cursor >= 0 && line[cursor] === '\\'; cursor--) {
|
|
7
|
-
backslashCount += 1;
|
|
8
|
-
}
|
|
9
|
-
return backslashCount % 2 === 1;
|
|
10
|
-
}
|
|
11
|
-
function scanMultilineStringDelimiter(line, initialDelimiter) {
|
|
12
|
-
let delimiter = initialDelimiter;
|
|
13
|
-
for (let index = 0; index < line.length - 2; index++) {
|
|
14
|
-
if (delimiter === null) {
|
|
15
|
-
const candidate = line.slice(index, index + 3);
|
|
16
|
-
if ((candidate === '"""' && !isEscapedBasicStringDelimiter(line, index)) ||
|
|
17
|
-
candidate === "'''") {
|
|
18
|
-
delimiter = candidate;
|
|
19
|
-
index += 2;
|
|
20
|
-
}
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
if (line.startsWith(delimiter, index) &&
|
|
24
|
-
(delimiter === "'''" || !isEscapedBasicStringDelimiter(line, index))) {
|
|
25
|
-
delimiter = null;
|
|
26
|
-
index += 2;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return delimiter;
|
|
30
|
-
}
|
|
31
|
-
function normalizeCodexConfigIndentation(content) {
|
|
32
|
-
let multilineDelimiter = null;
|
|
33
|
-
return content
|
|
34
|
-
.split('\n')
|
|
35
|
-
.map((line) => {
|
|
36
|
-
const normalizedLine = multilineDelimiter === null ? line.trimStart() : line;
|
|
37
|
-
multilineDelimiter = scanMultilineStringDelimiter(normalizedLine, multilineDelimiter);
|
|
38
|
-
return normalizedLine;
|
|
39
|
-
})
|
|
40
|
-
.join('\n');
|
|
41
|
-
}
|
|
3
|
+
import { removeTomlTable, renderTomlString, setTomlValue, } from './config-toml-edit.js';
|
|
42
4
|
function parseConfig(content) {
|
|
43
5
|
if (!content || content.trim() === '') {
|
|
44
6
|
return {};
|
|
@@ -57,39 +19,85 @@ function parseConfig(content) {
|
|
|
57
19
|
throw new CliError(`Failed to parse .codex/config.toml: ${error instanceof Error ? error.message : 'unknown parse error'}`);
|
|
58
20
|
}
|
|
59
21
|
}
|
|
22
|
+
function isTable(value) {
|
|
23
|
+
return (!!value &&
|
|
24
|
+
typeof value === 'object' &&
|
|
25
|
+
!Array.isArray(value) &&
|
|
26
|
+
!(value instanceof Date));
|
|
27
|
+
}
|
|
60
28
|
function getObject(value) {
|
|
61
|
-
|
|
29
|
+
return isTable(value) ? { ...value } : {};
|
|
30
|
+
}
|
|
31
|
+
function requireTableOrAbsent(value, label) {
|
|
32
|
+
if (value === undefined) {
|
|
62
33
|
return {};
|
|
63
34
|
}
|
|
64
|
-
|
|
35
|
+
if (!isTable(value)) {
|
|
36
|
+
throw new CliError(`Refusing to rewrite .codex/config.toml: ${label} must be a table. Fix it by hand and rerun sync.`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function requireNotTable(value, label) {
|
|
41
|
+
if (isTable(value)) {
|
|
42
|
+
throw new CliError(`Refusing to rewrite .codex/config.toml: ${label} must not be a table. Fix it by hand and rerun sync.`);
|
|
43
|
+
}
|
|
65
44
|
}
|
|
66
45
|
function getFiniteNumber(value) {
|
|
67
46
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
68
47
|
}
|
|
48
|
+
function canonicalTomlValue(value) {
|
|
49
|
+
if (value instanceof Date) {
|
|
50
|
+
return { $date: value.toISOString() };
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.map(canonicalTomlValue);
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'bigint') {
|
|
56
|
+
return { $bigint: value.toString() };
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
59
|
+
return { $number: String(value) };
|
|
60
|
+
}
|
|
61
|
+
if (isTable(value)) {
|
|
62
|
+
return Object.fromEntries(Object.keys(value)
|
|
63
|
+
.sort()
|
|
64
|
+
.map((key) => [key, canonicalTomlValue(value[key])]));
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
69
68
|
/** Reads a valid numeric agents.max_depth value from Codex TOML content. */
|
|
70
69
|
export function readCodexMaxDepth(content) {
|
|
71
70
|
const parsed = parseConfig(content);
|
|
72
71
|
const agents = getObject(parsed.agents);
|
|
73
72
|
return getFiniteNumber(agents.max_depth);
|
|
74
73
|
}
|
|
75
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* Merges managed Codex roles and required shared agent configuration in place:
|
|
76
|
+
* other tools keep ownership markers in this file's comments, so unmanaged
|
|
77
|
+
* content must survive byte for byte.
|
|
78
|
+
*/
|
|
76
79
|
export function mergeCodexConfig({ existingContent, desiredRoles, staleManagedRoles = [], inheritedMaxDepth, }) {
|
|
77
80
|
const parsed = parseConfig(existingContent);
|
|
78
|
-
const features =
|
|
79
|
-
const agents =
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
81
|
+
const features = requireTableOrAbsent(parsed.features, 'features');
|
|
82
|
+
const agents = requireTableOrAbsent(parsed.agents, 'agents');
|
|
83
|
+
const roles = new Map(desiredRoles.map((role) => [role.roleName, role]));
|
|
84
|
+
const stale = new Set(staleManagedRoles);
|
|
85
|
+
requireNotTable(features.multi_agent, 'features.multi_agent');
|
|
86
|
+
if (!stale.has('max_depth')) {
|
|
87
|
+
requireNotTable(agents.max_depth, 'agents.max_depth');
|
|
88
|
+
}
|
|
89
|
+
for (const roleName of roles.keys()) {
|
|
90
|
+
if (stale.has(roleName)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const role = requireTableOrAbsent(agents[roleName], `agents.${roleName}`);
|
|
94
|
+
requireNotTable(role.description, `agents.${roleName}.description`);
|
|
95
|
+
requireNotTable(role.config_file, `agents.${roleName}.config_file`);
|
|
96
|
+
}
|
|
97
|
+
const maxDepth = Math.max(2, getFiniteNumber(agents.max_depth) ?? 2, getFiniteNumber(inheritedMaxDepth) ?? 2);
|
|
98
|
+
const expectedAgents = { ...agents, max_depth: maxDepth };
|
|
99
|
+
for (const role of roles.values()) {
|
|
100
|
+
expectedAgents[role.roleName] = {
|
|
93
101
|
...getObject(agents[role.roleName]),
|
|
94
102
|
description: role.description,
|
|
95
103
|
config_file: role.configFile,
|
|
@@ -97,16 +105,57 @@ export function mergeCodexConfig({ existingContent, desiredRoles, staleManagedRo
|
|
|
97
105
|
}
|
|
98
106
|
const removedRoles = [];
|
|
99
107
|
for (const staleRole of staleManagedRoles) {
|
|
100
|
-
if (staleRole
|
|
101
|
-
delete
|
|
108
|
+
if (Object.hasOwn(expectedAgents, staleRole)) {
|
|
109
|
+
delete expectedAgents[staleRole];
|
|
102
110
|
removedRoles.push(staleRole);
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
|
-
const
|
|
106
|
-
const
|
|
113
|
+
const removed = new Set(removedRoles);
|
|
114
|
+
const original = existingContent !== null && existingContent.trim() !== ''
|
|
115
|
+
? existingContent
|
|
116
|
+
: '';
|
|
117
|
+
let mergedContent = original;
|
|
118
|
+
if (features.multi_agent !== true) {
|
|
119
|
+
mergedContent = setTomlValue(mergedContent, ['features', 'multi_agent'], 'true');
|
|
120
|
+
}
|
|
121
|
+
if (agents.max_depth !== maxDepth && !removed.has('max_depth')) {
|
|
122
|
+
mergedContent = setTomlValue(mergedContent, ['agents', 'max_depth'], String(maxDepth));
|
|
123
|
+
}
|
|
124
|
+
for (const staleRole of removedRoles) {
|
|
125
|
+
mergedContent = removeTomlTable(mergedContent, ['agents', staleRole]);
|
|
126
|
+
}
|
|
127
|
+
for (const role of roles.values()) {
|
|
128
|
+
if (removed.has(role.roleName)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const current = getObject(agents[role.roleName]);
|
|
132
|
+
if (current.description !== role.description) {
|
|
133
|
+
mergedContent = setTomlValue(mergedContent, ['agents', role.roleName, 'description'], renderTomlString(role.description));
|
|
134
|
+
}
|
|
135
|
+
if (current.config_file !== role.configFile) {
|
|
136
|
+
mergedContent = setTomlValue(mergedContent, ['agents', role.roleName, 'config_file'], renderTomlString(role.configFile));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const expected = {
|
|
140
|
+
...parsed,
|
|
141
|
+
features: { ...features, multi_agent: true },
|
|
142
|
+
agents: expectedAgents,
|
|
143
|
+
};
|
|
144
|
+
let actual;
|
|
145
|
+
try {
|
|
146
|
+
actual = parseConfig(mergedContent);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
actual = null;
|
|
150
|
+
}
|
|
151
|
+
if (actual === null ||
|
|
152
|
+
JSON.stringify(canonicalTomlValue(actual)) !==
|
|
153
|
+
JSON.stringify(canonicalTomlValue(expected))) {
|
|
154
|
+
throw new CliError('Refusing to write .codex/config.toml: the in-place edit did not produce the expected managed configuration. Edit the file by hand and rerun sync.');
|
|
155
|
+
}
|
|
107
156
|
return {
|
|
108
157
|
mergedContent,
|
|
109
|
-
changed,
|
|
158
|
+
changed: mergedContent !== (existingContent ?? ''),
|
|
110
159
|
removedRoles,
|
|
111
160
|
};
|
|
112
161
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type KeyPath = readonly string[];
|
|
2
|
+
/** Renders a TOML basic string; JSON escaping is not TOML-safe for all input. */
|
|
3
|
+
export declare function renderTomlString(value: string): string;
|
|
4
|
+
/** `renderedValue` must already be a valid TOML value literal. */
|
|
5
|
+
export declare function setTomlValue(text: string, path: KeyPath, renderedValue: string): string;
|
|
6
|
+
export declare function removeTomlTable(text: string, path: KeyPath): string;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=config-toml-edit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-toml-edit.d.ts","sourceRoot":"","sources":["../../../../src/providers/codex/codec/config-toml-edit.ts"],"names":[],"mappings":"AAOA,KAAK,OAAO,GAAG,SAAS,MAAM,EAAE,CAAC;AAofjC,iFAAiF;AACjF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAyBtD;AAqTD,kEAAkE;AAClE,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,aAAa,EAAE,MAAM,GACpB,MAAM,CAsFR;AA8BD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CA4BnE"}
|
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { CliError } from '../../../errors/index.js';
|
|
2
|
+
const BARE_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
3
|
+
const BARE_VALUE_TERMINATORS = new Set([
|
|
4
|
+
' ',
|
|
5
|
+
'\t',
|
|
6
|
+
',',
|
|
7
|
+
']',
|
|
8
|
+
'}',
|
|
9
|
+
'#',
|
|
10
|
+
'\r',
|
|
11
|
+
'\n',
|
|
12
|
+
]);
|
|
13
|
+
const SIMPLE_ESCAPES = {
|
|
14
|
+
b: '\b',
|
|
15
|
+
t: '\t',
|
|
16
|
+
n: '\n',
|
|
17
|
+
f: '\f',
|
|
18
|
+
r: '\r',
|
|
19
|
+
'"': '"',
|
|
20
|
+
'\\': '\\',
|
|
21
|
+
};
|
|
22
|
+
const MAX_EDIT_PASSES = 10_000;
|
|
23
|
+
function unsupported(detail) {
|
|
24
|
+
return new CliError(`Cannot edit .codex/config.toml without rewriting user content: ${detail}. Edit the file by hand and rerun sync.`);
|
|
25
|
+
}
|
|
26
|
+
function findLast(items, predicate) {
|
|
27
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
28
|
+
if (predicate(items[index])) {
|
|
29
|
+
return items[index];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
function pathsEqual(left, right) {
|
|
35
|
+
return (left.length === right.length &&
|
|
36
|
+
left.every((segment, index) => segment === right[index]));
|
|
37
|
+
}
|
|
38
|
+
function startsWithPath(path, prefix) {
|
|
39
|
+
return (path.length >= prefix.length &&
|
|
40
|
+
prefix.every((segment, index) => segment === path[index]));
|
|
41
|
+
}
|
|
42
|
+
function isStrictPrefix(prefix, path) {
|
|
43
|
+
return prefix.length < path.length && startsWithPath(path, prefix);
|
|
44
|
+
}
|
|
45
|
+
class TomlLexer {
|
|
46
|
+
text;
|
|
47
|
+
pos = 0;
|
|
48
|
+
sections = [];
|
|
49
|
+
entries = [];
|
|
50
|
+
comments = [];
|
|
51
|
+
constructor(text) {
|
|
52
|
+
this.text = text;
|
|
53
|
+
}
|
|
54
|
+
lex() {
|
|
55
|
+
let section = {
|
|
56
|
+
header: null,
|
|
57
|
+
path: [],
|
|
58
|
+
isArray: false,
|
|
59
|
+
lastEnd: 0,
|
|
60
|
+
};
|
|
61
|
+
this.sections.push(section);
|
|
62
|
+
while (this.pos < this.text.length) {
|
|
63
|
+
const lineStart = this.pos;
|
|
64
|
+
this.skipInlineWhitespace();
|
|
65
|
+
if (this.pos >= this.text.length) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
if (this.atNewline()) {
|
|
69
|
+
this.skipNewline();
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (this.peek() === '#') {
|
|
73
|
+
const start = this.pos;
|
|
74
|
+
this.skipComment();
|
|
75
|
+
this.comments.push({ lineStart, start, end: this.pos });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (this.peek() === '[') {
|
|
79
|
+
const isArray = this.text.startsWith('[[', this.pos);
|
|
80
|
+
this.pos += isArray ? 2 : 1;
|
|
81
|
+
const path = this.lexKey();
|
|
82
|
+
this.expect(isArray ? ']]' : ']');
|
|
83
|
+
const end = this.finishLine();
|
|
84
|
+
section = {
|
|
85
|
+
header: { path, isArray, lineStart, end },
|
|
86
|
+
path,
|
|
87
|
+
isArray,
|
|
88
|
+
lastEnd: end,
|
|
89
|
+
};
|
|
90
|
+
this.sections.push(section);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const entry = this.lexKeyValue(section, null, section.path, lineStart);
|
|
94
|
+
entry.end = this.finishLine();
|
|
95
|
+
section.lastEnd = entry.end;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
sections: this.sections,
|
|
99
|
+
entries: this.entries,
|
|
100
|
+
blocks: findMarkerBlocks(this.text, this.comments),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
peek(offset = 0) {
|
|
104
|
+
return this.text[this.pos + offset];
|
|
105
|
+
}
|
|
106
|
+
fail(detail) {
|
|
107
|
+
throw unsupported(`${detail} at offset ${this.pos}`);
|
|
108
|
+
}
|
|
109
|
+
atNewline() {
|
|
110
|
+
return (this.peek() === '\n' || (this.peek() === '\r' && this.peek(1) === '\n'));
|
|
111
|
+
}
|
|
112
|
+
skipNewline() {
|
|
113
|
+
this.pos += this.peek() === '\r' ? 2 : 1;
|
|
114
|
+
}
|
|
115
|
+
skipInlineWhitespace() {
|
|
116
|
+
while (this.peek() === ' ' || this.peek() === '\t') {
|
|
117
|
+
this.pos += 1;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
skipComment() {
|
|
121
|
+
while (this.pos < this.text.length && !this.atNewline()) {
|
|
122
|
+
this.pos += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
skipArrayWhitespace() {
|
|
126
|
+
for (;;) {
|
|
127
|
+
this.skipInlineWhitespace();
|
|
128
|
+
if (this.atNewline()) {
|
|
129
|
+
this.skipNewline();
|
|
130
|
+
}
|
|
131
|
+
else if (this.peek() === '#') {
|
|
132
|
+
this.skipComment();
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
expect(token) {
|
|
140
|
+
this.skipInlineWhitespace();
|
|
141
|
+
if (!this.text.startsWith(token, this.pos)) {
|
|
142
|
+
this.fail(`expected "${token}"`);
|
|
143
|
+
}
|
|
144
|
+
this.pos += token.length;
|
|
145
|
+
}
|
|
146
|
+
finishLine() {
|
|
147
|
+
this.skipInlineWhitespace();
|
|
148
|
+
if (this.peek() === '#') {
|
|
149
|
+
this.skipComment();
|
|
150
|
+
}
|
|
151
|
+
if (this.pos < this.text.length && !this.atNewline()) {
|
|
152
|
+
this.fail('expected end of line');
|
|
153
|
+
}
|
|
154
|
+
return this.pos;
|
|
155
|
+
}
|
|
156
|
+
lexKey() {
|
|
157
|
+
const segments = [];
|
|
158
|
+
for (;;) {
|
|
159
|
+
this.skipInlineWhitespace();
|
|
160
|
+
const char = this.peek();
|
|
161
|
+
if (char === '"') {
|
|
162
|
+
segments.push(this.lexBasicKey());
|
|
163
|
+
}
|
|
164
|
+
else if (char === "'") {
|
|
165
|
+
const close = this.text.indexOf("'", this.pos + 1);
|
|
166
|
+
if (close < 0) {
|
|
167
|
+
this.fail('unterminated literal key');
|
|
168
|
+
}
|
|
169
|
+
segments.push(this.text.slice(this.pos + 1, close));
|
|
170
|
+
this.pos = close + 1;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const start = this.pos;
|
|
174
|
+
while (/[A-Za-z0-9_-]/.test(this.peek() ?? '')) {
|
|
175
|
+
this.pos += 1;
|
|
176
|
+
}
|
|
177
|
+
if (start === this.pos) {
|
|
178
|
+
this.fail('expected a key');
|
|
179
|
+
}
|
|
180
|
+
segments.push(this.text.slice(start, this.pos));
|
|
181
|
+
}
|
|
182
|
+
this.skipInlineWhitespace();
|
|
183
|
+
if (this.peek() !== '.') {
|
|
184
|
+
return segments;
|
|
185
|
+
}
|
|
186
|
+
this.pos += 1;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
lexBasicKey() {
|
|
190
|
+
let value = '';
|
|
191
|
+
this.pos += 1;
|
|
192
|
+
for (;;) {
|
|
193
|
+
const char = this.peek();
|
|
194
|
+
if (char === undefined || char === '\n' || char === '\r') {
|
|
195
|
+
this.fail('unterminated quoted key');
|
|
196
|
+
}
|
|
197
|
+
this.pos += 1;
|
|
198
|
+
if (char === '"') {
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
if (char !== '\\') {
|
|
202
|
+
value += char;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const escape = this.peek() ?? '';
|
|
206
|
+
this.pos += 1;
|
|
207
|
+
if (escape in SIMPLE_ESCAPES) {
|
|
208
|
+
value += SIMPLE_ESCAPES[escape];
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const width = escape === 'u' ? 4 : escape === 'U' ? 8 : 0;
|
|
212
|
+
const hex = this.text.slice(this.pos, this.pos + width);
|
|
213
|
+
if (width === 0 || !/^[0-9A-Fa-f]+$/.test(hex) || hex.length !== width) {
|
|
214
|
+
this.fail('invalid escape in quoted key');
|
|
215
|
+
}
|
|
216
|
+
value += String.fromCodePoint(Number.parseInt(hex, 16));
|
|
217
|
+
this.pos += width;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
lexKeyValue(section, inline, containerPath, lineStart) {
|
|
221
|
+
this.skipInlineWhitespace();
|
|
222
|
+
const start = this.pos;
|
|
223
|
+
const keyPath = this.lexKey();
|
|
224
|
+
this.expect('=');
|
|
225
|
+
this.skipInlineWhitespace();
|
|
226
|
+
const entry = {
|
|
227
|
+
path: [...containerPath, ...keyPath],
|
|
228
|
+
keyPath,
|
|
229
|
+
containerPath,
|
|
230
|
+
section,
|
|
231
|
+
inline,
|
|
232
|
+
start,
|
|
233
|
+
lineStart,
|
|
234
|
+
valueStart: this.pos,
|
|
235
|
+
valueEnd: this.pos,
|
|
236
|
+
end: this.pos,
|
|
237
|
+
table: null,
|
|
238
|
+
};
|
|
239
|
+
if (!section.isArray) {
|
|
240
|
+
this.entries.push(entry);
|
|
241
|
+
}
|
|
242
|
+
entry.table = this.lexValue(entry.path, section);
|
|
243
|
+
entry.valueEnd = this.pos;
|
|
244
|
+
entry.end = this.pos;
|
|
245
|
+
return entry;
|
|
246
|
+
}
|
|
247
|
+
lexValue(path, section) {
|
|
248
|
+
const char = this.peek();
|
|
249
|
+
if (this.text.startsWith('"""', this.pos)) {
|
|
250
|
+
this.lexMultilineString('"""', true);
|
|
251
|
+
}
|
|
252
|
+
else if (this.text.startsWith("'''", this.pos)) {
|
|
253
|
+
this.lexMultilineString("'''", false);
|
|
254
|
+
}
|
|
255
|
+
else if (char === '"') {
|
|
256
|
+
this.lexSingleLineString('"', true);
|
|
257
|
+
}
|
|
258
|
+
else if (char === "'") {
|
|
259
|
+
this.lexSingleLineString("'", false);
|
|
260
|
+
}
|
|
261
|
+
else if (char === '[') {
|
|
262
|
+
this.lexArray(section);
|
|
263
|
+
}
|
|
264
|
+
else if (char === '{') {
|
|
265
|
+
return this.lexInlineTable(path, section);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
this.lexBareValue();
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
lexMultilineString(delimiter, escapes) {
|
|
273
|
+
this.pos += 3;
|
|
274
|
+
for (;;) {
|
|
275
|
+
if (this.pos >= this.text.length) {
|
|
276
|
+
this.fail('unterminated multiline string');
|
|
277
|
+
}
|
|
278
|
+
if (escapes && this.peek() === '\\') {
|
|
279
|
+
this.pos += 2;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (this.text.startsWith(delimiter, this.pos)) {
|
|
283
|
+
this.pos += 3;
|
|
284
|
+
for (let extra = 0; extra < 2 && this.peek() === delimiter[0]; extra++) {
|
|
285
|
+
this.pos += 1;
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.pos += 1;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
lexSingleLineString(delimiter, escapes) {
|
|
293
|
+
this.pos += 1;
|
|
294
|
+
for (;;) {
|
|
295
|
+
const char = this.peek();
|
|
296
|
+
if (char === undefined || char === '\n' || char === '\r') {
|
|
297
|
+
this.fail('unterminated string');
|
|
298
|
+
}
|
|
299
|
+
if (escapes && char === '\\') {
|
|
300
|
+
this.pos += 2;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
this.pos += 1;
|
|
304
|
+
if (char === delimiter) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
lexArray(section) {
|
|
310
|
+
this.pos += 1;
|
|
311
|
+
for (;;) {
|
|
312
|
+
this.skipArrayWhitespace();
|
|
313
|
+
if (this.peek() === ']') {
|
|
314
|
+
this.pos += 1;
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
if (this.pos >= this.text.length) {
|
|
318
|
+
this.fail('unterminated array');
|
|
319
|
+
}
|
|
320
|
+
this.lexValue(null, section);
|
|
321
|
+
this.skipArrayWhitespace();
|
|
322
|
+
if (this.peek() === ',') {
|
|
323
|
+
this.pos += 1;
|
|
324
|
+
}
|
|
325
|
+
else if (this.peek() !== ']') {
|
|
326
|
+
this.fail('expected "," or "]" in array');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
lexInlineTable(path, section) {
|
|
331
|
+
const table = {
|
|
332
|
+
path: path ?? [],
|
|
333
|
+
open: this.pos,
|
|
334
|
+
close: this.pos,
|
|
335
|
+
entries: [],
|
|
336
|
+
};
|
|
337
|
+
this.pos += 1;
|
|
338
|
+
this.skipInlineWhitespace();
|
|
339
|
+
if (this.peek() !== '}') {
|
|
340
|
+
for (;;) {
|
|
341
|
+
if (path === null) {
|
|
342
|
+
this.lexValueOnlyInlineEntry(section);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
table.entries.push(this.lexKeyValue(section, table, path, table.open));
|
|
346
|
+
}
|
|
347
|
+
this.skipInlineWhitespace();
|
|
348
|
+
if (this.peek() === ',') {
|
|
349
|
+
this.pos += 1;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (this.peek() !== '}') {
|
|
353
|
+
this.fail('expected "," or "}" in inline table');
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
table.close = this.pos;
|
|
359
|
+
this.pos += 1;
|
|
360
|
+
return path === null ? null : table;
|
|
361
|
+
}
|
|
362
|
+
/** Inline tables inside arrays are array elements, never table definitions. */
|
|
363
|
+
lexValueOnlyInlineEntry(section) {
|
|
364
|
+
this.skipInlineWhitespace();
|
|
365
|
+
this.lexKey();
|
|
366
|
+
this.expect('=');
|
|
367
|
+
this.skipInlineWhitespace();
|
|
368
|
+
this.lexValue(null, section);
|
|
369
|
+
}
|
|
370
|
+
lexBareValue() {
|
|
371
|
+
const start = this.pos;
|
|
372
|
+
while (this.pos < this.text.length &&
|
|
373
|
+
!BARE_VALUE_TERMINATORS.has(this.peek())) {
|
|
374
|
+
this.pos += 1;
|
|
375
|
+
}
|
|
376
|
+
if (start === this.pos) {
|
|
377
|
+
this.fail('expected a value');
|
|
378
|
+
}
|
|
379
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(this.text.slice(start, this.pos)) &&
|
|
380
|
+
/^ \d{2}:/.test(this.text.slice(this.pos, this.pos + 4))) {
|
|
381
|
+
this.pos += 1;
|
|
382
|
+
this.lexBareValue();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
function lexToml(text) {
|
|
387
|
+
return new TomlLexer(text).lex();
|
|
388
|
+
}
|
|
389
|
+
/** Renders a TOML basic string; JSON escaping is not TOML-safe for all input. */
|
|
390
|
+
export function renderTomlString(value) {
|
|
391
|
+
let rendered = '"';
|
|
392
|
+
for (const char of value) {
|
|
393
|
+
const code = char.codePointAt(0);
|
|
394
|
+
if (char === '"' || char === '\\') {
|
|
395
|
+
rendered += `\\${char}`;
|
|
396
|
+
}
|
|
397
|
+
else if (char === '\b') {
|
|
398
|
+
rendered += '\\b';
|
|
399
|
+
}
|
|
400
|
+
else if (char === '\t') {
|
|
401
|
+
rendered += '\\t';
|
|
402
|
+
}
|
|
403
|
+
else if (char === '\n') {
|
|
404
|
+
rendered += '\\n';
|
|
405
|
+
}
|
|
406
|
+
else if (char === '\f') {
|
|
407
|
+
rendered += '\\f';
|
|
408
|
+
}
|
|
409
|
+
else if (char === '\r') {
|
|
410
|
+
rendered += '\\r';
|
|
411
|
+
}
|
|
412
|
+
else if (code < 0x20 || code === 0x7f) {
|
|
413
|
+
rendered += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
414
|
+
}
|
|
415
|
+
else if (code >= 0xd800 && code <= 0xdfff) {
|
|
416
|
+
throw unsupported('a value contains an unpaired surrogate');
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
rendered += char;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return `${rendered}"`;
|
|
423
|
+
}
|
|
424
|
+
function renderKey(segment) {
|
|
425
|
+
return BARE_KEY_PATTERN.test(segment) ? segment : renderTomlString(segment);
|
|
426
|
+
}
|
|
427
|
+
function renderKeyPath(path) {
|
|
428
|
+
return path.map(renderKey).join('.');
|
|
429
|
+
}
|
|
430
|
+
function detectEol(text) {
|
|
431
|
+
const newline = text.indexOf('\n');
|
|
432
|
+
return newline > 0 && text[newline - 1] === '\r' ? '\r\n' : '\n';
|
|
433
|
+
}
|
|
434
|
+
function splice(text, start, end, replacement) {
|
|
435
|
+
return `${text.slice(0, start)}${replacement}${text.slice(end)}`;
|
|
436
|
+
}
|
|
437
|
+
function lineIndentAt(text, lineStart) {
|
|
438
|
+
return /^[ \t]*/.exec(text.slice(lineStart))[0];
|
|
439
|
+
}
|
|
440
|
+
function previousLineStart(text, lineStart) {
|
|
441
|
+
if (lineStart === 0) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
return text.lastIndexOf('\n', lineStart - 2) + 1;
|
|
445
|
+
}
|
|
446
|
+
function lineEndIncludingNewline(text, contentEnd) {
|
|
447
|
+
if (text.startsWith('\r\n', contentEnd)) {
|
|
448
|
+
return contentEnd + 2;
|
|
449
|
+
}
|
|
450
|
+
return text[contentEnd] === '\n' ? contentEnd + 1 : contentEnd;
|
|
451
|
+
}
|
|
452
|
+
function findDefinition(document, tablePath) {
|
|
453
|
+
if (tablePath.length === 0) {
|
|
454
|
+
return { kind: 'root' };
|
|
455
|
+
}
|
|
456
|
+
const section = document.sections.find((candidate) => candidate.header !== null &&
|
|
457
|
+
!candidate.isArray &&
|
|
458
|
+
pathsEqual(candidate.path, tablePath));
|
|
459
|
+
if (section) {
|
|
460
|
+
return { kind: 'header', section };
|
|
461
|
+
}
|
|
462
|
+
const inline = document.entries.find((entry) => pathsEqual(entry.path, tablePath));
|
|
463
|
+
if (inline) {
|
|
464
|
+
if (!inline.table) {
|
|
465
|
+
throw unsupported(`${renderKeyPath(tablePath)} is not a table`);
|
|
466
|
+
}
|
|
467
|
+
return { kind: 'inline', table: inline.table };
|
|
468
|
+
}
|
|
469
|
+
const dotted = document.entries.filter((entry) => isStrictPrefix(tablePath, entry.path) &&
|
|
470
|
+
isStrictPrefix(entry.containerPath, tablePath));
|
|
471
|
+
if (dotted.length > 0) {
|
|
472
|
+
return { kind: 'dotted', anchor: dotted.at(-1) };
|
|
473
|
+
}
|
|
474
|
+
return document.sections.some((candidate) => candidate.header !== null && isStrictPrefix(tablePath, candidate.path))
|
|
475
|
+
? { kind: 'implicit' }
|
|
476
|
+
: null;
|
|
477
|
+
}
|
|
478
|
+
function insertIntoInlineTable(text, document, table, path, member) {
|
|
479
|
+
const block = markerBlockAt(document, table.close);
|
|
480
|
+
if (block) {
|
|
481
|
+
throw insideMarkerBlock(path, block);
|
|
482
|
+
}
|
|
483
|
+
const last = table.entries.at(-1);
|
|
484
|
+
if (!last) {
|
|
485
|
+
return splice(text, table.open + 1, table.close, ` ${member} `);
|
|
486
|
+
}
|
|
487
|
+
return splice(text, last.valueEnd, last.valueEnd, `, ${member}`);
|
|
488
|
+
}
|
|
489
|
+
function insertAfterStatement(text, lineStart, contentEnd, line) {
|
|
490
|
+
return splice(text, contentEnd, contentEnd, `${detectEol(text)}${lineIndentAt(text, lineStart)}${line}`);
|
|
491
|
+
}
|
|
492
|
+
/*
|
|
493
|
+
* Other tools own `# BEGIN <label>` ... `# END <label>` blocks and delete every
|
|
494
|
+
* line between the pair, so nothing OAT writes may be inserted inside one.
|
|
495
|
+
* Only standalone comment lines count; text inside strings is not a marker.
|
|
496
|
+
*/
|
|
497
|
+
const MARKER_PATTERN = /^#[ \t]*(BEGIN|END)[ \t]+(\S(?:.*\S)?)[ \t]*$/;
|
|
498
|
+
function findMarkerBlocks(text, comments) {
|
|
499
|
+
const markers = comments.map((comment) => ({
|
|
500
|
+
comment,
|
|
501
|
+
match: MARKER_PATTERN.exec(text.slice(comment.start, comment.end)),
|
|
502
|
+
}));
|
|
503
|
+
const blocks = [];
|
|
504
|
+
markers.forEach(({ comment, match }, index) => {
|
|
505
|
+
if (match?.[1] !== 'BEGIN') {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const label = match[2];
|
|
509
|
+
const end = markers
|
|
510
|
+
.slice(index + 1)
|
|
511
|
+
.find((candidate) => candidate.match?.[1] === 'END' && candidate.match[2] === label);
|
|
512
|
+
if (end) {
|
|
513
|
+
blocks.push({
|
|
514
|
+
label,
|
|
515
|
+
beginLineStart: comment.lineStart,
|
|
516
|
+
beginEnd: comment.end,
|
|
517
|
+
endLineStart: end.comment.lineStart,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
return blocks;
|
|
522
|
+
}
|
|
523
|
+
function markerBlockAt(document, offset) {
|
|
524
|
+
return document.blocks.find((block) => block.beginEnd <= offset && offset <= block.endLineStart);
|
|
525
|
+
}
|
|
526
|
+
function outsideMarkerBlocks(document, offset) {
|
|
527
|
+
return offset !== null && markerBlockAt(document, offset) ? null : offset;
|
|
528
|
+
}
|
|
529
|
+
function insideMarkerBlock(path, block) {
|
|
530
|
+
return unsupported(`${renderKeyPath(path)} would be written inside the "# BEGIN ${block.label}" block`);
|
|
531
|
+
}
|
|
532
|
+
/** Appends after `anchor`, or after the last statement before a marker block. */
|
|
533
|
+
function insertIntoSection(text, document, section, anchor, path, line) {
|
|
534
|
+
let { lineStart, end } = anchor;
|
|
535
|
+
for (;;) {
|
|
536
|
+
const block = markerBlockAt(document, end);
|
|
537
|
+
if (!block) {
|
|
538
|
+
return insertAfterStatement(text, lineStart, end, line);
|
|
539
|
+
}
|
|
540
|
+
const previous = findLast(document.entries, (entry) => entry.section === section &&
|
|
541
|
+
entry.inline === null &&
|
|
542
|
+
entry.end <= block.beginLineStart);
|
|
543
|
+
const header = section.header;
|
|
544
|
+
if (previous) {
|
|
545
|
+
({ lineStart, end } = previous);
|
|
546
|
+
}
|
|
547
|
+
else if (header && header.end <= block.beginLineStart) {
|
|
548
|
+
({ lineStart, end } = header);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
throw insideMarkerBlock(path, block);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
function isCommentLine(line) {
|
|
556
|
+
return /^[ \t]*#/.test(line);
|
|
557
|
+
}
|
|
558
|
+
function isBlankLine(line) {
|
|
559
|
+
return /^[ \t]*(?:\r?\n)?$/.test(line);
|
|
560
|
+
}
|
|
561
|
+
/*
|
|
562
|
+
* Other tools wrap tables in marker comments. A comment run with content
|
|
563
|
+
* directly on both sides may be the `# END` of the block above or the
|
|
564
|
+
* `# BEGIN` of the block below, so the two helpers below return null rather
|
|
565
|
+
* than guess, and the caller appends at the end of the file instead.
|
|
566
|
+
*/
|
|
567
|
+
function leadingCommentStart(text, lineStart) {
|
|
568
|
+
let start = lineStart;
|
|
569
|
+
for (;;) {
|
|
570
|
+
const previous = previousLineStart(text, start);
|
|
571
|
+
if (previous === null) {
|
|
572
|
+
return start;
|
|
573
|
+
}
|
|
574
|
+
const line = text.slice(previous, start);
|
|
575
|
+
if (!isCommentLine(line)) {
|
|
576
|
+
return isBlankLine(line) || start === lineStart ? start : null;
|
|
577
|
+
}
|
|
578
|
+
start = previous;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function trailingCommentEnd(text, contentEnd) {
|
|
582
|
+
let end = contentEnd;
|
|
583
|
+
let cursor = lineEndIncludingNewline(text, contentEnd);
|
|
584
|
+
while (cursor < text.length) {
|
|
585
|
+
const newline = text.indexOf('\n', cursor);
|
|
586
|
+
const next = newline < 0 ? text.length : newline + 1;
|
|
587
|
+
const line = text.slice(cursor, next);
|
|
588
|
+
if (!isCommentLine(line)) {
|
|
589
|
+
return isBlankLine(line) || end === contentEnd ? end : null;
|
|
590
|
+
}
|
|
591
|
+
end = cursor + line.replace(/\r?\n$/, '').length;
|
|
592
|
+
cursor = next;
|
|
593
|
+
}
|
|
594
|
+
return end;
|
|
595
|
+
}
|
|
596
|
+
function insertSection(text, document, tablePath, lines) {
|
|
597
|
+
const eol = detectEol(text);
|
|
598
|
+
const block = [`[${renderKeyPath(tablePath)}]`, ...lines].join(eol);
|
|
599
|
+
const headers = document.sections.filter((section) => section.header !== null);
|
|
600
|
+
const firstSubTable = headers.find((section) => isStrictPrefix(tablePath, section.path));
|
|
601
|
+
const start = firstSubTable
|
|
602
|
+
? outsideMarkerBlocks(document, leadingCommentStart(text, firstSubTable.header.lineStart))
|
|
603
|
+
: null;
|
|
604
|
+
if (start !== null) {
|
|
605
|
+
return splice(text, start, start, `${block}${eol}${eol}`);
|
|
606
|
+
}
|
|
607
|
+
const familyDepth = firstSubTable ? 0 : tablePath.length - 1;
|
|
608
|
+
for (let depth = familyDepth; depth >= 1; depth--) {
|
|
609
|
+
const family = tablePath.slice(0, depth);
|
|
610
|
+
const lastInFamily = findLast(headers, (section) => startsWithPath(section.path, family));
|
|
611
|
+
if (lastInFamily) {
|
|
612
|
+
const end = outsideMarkerBlocks(document, trailingCommentEnd(text, lastInFamily.lastEnd));
|
|
613
|
+
if (end !== null) {
|
|
614
|
+
return splice(text, end, end, `${eol}${eol}${block}`);
|
|
615
|
+
}
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const contentEnd = text.length - /(?:\r?\n[ \t]*)*$/.exec(text)[0].length;
|
|
620
|
+
if (contentEnd === 0) {
|
|
621
|
+
return splice(text, 0, 0, `${block}${eol}`);
|
|
622
|
+
}
|
|
623
|
+
return splice(text, contentEnd, contentEnd, `${eol}${eol}${block}`);
|
|
624
|
+
}
|
|
625
|
+
/** `renderedValue` must already be a valid TOML value literal. */
|
|
626
|
+
export function setTomlValue(text, path, renderedValue) {
|
|
627
|
+
const document = lexToml(text);
|
|
628
|
+
const existing = document.entries.find((entry) => pathsEqual(entry.path, path));
|
|
629
|
+
if (existing) {
|
|
630
|
+
return splice(text, existing.valueStart, existing.valueEnd, renderedValue);
|
|
631
|
+
}
|
|
632
|
+
const tablePath = path.slice(0, -1);
|
|
633
|
+
let depth = tablePath.length;
|
|
634
|
+
let definition = findDefinition(document, tablePath);
|
|
635
|
+
while (definition === null) {
|
|
636
|
+
depth -= 1;
|
|
637
|
+
definition = findDefinition(document, tablePath.slice(0, depth));
|
|
638
|
+
}
|
|
639
|
+
const rest = path.slice(depth);
|
|
640
|
+
switch (definition.kind) {
|
|
641
|
+
case 'header':
|
|
642
|
+
if (rest.length === 1) {
|
|
643
|
+
const { section } = definition;
|
|
644
|
+
const lastEntry = findLast(document.entries, (entry) => entry.section === section && entry.inline === null);
|
|
645
|
+
return insertIntoSection(text, document, section, {
|
|
646
|
+
lineStart: lastEntry?.lineStart ?? section.header.lineStart,
|
|
647
|
+
end: section.lastEnd,
|
|
648
|
+
}, path, `${renderKey(rest[0])} = ${renderedValue}`);
|
|
649
|
+
}
|
|
650
|
+
break;
|
|
651
|
+
case 'inline': {
|
|
652
|
+
const member = rest
|
|
653
|
+
.slice(0, -1)
|
|
654
|
+
.reduceRight((inner, segment) => `${renderKey(segment)} = { ${inner} }`, `${renderKey(rest.at(-1))} = ${renderedValue}`);
|
|
655
|
+
return insertIntoInlineTable(text, document, definition.table, path, member);
|
|
656
|
+
}
|
|
657
|
+
case 'dotted': {
|
|
658
|
+
const { anchor } = definition;
|
|
659
|
+
const member = `${renderKeyPath(path.slice(anchor.containerPath.length))} = ${renderedValue}`;
|
|
660
|
+
if (anchor.inline) {
|
|
661
|
+
return insertIntoInlineTable(text, document, anchor.inline, path, member);
|
|
662
|
+
}
|
|
663
|
+
return insertIntoSection(text, document, anchor.section, anchor, path, member);
|
|
664
|
+
}
|
|
665
|
+
case 'root':
|
|
666
|
+
case 'implicit':
|
|
667
|
+
break;
|
|
668
|
+
}
|
|
669
|
+
if (tablePath.length === 0) {
|
|
670
|
+
throw unsupported(`top-level key ${renderKeyPath(path)} is not editable`);
|
|
671
|
+
}
|
|
672
|
+
return insertSection(text, document, tablePath, [
|
|
673
|
+
`${renderKey(path.at(-1))} = ${renderedValue}`,
|
|
674
|
+
]);
|
|
675
|
+
}
|
|
676
|
+
function removeLines(text, start, contentEnd) {
|
|
677
|
+
let removeStart = start;
|
|
678
|
+
const removeEnd = lineEndIncludingNewline(text, contentEnd);
|
|
679
|
+
const previous = previousLineStart(text, start);
|
|
680
|
+
const following = text.slice(removeEnd);
|
|
681
|
+
if (previous !== null &&
|
|
682
|
+
/^[ \t]*\r?\n$/.test(text.slice(previous, start)) &&
|
|
683
|
+
/^[ \t]*(?:\r?\n|$)/.test(following)) {
|
|
684
|
+
removeStart = previous;
|
|
685
|
+
}
|
|
686
|
+
return splice(text, removeStart, removeEnd, '');
|
|
687
|
+
}
|
|
688
|
+
function removeInlineEntry(text, entry) {
|
|
689
|
+
const table = entry.inline;
|
|
690
|
+
const index = table.entries.indexOf(entry);
|
|
691
|
+
if (table.entries.length === 1) {
|
|
692
|
+
return splice(text, table.open + 1, table.close, '');
|
|
693
|
+
}
|
|
694
|
+
const next = table.entries[index + 1];
|
|
695
|
+
if (next) {
|
|
696
|
+
return splice(text, entry.start, next.start, '');
|
|
697
|
+
}
|
|
698
|
+
return splice(text, table.entries[index - 1].valueEnd, entry.valueEnd, '');
|
|
699
|
+
}
|
|
700
|
+
export function removeTomlTable(text, path) {
|
|
701
|
+
let current = text;
|
|
702
|
+
for (let pass = 0; pass < MAX_EDIT_PASSES; pass++) {
|
|
703
|
+
const document = lexToml(current);
|
|
704
|
+
const section = document.sections.find((candidate) => candidate.header !== null && startsWithPath(candidate.path, path));
|
|
705
|
+
if (section) {
|
|
706
|
+
current = removeLines(current, section.header.lineStart, section.lastEnd);
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
const entry = document.entries.find((candidate) => startsWithPath(candidate.path, path));
|
|
710
|
+
if (!entry) {
|
|
711
|
+
return current;
|
|
712
|
+
}
|
|
713
|
+
current = entry.inline
|
|
714
|
+
? removeInlineEntry(current, entry)
|
|
715
|
+
: removeLines(current, entry.lineStart, entry.end);
|
|
716
|
+
}
|
|
717
|
+
throw unsupported(`${renderKeyPath(path)} could not be removed`);
|
|
718
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-agent-toolkit/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.37",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Open Agent Toolkit CLI",
|
|
6
6
|
"homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"unified": "11.0.5",
|
|
42
42
|
"yaml": "2.8.2",
|
|
43
43
|
"zod": "^3.25.76",
|
|
44
|
-
"@open-agent-toolkit/control-plane": "0.3.
|
|
45
|
-
"@open-agent-toolkit/docs-transforms": "0.3.
|
|
44
|
+
"@open-agent-toolkit/control-plane": "0.3.37",
|
|
45
|
+
"@open-agent-toolkit/docs-transforms": "0.3.37"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/node": "^22.10.0",
|