@agentdeck/hooks 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/codex-install.d.ts +64 -0
- package/dist/codex-install.d.ts.map +1 -0
- package/dist/codex-install.js +342 -0
- package/dist/codex-install.js.map +1 -0
- package/dist/codex-mini-toml.d.ts +22 -0
- package/dist/codex-mini-toml.d.ts.map +1 -0
- package/dist/codex-mini-toml.js +209 -0
- package/dist/codex-mini-toml.js.map +1 -0
- package/dist/install.d.ts +23 -3
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +89 -9
- package/dist/install.js.map +1 -1
- package/dist/opencode-install.d.ts +25 -0
- package/dist/opencode-install.d.ts.map +1 -0
- package/dist/opencode-install.js +345 -0
- package/dist/opencode-install.js.map +1 -0
- package/package.json +2 -7
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export declare const DEFAULT_CODEX_CONFIG_PATH: string;
|
|
2
|
+
/** Sidecar script for the Windows notify fallback. Codex execs `notify`
|
|
3
|
+
* as a bare argv array (no shell) and appends the JSON payload as the
|
|
4
|
+
* last element; `powershell -File` is the only Windows invocation that
|
|
5
|
+
* binds trailing argv into `$args` (`-Command` concatenates them into
|
|
6
|
+
* the command text and `-EncodedCommand` rejects them outright), and
|
|
7
|
+
* `-File` requires a real script on disk. */
|
|
8
|
+
export declare const DEFAULT_WINDOWS_NOTIFY_SCRIPT_PATH: string;
|
|
9
|
+
export interface InstallOptions {
|
|
10
|
+
/** Override the codex config path (tests + non-default homes). */
|
|
11
|
+
configPath?: string;
|
|
12
|
+
/** Daemon HTTP port to bake into the OTel exporter endpoint. If
|
|
13
|
+
* omitted, the installer reads `~/.agentdeck/daemon.json`. */
|
|
14
|
+
daemonHttpPort?: number;
|
|
15
|
+
/** Override platform for tests. Defaults to process.platform. */
|
|
16
|
+
platform?: NodeJS.Platform;
|
|
17
|
+
/** Override the Windows notify sidecar script path (tests). */
|
|
18
|
+
notifyScriptPath?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface InstallResult {
|
|
21
|
+
installed: boolean;
|
|
22
|
+
/** Human-readable reason when `installed === false`. */
|
|
23
|
+
reason?: string;
|
|
24
|
+
/** Non-fatal degradation while `installed === true` (e.g. the Windows
|
|
25
|
+
* notify sidecar could not be written, so notify was omitted). */
|
|
26
|
+
warning?: string;
|
|
27
|
+
}
|
|
28
|
+
interface ManagedBlockOptions {
|
|
29
|
+
includeNotify?: boolean;
|
|
30
|
+
includeOtel?: boolean;
|
|
31
|
+
otelEndpoint?: string;
|
|
32
|
+
daemonHttpPort?: number;
|
|
33
|
+
platform?: NodeJS.Platform;
|
|
34
|
+
notifyScriptPath?: string;
|
|
35
|
+
}
|
|
36
|
+
/** Assemble the body of the AgentDeck-managed fence. Tests call this
|
|
37
|
+
* directly to assert schema regressions without driving the file
|
|
38
|
+
* installer. */
|
|
39
|
+
export declare function managedBlockBody(opts?: ManagedBlockOptions): string;
|
|
40
|
+
/** Body of the Windows notify sidecar script (`-File` execution binds
|
|
41
|
+
* Codex's appended payload argv into `$args`; take the last element to
|
|
42
|
+
* mirror POSIX `$1`). ASCII-only so PowerShell 5.1's BOM-less ANSI
|
|
43
|
+
* fallback reads it identically to UTF-8. Exported for tests. */
|
|
44
|
+
export declare function windowsNotifyScriptContent(event?: string): string;
|
|
45
|
+
/** Install AgentDeck's Codex observation entries into `~/.codex/config.toml`
|
|
46
|
+
* unless the user opted out, the user has authored a conflicting top-level
|
|
47
|
+
* `[features]` / `[hooks]` table outside the fence, or the file write
|
|
48
|
+
* failed. Idempotent: re-running with the same daemon port produces a
|
|
49
|
+
* byte-identical file, so safe to call from setup, `agentdeck codex`,
|
|
50
|
+
* and `agentdeck daemon install`. */
|
|
51
|
+
export declare function installCodexHooksIfNeeded(opts?: InstallOptions): InstallResult;
|
|
52
|
+
/** Strip the AgentDeck-managed fence. Idempotent — no-op when the fence
|
|
53
|
+
* is absent. Does not delete the config file even if it becomes empty
|
|
54
|
+
* (Codex may rely on its existence). */
|
|
55
|
+
export declare function uninstallCodexHooks(opts?: {
|
|
56
|
+
configPath?: string;
|
|
57
|
+
notifyScriptPath?: string;
|
|
58
|
+
}): void;
|
|
59
|
+
/** Re-apply the managed block when the daemon port (or any other resolved
|
|
60
|
+
* field) might have changed. Equivalent to install but renamed for the
|
|
61
|
+
* daemon-restart code path so the intent is clearer at the call site. */
|
|
62
|
+
export declare function migrateCodexHooks(opts?: InstallOptions): InstallResult;
|
|
63
|
+
export {};
|
|
64
|
+
//# sourceMappingURL=codex-install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-install.d.ts","sourceRoot":"","sources":["../src/codex-install.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,yBAAyB,QAA2C,CAAC;AAClF;;;;;8CAK8C;AAC9C,eAAO,MAAM,kCAAkC,QAAoD,CAAC;AAGpG,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;mEAC+D;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,+DAA+D;IAC/D,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;uEACmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAgCD,UAAU,mBAAmB;IAC3B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;iBAEiB;AACjB,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,mBAAwB,GAAG,MAAM,CAsCvE;AAgHD;;;kEAGkE;AAClE,wBAAgB,0BAA0B,CAAC,KAAK,SAAwB,GAAG,MAAM,CAEhF;AAsED;;;;;sCAKsC;AACtC,wBAAgB,yBAAyB,CAAC,IAAI,GAAE,cAAmB,GAAG,aAAa,CAqDlF;AAED;;yCAEyC;AACzC,wBAAgB,mBAAmB,CAAC,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,IAAI,CASvG;AAED;;0EAE0E;AAC1E,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,cAAmB,GAAG,aAAa,CAE1E"}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// codex-install.ts — Install AgentDeck observation entries into
|
|
2
|
+
// ~/.codex/config.toml so Codex CLI sessions report turn boundaries and
|
|
3
|
+
// tool calls to the daemon via lifecycle hooks.
|
|
4
|
+
//
|
|
5
|
+
// Port of apple/AgentDeck/Daemon/Core/CodexConfigInstaller.swift, with
|
|
6
|
+
// the macOS-specific consent flow (NSAlert / NSOpenPanel / security-
|
|
7
|
+
// scoped bookmarks) stripped — Node CLI runs without sandboxing, so
|
|
8
|
+
// invocation is implicit consent. An opt-out env var
|
|
9
|
+
// `AGENTDECK_NO_CODEX_HOOKS=1` (or the `--no-codex-hooks` CLI flag) is
|
|
10
|
+
// honoured by callers.
|
|
11
|
+
//
|
|
12
|
+
// The fence sentinels and PORT-resolution shell are byte-identical with
|
|
13
|
+
// the Swift installer, so a config installed by either side can be
|
|
14
|
+
// migrated / uninstalled by the other without conflict.
|
|
15
|
+
//
|
|
16
|
+
// MUST stay in sync with apple/AgentDeck/Daemon/Core/CodexConfigInstaller.swift
|
|
17
|
+
// (managedBlockBody schema + lifecycle hook table layout).
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from 'fs';
|
|
19
|
+
import { dirname, join } from 'path';
|
|
20
|
+
import { homedir } from 'os';
|
|
21
|
+
import { applyManagedBlock, removeManagedBlock, hasTopLevelKeyOutsideFence, hasTableOutsideFence, quoted, } from './codex-mini-toml.js';
|
|
22
|
+
export const DEFAULT_CODEX_CONFIG_PATH = join(homedir(), '.codex', 'config.toml');
|
|
23
|
+
/** Sidecar script for the Windows notify fallback. Codex execs `notify`
|
|
24
|
+
* as a bare argv array (no shell) and appends the JSON payload as the
|
|
25
|
+
* last element; `powershell -File` is the only Windows invocation that
|
|
26
|
+
* binds trailing argv into `$args` (`-Command` concatenates them into
|
|
27
|
+
* the command text and `-EncodedCommand` rejects them outright), and
|
|
28
|
+
* `-File` requires a real script on disk. */
|
|
29
|
+
export const DEFAULT_WINDOWS_NOTIFY_SCRIPT_PATH = join(homedir(), '.agentdeck', 'codex-notify.ps1');
|
|
30
|
+
const DEFAULT_DAEMON_PORT = 9120;
|
|
31
|
+
/** Honour `AGENTDECK_NO_CODEX_HOOKS=1` from the environment. Callers
|
|
32
|
+
* should also accept a `--no-codex-hooks` flag and short-circuit before
|
|
33
|
+
* calling install. */
|
|
34
|
+
function envOptOut() {
|
|
35
|
+
return process.env.AGENTDECK_NO_CODEX_HOOKS === '1';
|
|
36
|
+
}
|
|
37
|
+
/** Read `~/.agentdeck/daemon.json` and return the daemon's HTTP port,
|
|
38
|
+
* preferring `httpPort` over `port`. The Apple build splits HTTP and
|
|
39
|
+
* WebSocket across different ports; the Node CLI uses a single port,
|
|
40
|
+
* so this still resolves correctly. Returns null if the file is
|
|
41
|
+
* missing or malformed. */
|
|
42
|
+
function currentDaemonHttpPort() {
|
|
43
|
+
const path = join(homedir(), '.agentdeck', 'daemon.json');
|
|
44
|
+
if (!existsSync(path))
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
const obj = JSON.parse(readFileSync(path, 'utf-8'));
|
|
48
|
+
if (typeof obj.httpPort === 'number' && obj.httpPort > 0)
|
|
49
|
+
return obj.httpPort;
|
|
50
|
+
if (typeof obj.port === 'number' && obj.port > 0)
|
|
51
|
+
return obj.port;
|
|
52
|
+
}
|
|
53
|
+
catch { /* malformed JSON — fall through */ }
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function buildOtelEndpoint(daemonHttpPort) {
|
|
57
|
+
const port = daemonHttpPort ?? currentDaemonHttpPort() ?? DEFAULT_DAEMON_PORT;
|
|
58
|
+
return `http://127.0.0.1:${port}/otel/v1/traces`;
|
|
59
|
+
}
|
|
60
|
+
/** Assemble the body of the AgentDeck-managed fence. Tests call this
|
|
61
|
+
* directly to assert schema regressions without driving the file
|
|
62
|
+
* installer. */
|
|
63
|
+
export function managedBlockBody(opts = {}) {
|
|
64
|
+
const includeNotify = opts.includeNotify ?? true;
|
|
65
|
+
const includeOtel = opts.includeOtel ?? true;
|
|
66
|
+
const platform = opts.platform ?? process.platform;
|
|
67
|
+
const lines = [
|
|
68
|
+
'# Codex lifecycle hooks. Command hooks receive JSON on stdin;',
|
|
69
|
+
'# each snippet forwards that stdin body unchanged to AgentDeck.',
|
|
70
|
+
'[features]',
|
|
71
|
+
'hooks = true',
|
|
72
|
+
];
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(...buildLifecycleHookTables(platform));
|
|
75
|
+
if (includeNotify) {
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push('# Optional turn-complete notification fallback.');
|
|
78
|
+
if (platform === 'win32') {
|
|
79
|
+
lines.push('# Codex appends the JSON payload as the last argv entry;');
|
|
80
|
+
lines.push('# powershell -File binds it into $args. (-Command cannot:');
|
|
81
|
+
lines.push('# it concatenates trailing argv into the command text.)');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
lines.push('# Codex appends the JSON payload as the last argv entry,');
|
|
85
|
+
lines.push('# so the 4th array element acts as $0 and payload lands at $1.');
|
|
86
|
+
}
|
|
87
|
+
lines.push(buildNotifyAssignment('codex_turn_complete', platform, opts.notifyScriptPath));
|
|
88
|
+
}
|
|
89
|
+
if (includeOtel) {
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('# OTel trace exporter — best-effort live progress signal.');
|
|
92
|
+
lines.push('# Schema: [otel.trace_exporter.otlp-http].');
|
|
93
|
+
lines.push('[otel.trace_exporter.otlp-http]');
|
|
94
|
+
lines.push(`endpoint = ${quoted(opts.otelEndpoint ?? buildOtelEndpoint(opts.daemonHttpPort))}`);
|
|
95
|
+
lines.push('protocol = "json"');
|
|
96
|
+
}
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
/** `notify = ["sh", "-c", "<snippet>", "agentdeck-notify"]`. Two design
|
|
100
|
+
* choices stacked here:
|
|
101
|
+
* 1. `"sh"` uses PATH lookup so no absolute shell path lands in the
|
|
102
|
+
* installer config.
|
|
103
|
+
* 2. The trailing `"agentdeck-notify"` is a dummy `$0`. Codex invokes
|
|
104
|
+
* `notify` by appending the JSON payload as the last argv entry.
|
|
105
|
+
* Without our 4th element, `sh -c "<snippet>" <json>` would assign
|
|
106
|
+
* `<json>` to `$0` and leave `$1` empty. With the dummy in place,
|
|
107
|
+
* `<json>` lands at `$1` as the snippet expects. */
|
|
108
|
+
function buildNotifyAssignment(event, platform, notifyScriptPath) {
|
|
109
|
+
if (platform === 'win32') {
|
|
110
|
+
const scriptPath = notifyScriptPath ?? DEFAULT_WINDOWS_NOTIFY_SCRIPT_PATH;
|
|
111
|
+
return `notify = ["powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", ${quoted(scriptPath)}]`;
|
|
112
|
+
}
|
|
113
|
+
return `notify = ["sh", "-c", ${quoted(buildNotifySnippet(event))}, "agentdeck-notify"]`;
|
|
114
|
+
}
|
|
115
|
+
/** Codex notify snippet. PORT-resolution lines are byte-identical with
|
|
116
|
+
* `hooks/src/install.ts:buildHookCommand` so the two integrations share
|
|
117
|
+
* one canonical port-discovery contract. Only the trailing curl line
|
|
118
|
+
* differs: Claude hooks pipe stdin (`-d @-`); Codex notify hands the
|
|
119
|
+
* JSON payload as `$1` (`--data-raw "$1"`). */
|
|
120
|
+
function buildNotifySnippet(event) {
|
|
121
|
+
return [
|
|
122
|
+
`PORT="\${AGENTDECK_PORT:-}"`,
|
|
123
|
+
`if [ -z "$PORT" ]; then`,
|
|
124
|
+
` for F in "$HOME/.agentdeck/daemon.json" "$HOME/Library/Containers/bound.serendipity.agent.deck/Data/Library/Application Support/AgentDeck/daemon.json" "$HOME/Library/Group Containers/group.bound.serendipity.agent.deck/daemon.json"; do`,
|
|
125
|
+
` [ -f "$F" ] || continue`,
|
|
126
|
+
` P=$(python3 -c "import json;d=json.load(open('$F'));print(d.get('httpPort') or d.get('port',''))" 2>/dev/null)`,
|
|
127
|
+
` [ -n "$P" ] && curl -sf --max-time 0.3 "http://127.0.0.1:$P/health" >/dev/null 2>&1 && { PORT="$P"; break; }`,
|
|
128
|
+
` done`,
|
|
129
|
+
`fi`,
|
|
130
|
+
`PORT="\${PORT:-9120}"`,
|
|
131
|
+
`curl -sf --connect-timeout 0.2 --max-time 0.8 -X POST "http://127.0.0.1:$PORT/hooks/${event}" -H 'Content-Type: application/json' --data-raw "$1" 2>/dev/null || true`,
|
|
132
|
+
].join('\n');
|
|
133
|
+
}
|
|
134
|
+
const LIFECYCLE_HOOKS = [
|
|
135
|
+
{ codexEvent: 'SessionStart', agentDeckEvent: 'codex_session_start', matcher: 'startup|resume|clear' },
|
|
136
|
+
{ codexEvent: 'UserPromptSubmit', agentDeckEvent: 'codex_user_prompt_submit', matcher: null },
|
|
137
|
+
{ codexEvent: 'PreToolUse', agentDeckEvent: 'codex_tool_start', matcher: '*' },
|
|
138
|
+
{ codexEvent: 'PostToolUse', agentDeckEvent: 'codex_tool_end', matcher: '*' },
|
|
139
|
+
{ codexEvent: 'Stop', agentDeckEvent: 'codex_stop', matcher: null },
|
|
140
|
+
];
|
|
141
|
+
function buildLifecycleHookTables(platform) {
|
|
142
|
+
const lines = [];
|
|
143
|
+
for (let idx = 0; idx < LIFECYCLE_HOOKS.length; idx++) {
|
|
144
|
+
const hook = LIFECYCLE_HOOKS[idx];
|
|
145
|
+
if (idx > 0)
|
|
146
|
+
lines.push('');
|
|
147
|
+
lines.push(`[[hooks.${hook.codexEvent}]]`);
|
|
148
|
+
if (hook.matcher !== null) {
|
|
149
|
+
lines.push(`matcher = ${quoted(hook.matcher)}`);
|
|
150
|
+
}
|
|
151
|
+
lines.push(`[[hooks.${hook.codexEvent}.hooks]]`);
|
|
152
|
+
lines.push(`type = "command"`);
|
|
153
|
+
lines.push(`command = ${quoted(buildLifecycleHookCommand(hook.agentDeckEvent, platform))}`);
|
|
154
|
+
lines.push(`timeout = 5`);
|
|
155
|
+
}
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
158
|
+
/** Official Codex lifecycle hooks pass their JSON payload on stdin.
|
|
159
|
+
* Keep stdout quiet so Stop / UserPromptSubmit hooks do not accidentally
|
|
160
|
+
* feed the daemon's acknowledgement back into Codex as hook output. */
|
|
161
|
+
function buildLifecycleHookCommand(event, platform) {
|
|
162
|
+
if (platform === 'win32') {
|
|
163
|
+
return buildWindowsLifecycleHookCommand(event);
|
|
164
|
+
}
|
|
165
|
+
return `sh -c ${shellSingleQuoted(buildStdinPostSnippet(event))}`;
|
|
166
|
+
}
|
|
167
|
+
function buildStdinPostSnippet(event) {
|
|
168
|
+
return [
|
|
169
|
+
`PORT="\${AGENTDECK_PORT:-}"`,
|
|
170
|
+
`if [ -z "$PORT" ]; then`,
|
|
171
|
+
` for F in "$HOME/.agentdeck/daemon.json" "$HOME/Library/Containers/bound.serendipity.agent.deck/Data/Library/Application Support/AgentDeck/daemon.json" "$HOME/Library/Group Containers/group.bound.serendipity.agent.deck/daemon.json"; do`,
|
|
172
|
+
` [ -f "$F" ] || continue`,
|
|
173
|
+
` P=$(python3 -c "import json;d=json.load(open('$F'));print(d.get('httpPort') or d.get('port',''))" 2>/dev/null)`,
|
|
174
|
+
` [ -n "$P" ] && curl -sf --max-time 0.3 "http://127.0.0.1:$P/health" >/dev/null 2>&1 && { PORT="$P"; break; }`,
|
|
175
|
+
` done`,
|
|
176
|
+
`fi`,
|
|
177
|
+
`PORT="\${PORT:-9120}"`,
|
|
178
|
+
`curl -sf --connect-timeout 0.2 --max-time 0.8 -X POST "http://127.0.0.1:$PORT/hooks/${event}" -H 'Content-Type: application/json' -d @- >/dev/null 2>&1 || true`,
|
|
179
|
+
].join('\n');
|
|
180
|
+
}
|
|
181
|
+
function shellSingleQuoted(s) {
|
|
182
|
+
return `'${s.replace(/'/g, "'\"'\"'")}'`;
|
|
183
|
+
}
|
|
184
|
+
function buildWindowsLifecycleHookCommand(event) {
|
|
185
|
+
return `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${windowsEncodedCommand(buildWindowsStdinPostSnippet(event))}`;
|
|
186
|
+
}
|
|
187
|
+
function buildWindowsStdinPostSnippet(event) {
|
|
188
|
+
// Read raw stdin as UTF-8: [Console]::In decodes with the console's OEM
|
|
189
|
+
// codepage (e.g. CP949) and would garble non-ASCII payload text.
|
|
190
|
+
return buildWindowsPostSnippet(event, '(New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)).ReadToEnd()');
|
|
191
|
+
}
|
|
192
|
+
/** Body of the Windows notify sidecar script (`-File` execution binds
|
|
193
|
+
* Codex's appended payload argv into `$args`; take the last element to
|
|
194
|
+
* mirror POSIX `$1`). ASCII-only so PowerShell 5.1's BOM-less ANSI
|
|
195
|
+
* fallback reads it identically to UTF-8. Exported for tests. */
|
|
196
|
+
export function windowsNotifyScriptContent(event = 'codex_turn_complete') {
|
|
197
|
+
return buildWindowsPostSnippet(event, `$(if ($args.Count -gt 0) { [string]$args[$args.Count - 1] } else { '' })`) + '\n';
|
|
198
|
+
}
|
|
199
|
+
function buildWindowsPostSnippet(event, bodyExpression) {
|
|
200
|
+
return [
|
|
201
|
+
`$ErrorActionPreference = 'SilentlyContinue'`,
|
|
202
|
+
`$ProgressPreference = 'SilentlyContinue'`,
|
|
203
|
+
`$port = $env:AGENTDECK_PORT`,
|
|
204
|
+
`if ([string]::IsNullOrWhiteSpace($port)) {`,
|
|
205
|
+
` $daemonFile = Join-Path $env:USERPROFILE '.agentdeck\\daemon.json'`,
|
|
206
|
+
` if (Test-Path -LiteralPath $daemonFile) {`,
|
|
207
|
+
` try {`,
|
|
208
|
+
` $daemon = Get-Content -LiteralPath $daemonFile -Raw | ConvertFrom-Json`,
|
|
209
|
+
` $candidate = if ($daemon.httpPort) { $daemon.httpPort } else { $daemon.port }`,
|
|
210
|
+
` if ($candidate) {`,
|
|
211
|
+
` try { Invoke-WebRequest -UseBasicParsing -TimeoutSec 1 -Uri ('http://127.0.0.1:' + $candidate + '/health') | Out-Null; $port = [string]$candidate } catch {}`,
|
|
212
|
+
` }`,
|
|
213
|
+
` } catch {}`,
|
|
214
|
+
` }`,
|
|
215
|
+
`}`,
|
|
216
|
+
`if ([string]::IsNullOrWhiteSpace($port)) { $port = '9120' }`,
|
|
217
|
+
`$body = ${bodyExpression}`,
|
|
218
|
+
// Post UTF-8 bytes: Invoke-RestMethod encodes string bodies as
|
|
219
|
+
// ISO-8859-1 when the content type carries no charset, replacing
|
|
220
|
+
// non-ASCII payload characters with '?'.
|
|
221
|
+
`$bytes = [System.Text.Encoding]::UTF8.GetBytes([string]$body)`,
|
|
222
|
+
`try { Invoke-RestMethod -Method Post -TimeoutSec 1 -Uri ('http://127.0.0.1:' + $port + '/hooks/${event}') -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null } catch {}`,
|
|
223
|
+
`exit 0`,
|
|
224
|
+
].join('\n');
|
|
225
|
+
}
|
|
226
|
+
function windowsEncodedCommand(s) {
|
|
227
|
+
return Buffer.from(s, 'utf16le').toString('base64');
|
|
228
|
+
}
|
|
229
|
+
// ─── File I/O ───────────────────────────────────────────────────────────
|
|
230
|
+
function readText(path) {
|
|
231
|
+
if (!existsSync(path))
|
|
232
|
+
return '';
|
|
233
|
+
try {
|
|
234
|
+
return readFileSync(path, 'utf-8');
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return '';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** Write the notify sidecar only when its content changed, so repeated
|
|
241
|
+
* installs stay idempotent (no mtime churn). Returns false when the
|
|
242
|
+
* script cannot be guaranteed on disk. */
|
|
243
|
+
function writeScriptIfChanged(content, path) {
|
|
244
|
+
try {
|
|
245
|
+
if (existsSync(path) && readFileSync(path, 'utf-8') === content)
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch { /* unreadable — fall through to rewrite */ }
|
|
249
|
+
return writeTextAtomic(content, path);
|
|
250
|
+
}
|
|
251
|
+
function writeTextAtomic(text, path) {
|
|
252
|
+
const dir = dirname(path);
|
|
253
|
+
try {
|
|
254
|
+
if (!existsSync(dir))
|
|
255
|
+
mkdirSync(dir, { recursive: true });
|
|
256
|
+
const tmp = `${path}.agentdeck.tmp`;
|
|
257
|
+
writeFileSync(tmp, text, 'utf-8');
|
|
258
|
+
renameSync(tmp, path);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ─── Public install / uninstall / migrate ──────────────────────────────
|
|
266
|
+
/** Install AgentDeck's Codex observation entries into `~/.codex/config.toml`
|
|
267
|
+
* unless the user opted out, the user has authored a conflicting top-level
|
|
268
|
+
* `[features]` / `[hooks]` table outside the fence, or the file write
|
|
269
|
+
* failed. Idempotent: re-running with the same daemon port produces a
|
|
270
|
+
* byte-identical file, so safe to call from setup, `agentdeck codex`,
|
|
271
|
+
* and `agentdeck daemon install`. */
|
|
272
|
+
export function installCodexHooksIfNeeded(opts = {}) {
|
|
273
|
+
if (envOptOut())
|
|
274
|
+
return { installed: false, reason: 'AGENTDECK_NO_CODEX_HOOKS=1' };
|
|
275
|
+
const path = opts.configPath ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
276
|
+
const original = readText(path);
|
|
277
|
+
// Refuse to clobber user-authored lifecycle hook config. This line-mode
|
|
278
|
+
// editor cannot safely merge existing `[features]` or `[hooks]` tables
|
|
279
|
+
// without a real TOML parser, and duplicate tables would make Codex
|
|
280
|
+
// reject config.toml.
|
|
281
|
+
if (hasTableOutsideFence(original, 'features')) {
|
|
282
|
+
return { installed: false, reason: 'user-authored [features] present' };
|
|
283
|
+
}
|
|
284
|
+
if (hasTableOutsideFence(original, 'hooks')) {
|
|
285
|
+
return { installed: false, reason: 'user-authored [hooks] present' };
|
|
286
|
+
}
|
|
287
|
+
const platform = opts.platform ?? process.platform;
|
|
288
|
+
let includeNotify = !hasTopLevelKeyOutsideFence(original, 'notify');
|
|
289
|
+
// OTel exporter stays POSIX-only for now — deliberately omitted on
|
|
290
|
+
// win32 (unverified there); lifecycle hooks + notify carry the signal.
|
|
291
|
+
const includeOtel = platform !== 'win32' && !hasTableOutsideFence(original, 'otel');
|
|
292
|
+
let warning;
|
|
293
|
+
const notifyScriptPath = opts.notifyScriptPath ?? DEFAULT_WINDOWS_NOTIFY_SCRIPT_PATH;
|
|
294
|
+
if (includeNotify && platform === 'win32') {
|
|
295
|
+
// The Stop lifecycle hook already reports turn-complete, so a failed
|
|
296
|
+
// sidecar write downgrades to lifecycle-only rather than aborting.
|
|
297
|
+
if (!writeScriptIfChanged(windowsNotifyScriptContent(), notifyScriptPath)) {
|
|
298
|
+
includeNotify = false;
|
|
299
|
+
warning = `notify sidecar write failed (${notifyScriptPath}) — notify fallback omitted`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const otelEndpoint = includeOtel ? buildOtelEndpoint(opts.daemonHttpPort) : undefined;
|
|
303
|
+
const body = managedBlockBody({
|
|
304
|
+
includeNotify,
|
|
305
|
+
includeOtel,
|
|
306
|
+
otelEndpoint,
|
|
307
|
+
daemonHttpPort: opts.daemonHttpPort,
|
|
308
|
+
platform,
|
|
309
|
+
notifyScriptPath,
|
|
310
|
+
});
|
|
311
|
+
const updated = applyManagedBlock(original, body);
|
|
312
|
+
if (updated === original) {
|
|
313
|
+
return { installed: true, warning };
|
|
314
|
+
}
|
|
315
|
+
if (writeTextAtomic(updated, path)) {
|
|
316
|
+
return { installed: true, warning };
|
|
317
|
+
}
|
|
318
|
+
return { installed: false, reason: `write failed: ${path}` };
|
|
319
|
+
}
|
|
320
|
+
/** Strip the AgentDeck-managed fence. Idempotent — no-op when the fence
|
|
321
|
+
* is absent. Does not delete the config file even if it becomes empty
|
|
322
|
+
* (Codex may rely on its existence). */
|
|
323
|
+
export function uninstallCodexHooks(opts = {}) {
|
|
324
|
+
try {
|
|
325
|
+
unlinkSync(opts.notifyScriptPath ?? DEFAULT_WINDOWS_NOTIFY_SCRIPT_PATH);
|
|
326
|
+
}
|
|
327
|
+
catch { /* absent on POSIX installs — nothing to remove */ }
|
|
328
|
+
const path = opts.configPath ?? DEFAULT_CODEX_CONFIG_PATH;
|
|
329
|
+
if (!existsSync(path))
|
|
330
|
+
return;
|
|
331
|
+
const original = readText(path);
|
|
332
|
+
const stripped = removeManagedBlock(original);
|
|
333
|
+
if (stripped !== original)
|
|
334
|
+
writeTextAtomic(stripped, path);
|
|
335
|
+
}
|
|
336
|
+
/** Re-apply the managed block when the daemon port (or any other resolved
|
|
337
|
+
* field) might have changed. Equivalent to install but renamed for the
|
|
338
|
+
* daemon-restart code path so the intent is clearer at the call site. */
|
|
339
|
+
export function migrateCodexHooks(opts = {}) {
|
|
340
|
+
return installCodexHooksIfNeeded(opts);
|
|
341
|
+
}
|
|
342
|
+
//# sourceMappingURL=codex-install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-install.js","sourceRoot":"","sources":["../src/codex-install.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const OPEN_FENCE = "# >>> AgentDeck managed (do not edit) <<<";
|
|
2
|
+
export declare const CLOSE_FENCE = "# <<< AgentDeck managed (do not edit) >>>";
|
|
3
|
+
/** Replace the AgentDeck-managed fenced block (or append one when none
|
|
4
|
+
* exists). The body is wrapped between OPEN_FENCE / CLOSE_FENCE so
|
|
5
|
+
* removeManagedBlock can strip it cleanly later. Returns the full
|
|
6
|
+
* updated TOML text. */
|
|
7
|
+
export declare function applyManagedBlock(text: string, body: string): string;
|
|
8
|
+
/** Strip the AgentDeck-managed block entirely. Idempotent — no-op when
|
|
9
|
+
* the fence is absent. */
|
|
10
|
+
export declare function removeManagedBlock(text: string): string;
|
|
11
|
+
/** Detect a top-level `<key> = ...` definition outside the fence. Codex
|
|
12
|
+
* `notify` is a top-level key; if the user already wrote one our fenced
|
|
13
|
+
* `notify` would be a duplicate-key TOML error. */
|
|
14
|
+
export declare function hasTopLevelKeyOutsideFence(text: string, key: string): boolean;
|
|
15
|
+
/** Detect a `[<table>]`, `[<table>.subkey]`, or matching array-of-table
|
|
16
|
+
* header outside the fence. Codex `[otel]` / `[features]` / `[hooks]`
|
|
17
|
+
* tables collide with the fence we'd write. */
|
|
18
|
+
export declare function hasTableOutsideFence(text: string, table: string): boolean;
|
|
19
|
+
/** Quote a string as a TOML basic string. Escape backslash, double quote,
|
|
20
|
+
* and control characters so the output is always single-line safe. */
|
|
21
|
+
export declare function quoted(s: string): string;
|
|
22
|
+
//# sourceMappingURL=codex-mini-toml.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-mini-toml.d.ts","sourceRoot":"","sources":["../src/codex-mini-toml.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,UAAU,8CAA8C,CAAC;AACtE,eAAO,MAAM,WAAW,8CAA8C,CAAC;AAEvE;;;yBAGyB;AACzB,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAwBpE;AAED;2BAC2B;AAC3B,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAcvD;AAED;;oDAEoD;AACpD,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAkB7E;AAED;;gDAEgD;AAChD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAczE;AAED;uEACuE;AACvE,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAcxC"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// codex-mini-toml.ts — minimal lossless TOML editor for ~/.codex/config.toml.
|
|
2
|
+
//
|
|
3
|
+
// Direct port of apple/AgentDeck/Daemon/Core/MiniToml.swift. Must stay
|
|
4
|
+
// byte-compatible: the fence sentinels are shared between the App Store
|
|
5
|
+
// Swift daemon and the Node CLI bridge so a config installed by either
|
|
6
|
+
// side can be uninstalled / re-applied by the other.
|
|
7
|
+
//
|
|
8
|
+
// We deliberately do NOT parse TOML semantically. Codex configs contain
|
|
9
|
+
// user-authored keys, comments, profile tables, and MCP server tables that
|
|
10
|
+
// we have no business round-tripping through a semantic serializer.
|
|
11
|
+
// AgentDeck-managed entries live inside a fenced block:
|
|
12
|
+
//
|
|
13
|
+
// # >>> AgentDeck managed (do not edit) <<<
|
|
14
|
+
// <our keys>
|
|
15
|
+
// # <<< AgentDeck managed (do not edit) >>>
|
|
16
|
+
//
|
|
17
|
+
// applyManagedBlock replaces (or appends) the fence; removeManagedBlock
|
|
18
|
+
// strips it. Everything outside the fence is preserved byte-for-byte.
|
|
19
|
+
export const OPEN_FENCE = '# >>> AgentDeck managed (do not edit) <<<';
|
|
20
|
+
export const CLOSE_FENCE = '# <<< AgentDeck managed (do not edit) >>>';
|
|
21
|
+
/** Replace the AgentDeck-managed fenced block (or append one when none
|
|
22
|
+
* exists). The body is wrapped between OPEN_FENCE / CLOSE_FENCE so
|
|
23
|
+
* removeManagedBlock can strip it cleanly later. Returns the full
|
|
24
|
+
* updated TOML text. */
|
|
25
|
+
export function applyManagedBlock(text, body) {
|
|
26
|
+
const lines = splitLines(text);
|
|
27
|
+
const fenceRange = locateFence(lines);
|
|
28
|
+
const bodyLines = body.length === 0 ? [] : splitLines(body);
|
|
29
|
+
let replacement = [OPEN_FENCE, ...bodyLines, CLOSE_FENCE];
|
|
30
|
+
if (fenceRange) {
|
|
31
|
+
const preservedHookState = extractCodexHookState(lines.slice(fenceRange.start + 1, fenceRange.end - 1));
|
|
32
|
+
if (preservedHookState.length > 0) {
|
|
33
|
+
replacement = [...replacement, '', ...preservedHookState];
|
|
34
|
+
}
|
|
35
|
+
lines.splice(fenceRange.start, fenceRange.end - fenceRange.start, ...replacement);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
// Pad with a blank line for readability when appending to a non-empty
|
|
39
|
+
// file. Avoids glueing our fence onto the user's last key.
|
|
40
|
+
if (lines.length > 0 && lines[lines.length - 1] !== '') {
|
|
41
|
+
lines.push('');
|
|
42
|
+
}
|
|
43
|
+
lines.push(...replacement);
|
|
44
|
+
}
|
|
45
|
+
return lines.join('\n');
|
|
46
|
+
}
|
|
47
|
+
/** Strip the AgentDeck-managed block entirely. Idempotent — no-op when
|
|
48
|
+
* the fence is absent. */
|
|
49
|
+
export function removeManagedBlock(text) {
|
|
50
|
+
const lines = splitLines(text);
|
|
51
|
+
const fenceRange = locateFence(lines);
|
|
52
|
+
if (!fenceRange)
|
|
53
|
+
return text;
|
|
54
|
+
lines.splice(fenceRange.start, fenceRange.end - fenceRange.start);
|
|
55
|
+
// Collapse a trailing blank line that we may have inserted in
|
|
56
|
+
// applyManagedBlock so removeManagedBlock truly returns the file to
|
|
57
|
+
// its pre-apply shape.
|
|
58
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') {
|
|
59
|
+
lines.pop();
|
|
60
|
+
}
|
|
61
|
+
// Restore a single trailing newline if the original ended with one.
|
|
62
|
+
if (text.endsWith('\n'))
|
|
63
|
+
lines.push('');
|
|
64
|
+
return lines.join('\n');
|
|
65
|
+
}
|
|
66
|
+
/** Detect a top-level `<key> = ...` definition outside the fence. Codex
|
|
67
|
+
* `notify` is a top-level key; if the user already wrote one our fenced
|
|
68
|
+
* `notify` would be a duplicate-key TOML error. */
|
|
69
|
+
export function hasTopLevelKeyOutsideFence(text, key) {
|
|
70
|
+
const escaped = escapeRegex(key);
|
|
71
|
+
const regex = new RegExp(`^\\s*${escaped}\\s*=`);
|
|
72
|
+
let insideFence = false;
|
|
73
|
+
let insideTable = false;
|
|
74
|
+
for (const line of splitLines(text)) {
|
|
75
|
+
if (line === OPEN_FENCE) {
|
|
76
|
+
insideFence = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (line === CLOSE_FENCE) {
|
|
80
|
+
insideFence = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (insideFence)
|
|
84
|
+
continue;
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
87
|
+
insideTable = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (insideTable)
|
|
91
|
+
continue;
|
|
92
|
+
if (regex.test(line))
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
/** Detect a `[<table>]`, `[<table>.subkey]`, or matching array-of-table
|
|
98
|
+
* header outside the fence. Codex `[otel]` / `[features]` / `[hooks]`
|
|
99
|
+
* tables collide with the fence we'd write. */
|
|
100
|
+
export function hasTableOutsideFence(text, table) {
|
|
101
|
+
const escaped = escapeRegex(table);
|
|
102
|
+
// Match exactly `[otel]`, `[otel.something]`, `[[otel.something]]`,
|
|
103
|
+
// but not `[otelfoo]`. Whitespace inside brackets is permissive.
|
|
104
|
+
const regex = new RegExp(`^\\s*\\[\\[?\\s*${escaped}(\\.[A-Za-z0-9_\\-]+)*\\s*\\]\\]?\\s*$`);
|
|
105
|
+
let insideFence = false;
|
|
106
|
+
for (const line of splitLines(text)) {
|
|
107
|
+
if (line === OPEN_FENCE) {
|
|
108
|
+
insideFence = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (line === CLOSE_FENCE) {
|
|
112
|
+
insideFence = false;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (insideFence)
|
|
116
|
+
continue;
|
|
117
|
+
if (table === 'hooks' && isCodexHookStateHeader(line))
|
|
118
|
+
continue;
|
|
119
|
+
if (regex.test(line))
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
/** Quote a string as a TOML basic string. Escape backslash, double quote,
|
|
125
|
+
* and control characters so the output is always single-line safe. */
|
|
126
|
+
export function quoted(s) {
|
|
127
|
+
let out = '"';
|
|
128
|
+
for (const ch of s) {
|
|
129
|
+
const cp = ch.codePointAt(0);
|
|
130
|
+
if (ch === '\\')
|
|
131
|
+
out += '\\\\';
|
|
132
|
+
else if (ch === '"')
|
|
133
|
+
out += '\\"';
|
|
134
|
+
else if (ch === '\n')
|
|
135
|
+
out += '\\n';
|
|
136
|
+
else if (ch === '\r')
|
|
137
|
+
out += '\\r';
|
|
138
|
+
else if (ch === '\t')
|
|
139
|
+
out += '\\t';
|
|
140
|
+
else if (cp < 0x20)
|
|
141
|
+
out += `\\u${cp.toString(16).padStart(4, '0')}`;
|
|
142
|
+
else
|
|
143
|
+
out += ch;
|
|
144
|
+
}
|
|
145
|
+
out += '"';
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
// ─── internals ──────────────────────────────────────────────────────────
|
|
149
|
+
function splitLines(text) {
|
|
150
|
+
// String.split('\n') keeps trailing-empty so an input ending in "\n"
|
|
151
|
+
// round-trips cleanly when re-joined with "\n".
|
|
152
|
+
return text.split('\n');
|
|
153
|
+
}
|
|
154
|
+
function locateFence(lines) {
|
|
155
|
+
const start = lines.indexOf(OPEN_FENCE);
|
|
156
|
+
if (start === -1)
|
|
157
|
+
return null;
|
|
158
|
+
// Find first close fence at-or-after start. Defensive against truncated
|
|
159
|
+
// files: if no close fence is found, treat everything from the open
|
|
160
|
+
// fence to the end as managed.
|
|
161
|
+
let end = -1;
|
|
162
|
+
for (let i = start; i < lines.length; i++) {
|
|
163
|
+
if (lines[i] === CLOSE_FENCE) {
|
|
164
|
+
end = i;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (end === -1)
|
|
169
|
+
end = lines.length - 1;
|
|
170
|
+
return { start, end: end + 1 };
|
|
171
|
+
}
|
|
172
|
+
function escapeRegex(s) {
|
|
173
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
174
|
+
}
|
|
175
|
+
function extractCodexHookState(lines) {
|
|
176
|
+
const out = [];
|
|
177
|
+
let capturing = false;
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
const tableHeader = isTableHeader(line);
|
|
180
|
+
if (isCodexHookStateHeader(line)) {
|
|
181
|
+
capturing = true;
|
|
182
|
+
out.push(line);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (capturing && tableHeader) {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
if (capturing) {
|
|
189
|
+
out.push(line);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
while (out.length > 0 && isTrailingNonDataLine(out[out.length - 1])) {
|
|
193
|
+
out.pop();
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
function isCodexHookStateHeader(line) {
|
|
198
|
+
const trimmed = line.trim();
|
|
199
|
+
return trimmed === '[hooks.state]' || (trimmed.startsWith('[hooks.state.') && trimmed.endsWith(']'));
|
|
200
|
+
}
|
|
201
|
+
function isTableHeader(line) {
|
|
202
|
+
const trimmed = line.trim();
|
|
203
|
+
return trimmed.startsWith('[') && trimmed.endsWith(']');
|
|
204
|
+
}
|
|
205
|
+
function isTrailingNonDataLine(line) {
|
|
206
|
+
const trimmed = line.trim();
|
|
207
|
+
return trimmed.length === 0 || trimmed.startsWith('#');
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=codex-mini-toml.js.map
|