@open-agent-toolkit/cli 0.3.35 → 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.
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "oatVersion": "0.3.35"
3
+ "oatVersion": "0.3.37"
4
4
  }
@@ -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`, an existing
49
- `pre-commit` hook and the other settings in an existing `.codex/config.toml`
50
- (OAT does change `multi_agent` and `max_depth` there; see the
51
- [provider table](#syncing-provider-views)). A pre-existing
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
@@ -320,10 +320,10 @@ Visibility reports source metadata: “user” is `user-invocable` and “model
320
320
 
321
321
  ### Project execution
322
322
 
323
- | Skill | Description | Visibility | Needs an active project? |
324
- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------ |
325
- | [`oat-project-implement`](../workflows/projects/execution/execution-skills.md#oat-project-implement) | Use when plan.md is ready for execution. Dispatches one phase implementer per phase, owns independent phase review and bounded fix routing, and supports plan-declared worktree-isolated parallel phases. | user: true; model disabled: true | Yes |
326
- | [`oat-project-reconcile`](../workflows/projects/execution/execution-skills.md#oat-project-reconcile) | Use when human-implemented commits need to be mapped back to planned tasks. Reconciles implementation.md and state.md after manual work outside the OAT workflow. | user: true; model disabled: true | Yes |
323
+ | Skill | Description | Visibility | Needs an active project? |
324
+ | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------- | ------------------------ |
325
+ | [`oat-project-implement`](../workflows/projects/execution/execution-skills.md#oat-project-implement) | Use when the user explicitly asks to implement an approved OAT plan — e.g. "implement the plan", "start implementation", or accepts a previously offered implementation step. Do NOT auto-invoke just because plan.md is ready. Dispatches one phase implementer per phase, owns independent phase review and bounded fix routing, and supports plan-declared worktree-isolated parallel phases. | user: true; model disabled: false | Yes |
326
+ | [`oat-project-reconcile`](../workflows/projects/execution/execution-skills.md#oat-project-reconcile) | Use when human-implemented commits need to be mapped back to planned tasks. Reconciles implementation.md and state.md after manual work outside the OAT workflow. | user: true; model disabled: true | Yes |
327
327
 
328
328
  ### Project planning
329
329
 
@@ -69,6 +69,11 @@ synced project. It is not proof that implementation or its tests have passed.
69
69
  The second begins execution. These are agent skill invocations, not `oat`
70
70
  terminal subcommands; use the invocation syntax your host supports.
71
71
 
72
+ The skill is model-invokable when you ask to implement an approved plan, or
73
+ when you accept an offered implementation step. A ready `plan.md` is not that
74
+ ask. The agent does not start implementation on its own just because the plan
75
+ is ready.
76
+
72
77
  The number of fix attempts after a failed review comes from
73
78
  `oat_orchestration_retry_limit` in the project's `state.md` (default 2, range
74
79
  0–5). The skill's argument hint also lists `--retry-limit`, but no step reads
@@ -1,6 +1,6 @@
1
1
  {
2
- "cli": "0.3.35",
3
- "docs-config": "0.3.35",
4
- "docs-theme": "0.3.35",
5
- "docs-transforms": "0.3.35"
2
+ "cli": "0.3.37",
3
+ "docs-config": "0.3.37",
4
+ "docs-theme": "0.3.37",
5
+ "docs-transforms": "0.3.37"
6
6
  }
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: oat-project-implement
3
- description: Use when plan.md is ready for execution. Dispatches one phase implementer per phase, owns independent phase review and bounded fix routing, and supports plan-declared worktree-isolated parallel phases.
3
+ description: Use when the user explicitly asks to implement an approved OAT plan — e.g. "implement the plan", "start implementation", or accepts a previously offered implementation step. Do NOT auto-invoke just because plan.md is ready. Dispatches one phase implementer per phase, owns independent phase review and bounded fix routing, and supports plan-declared worktree-isolated parallel phases.
4
4
  oat_gateable: true
5
5
  argument-hint: '[--retry-limit <N>] [--dry-run]'
6
- disable-model-invocation: true
6
+ disable-model-invocation: false
7
7
  user-invocable: true
8
8
  allowed-tools: Read, Write, Bash(git:*), Bash(oat:*), Bash(oat project log:*), Glob, Grep, AskUserQuestion, Task
9
9
  metadata:
10
- version: 2.3.21
10
+ version: 2.3.22
11
11
  ---
12
12
 
13
13
  # Implementation Phase
@@ -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
- /** Merges managed Codex roles and required shared agent configuration. */
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":"AAKA,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;AA8FD,4EAA4E;AAC5E,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAIvE;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,EAC/B,eAAe,EACf,YAAY,EACZ,iBAAsB,EACtB,iBAAiB,GAClB,EAAE,oBAAoB,GAAG,sBAAsB,CAgD/C;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,EACtC,eAAe,EACf,IAAI,EACJ,iBAAsB,EACtB,iBAAiB,GAClB,EAAE,8BAA8B,GAAG,sBAAsB,CAOzD"}
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 { stringifyToml } from './shared.js';
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
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
29
+ return isTable(value) ? { ...value } : {};
30
+ }
31
+ function requireTableOrAbsent(value, label) {
32
+ if (value === undefined) {
62
33
  return {};
63
34
  }
64
- return { ...value };
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
- /** Merges managed Codex roles and required shared agent configuration. */
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 = getObject(parsed.features);
79
- const agents = getObject(parsed.agents);
80
- const targetMaxDepth = getFiniteNumber(agents.max_depth);
81
- const validInheritedMaxDepth = getFiniteNumber(inheritedMaxDepth);
82
- agents.max_depth = Math.max(2, targetMaxDepth ?? 2, validInheritedMaxDepth ?? 2);
83
- const nextConfig = {
84
- ...parsed,
85
- features: {
86
- ...features,
87
- multi_agent: true,
88
- },
89
- agents,
90
- };
91
- for (const role of desiredRoles) {
92
- agents[role.roleName] = {
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 in agents) {
101
- delete agents[staleRole];
108
+ if (Object.hasOwn(expectedAgents, staleRole)) {
109
+ delete expectedAgents[staleRole];
102
110
  removedRoles.push(staleRole);
103
111
  }
104
112
  }
105
- const mergedContent = normalizeCodexConfigIndentation(stringifyToml(nextConfig));
106
- const changed = (existingContent ?? '').trimEnd() !== mergedContent.trimEnd();
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.35",
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.35",
45
- "@open-agent-toolkit/docs-transforms": "0.3.35"
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",