@agentdeck/hooks 0.2.0 → 0.2.3
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 +72 -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 +241 -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codex-mini-toml.js","sourceRoot":"","sources":["../src/codex-mini-toml.ts"],"names":[],"mappings":"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"}
|
package/dist/install.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export { installCodexHooksIfNeeded, uninstallCodexHooks, migrateCodexHooks, managedBlockBody as codexManagedBlockBody, DEFAULT_CODEX_CONFIG_PATH, } from './codex-install.js';
|
|
2
|
+
export type { InstallOptions as CodexInstallOptions, InstallResult as CodexInstallResult } from './codex-install.js';
|
|
3
|
+
export { installOpenCodeHooksIfNeeded, uninstallOpenCodeHooks, opencodePluginPath, opencodePluginSource, } from './opencode-install.js';
|
|
4
|
+
export type { OpenCodeInstallOptions, OpenCodeInstallResult } from './opencode-install.js';
|
|
1
5
|
export declare const HOOK_EVENTS: readonly ["SessionStart", "SessionEnd", "PreToolUse", "PostToolUse", "Stop", "Notification", "UserPromptSubmit"];
|
|
2
6
|
/**
|
|
3
7
|
* Shell snippet that resolves the AgentDeck daemon's HTTP port at hook
|
|
@@ -10,15 +14,31 @@ export declare const HOOK_EVENTS: readonly ["SessionStart", "SessionEnd", "PreTo
|
|
|
10
14
|
* so the hook targets the *owning* daemon even when several daemons
|
|
11
15
|
* are running in parallel)
|
|
12
16
|
* 2. `~/.agentdeck/daemon.json` (CLI)
|
|
13
|
-
* 3. App Store
|
|
14
|
-
* 4.
|
|
17
|
+
* 3. App Store sandbox container `daemon.json` (Swift)
|
|
18
|
+
* 4. Legacy App Store group container `daemon.json` (Swift)
|
|
19
|
+
* 5. `9120` fallback (legacy, never reached in practice)
|
|
15
20
|
*
|
|
16
|
-
* For (2)
|
|
21
|
+
* For (2)-(4) the snippet prefers the daemon's `httpPort` field when
|
|
17
22
|
* present — Swift runs WS and HTTP on separate ports. Each candidate is
|
|
18
23
|
* verified with a short `/health` probe so a stale daemon.json from a
|
|
19
24
|
* crashed daemon doesn't swallow the hook.
|
|
20
25
|
*/
|
|
21
26
|
export declare function buildHookCommand(eventName: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Windows variant of `buildHookCommand`. Claude Code v2.1+ executes hook
|
|
29
|
+
* commands through `cmd.exe` on Windows, so we shell out to PowerShell for
|
|
30
|
+
* the JSON read + HTTP POST. Discovery is narrower than POSIX since the
|
|
31
|
+
* macOS App Store sandbox paths don't exist on Windows:
|
|
32
|
+
*
|
|
33
|
+
* 1. `$env:AGENTDECK_PORT`
|
|
34
|
+
* 2. `%USERPROFILE%\.agentdeck\daemon.json` (verified with `/health` probe)
|
|
35
|
+
* 3. `9120` fallback
|
|
36
|
+
*
|
|
37
|
+
* Single quotes are used inside the PowerShell script so the entire `-Command`
|
|
38
|
+
* argument can stay double-quoted under cmd.exe. Errors are swallowed so a
|
|
39
|
+
* dead daemon never blocks the host session.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildHookCommandWin(eventName: string): string;
|
|
22
42
|
export declare function buildHookEntry(eventName: string): {
|
|
23
43
|
matcher: string;
|
|
24
44
|
hooks: any[];
|
package/dist/install.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,IAAI,qBAAqB,EACzC,yBAAyB,GAC1B,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,cAAc,IAAI,mBAAmB,EAAE,aAAa,IAAI,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAIrH,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,kBAAkB,EAClB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAE3F,eAAO,MAAM,WAAW,kHAQd,CAAC;AAEX;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CA0B1D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAe7D;AAGD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM;;;EAgB/C;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,QAAQ,EAAE,GAAG,GAAG,GAAG,CAuB7C;AAED,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,QAAQ,EAAE,GAAG,GAAG,GAAG,CAwB9C;AAED,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,GAAG,GAAG;IAAE,QAAQ,EAAE,GAAG,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAyChF;AAED,4EAA4E;AAC5E,wBAAgB,YAAY,IAAI,IAAI,CAkBnC;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,IAAI,IAAI,CASrC;AAED;iEACiE;AACjE,wBAAgB,oBAAoB,IAAI,IAAI,CAwB3C"}
|
package/dist/install.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
// Re-export the Codex installer surface so callers reach it via the
|
|
5
|
+
// canonical `@agentdeck/hooks` entry point alongside the Claude installer.
|
|
6
|
+
export { installCodexHooksIfNeeded, uninstallCodexHooks, migrateCodexHooks, managedBlockBody as codexManagedBlockBody, DEFAULT_CODEX_CONFIG_PATH, } from './codex-install.js';
|
|
7
|
+
// Re-export the OpenCode plugin installer (standalone-session observation
|
|
8
|
+
// via opencode_* lifecycle hooks) through the same canonical entry point.
|
|
9
|
+
export { installOpenCodeHooksIfNeeded, uninstallOpenCodeHooks, opencodePluginPath, opencodePluginSource, } from './opencode-install.js';
|
|
4
10
|
export const HOOK_EVENTS = [
|
|
5
11
|
'SessionStart',
|
|
6
12
|
'SessionEnd',
|
|
@@ -21,33 +27,80 @@ export const HOOK_EVENTS = [
|
|
|
21
27
|
* so the hook targets the *owning* daemon even when several daemons
|
|
22
28
|
* are running in parallel)
|
|
23
29
|
* 2. `~/.agentdeck/daemon.json` (CLI)
|
|
24
|
-
* 3. App Store
|
|
25
|
-
* 4.
|
|
30
|
+
* 3. App Store sandbox container `daemon.json` (Swift)
|
|
31
|
+
* 4. Legacy App Store group container `daemon.json` (Swift)
|
|
32
|
+
* 5. `9120` fallback (legacy, never reached in practice)
|
|
26
33
|
*
|
|
27
|
-
* For (2)
|
|
34
|
+
* For (2)-(4) the snippet prefers the daemon's `httpPort` field when
|
|
28
35
|
* present — Swift runs WS and HTTP on separate ports. Each candidate is
|
|
29
36
|
* verified with a short `/health` probe so a stale daemon.json from a
|
|
30
37
|
* crashed daemon doesn't swallow the hook.
|
|
31
38
|
*/
|
|
32
39
|
export function buildHookCommand(eventName) {
|
|
33
|
-
|
|
40
|
+
const preamble = [
|
|
34
41
|
`PORT="\${AGENTDECK_PORT:-}"`,
|
|
35
42
|
`if [ -z "$PORT" ]; then`,
|
|
36
|
-
` for F in "$HOME/.agentdeck/daemon.json" "$HOME/Library/Group Containers/group.bound.serendipity.
|
|
43
|
+
` 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`,
|
|
37
44
|
` [ -f "$F" ] || continue`,
|
|
38
45
|
` P=$(python3 -c "import json;d=json.load(open('$F'));print(d.get('httpPort') or d.get('port',''))" 2>/dev/null)`,
|
|
39
46
|
` [ -n "$P" ] && curl -sf --max-time 0.3 "http://127.0.0.1:$P/health" >/dev/null 2>&1 && { PORT="$P"; break; }`,
|
|
40
47
|
` done`,
|
|
41
48
|
`fi`,
|
|
42
49
|
`PORT="\${PORT:-9120}"`,
|
|
50
|
+
];
|
|
51
|
+
// PreToolUse is request-response: the daemon may hold the connection open and
|
|
52
|
+
// return a permission decision (device approval). Capture the body and echo it
|
|
53
|
+
// to stdout so Claude can gate the tool; empty output (timeout/error/disabled)
|
|
54
|
+
// = Claude's normal permission flow. `--max-time 60` exceeds the daemon's
|
|
55
|
+
// internal 45s "ask" fallback so that fallback reaches Claude before curl quits.
|
|
56
|
+
if (eventName === 'PreToolUse') {
|
|
57
|
+
return preamble.concat([
|
|
58
|
+
`RESP=$(curl -s -X POST "http://127.0.0.1:$PORT/hooks/PreToolUse" -H 'Content-Type: application/json' --max-time 60 -d @- 2>/dev/null)`,
|
|
59
|
+
`printf '%s' "\${RESP:-}"`,
|
|
60
|
+
]).join('\n');
|
|
61
|
+
}
|
|
62
|
+
return preamble.concat([
|
|
43
63
|
`curl -sf -X POST "http://127.0.0.1:$PORT/hooks/${eventName}" -H 'Content-Type: application/json' -d @- 2>/dev/null || true`,
|
|
44
|
-
].join('\n');
|
|
64
|
+
]).join('\n');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Windows variant of `buildHookCommand`. Claude Code v2.1+ executes hook
|
|
68
|
+
* commands through `cmd.exe` on Windows, so we shell out to PowerShell for
|
|
69
|
+
* the JSON read + HTTP POST. Discovery is narrower than POSIX since the
|
|
70
|
+
* macOS App Store sandbox paths don't exist on Windows:
|
|
71
|
+
*
|
|
72
|
+
* 1. `$env:AGENTDECK_PORT`
|
|
73
|
+
* 2. `%USERPROFILE%\.agentdeck\daemon.json` (verified with `/health` probe)
|
|
74
|
+
* 3. `9120` fallback
|
|
75
|
+
*
|
|
76
|
+
* Single quotes are used inside the PowerShell script so the entire `-Command`
|
|
77
|
+
* argument can stay double-quoted under cmd.exe. Errors are swallowed so a
|
|
78
|
+
* dead daemon never blocks the host session.
|
|
79
|
+
*/
|
|
80
|
+
export function buildHookCommandWin(eventName) {
|
|
81
|
+
const ps = [
|
|
82
|
+
`$ev='${eventName}'`,
|
|
83
|
+
`$port=$env:AGENTDECK_PORT`,
|
|
84
|
+
`if(-not $port){$f=Join-Path $env:USERPROFILE '.agentdeck\\daemon.json'; if(Test-Path $f){try{$d=Get-Content -Raw $f|ConvertFrom-Json; $p=if($d.httpPort){$d.httpPort}else{$d.port}; if($p){try{Invoke-RestMethod -Uri ('http://127.0.0.1:'+$p+'/health') -TimeoutSec 1 -ErrorAction Stop|Out-Null; $port=$p}catch{}}}catch{}}}`,
|
|
85
|
+
`if(-not $port){$port=9120}`,
|
|
86
|
+
// Read stdin as UTF-8: [Console]::In decodes piped stdin with the console OEM
|
|
87
|
+
// codepage (e.g. CP949), garbling non-ASCII payload text.
|
|
88
|
+
`$body=(New-Object System.IO.StreamReader([Console]::OpenStandardInput(),[System.Text.Encoding]::UTF8)).ReadToEnd()`,
|
|
89
|
+
// Post UTF-8 bytes: Invoke-RestMethod encodes a string body as ISO-8859-1 when
|
|
90
|
+
// the content type carries no charset, replacing non-ASCII characters with '?'.
|
|
91
|
+
`$bytes=[System.Text.Encoding]::UTF8.GetBytes([string]$body)`,
|
|
92
|
+
`try{Invoke-RestMethod -Uri ('http://127.0.0.1:'+$port+'/hooks/'+$ev) -Method Post -Body $bytes -ContentType 'application/json; charset=utf-8' -TimeoutSec 2 -ErrorAction Stop|Out-Null}catch{}`,
|
|
93
|
+
].join('; ');
|
|
94
|
+
return `powershell -NoProfile -ExecutionPolicy Bypass -Command "${ps}"`;
|
|
45
95
|
}
|
|
46
96
|
// Claude Code v2.1+ requires 3-level nesting: event → matcher group → hook handler.
|
|
47
97
|
export function buildHookEntry(eventName) {
|
|
98
|
+
const command = process.platform === 'win32'
|
|
99
|
+
? buildHookCommandWin(eventName)
|
|
100
|
+
: buildHookCommand(eventName);
|
|
48
101
|
const handler = {
|
|
49
102
|
type: 'command',
|
|
50
|
-
command
|
|
103
|
+
command,
|
|
51
104
|
};
|
|
52
105
|
// Tool-specific hooks (PreToolUse, PostToolUse) need a glob matcher to fire.
|
|
53
106
|
// Empty string "" means "match nothing" for tool events — use "" for non-tool
|
|
@@ -194,12 +247,22 @@ export function migrateHooksIfNeeded() {
|
|
|
194
247
|
}
|
|
195
248
|
}
|
|
196
249
|
// CLI execution
|
|
197
|
-
|
|
198
|
-
|
|
250
|
+
// Use pathToFileURL so the comparison is correct on Windows too — process.argv[1]
|
|
251
|
+
// is a native path (E:\dev\...\install.js), but import.meta.url is a file:// URL
|
|
252
|
+
// (file:///E:/dev/.../install.js). Manually building `file://${argv[1]}` only
|
|
253
|
+
// matches on POSIX, which is why the installer used to be a silent no-op on
|
|
254
|
+
// Windows.
|
|
255
|
+
import { pathToFileURL } from 'url';
|
|
256
|
+
const isMainModule = process.argv[1] ? import.meta.url === pathToFileURL(process.argv[1]).href : false;
|
|
199
257
|
if (isMainModule) {
|
|
200
258
|
const action = process.argv[2] || 'install';
|
|
201
259
|
if (action === 'uninstall') {
|
|
202
260
|
uninstallHooks();
|
|
261
|
+
// The OpenCode observer plugin is AgentDeck-owned in its entirety, so
|
|
262
|
+
// uninstall removes the file (unlike ~/.codex/config.toml, where only
|
|
263
|
+
// the fenced block is AgentDeck's and removal has its own dedicated
|
|
264
|
+
// flow to avoid touching user TOML).
|
|
265
|
+
import('./opencode-install.js').then((m) => m.uninstallOpenCodeHooks()).catch(() => { });
|
|
203
266
|
}
|
|
204
267
|
else {
|
|
205
268
|
installHooks();
|
package/dist/install.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAE7B,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,cAAc;IACd,YAAY;IACZ,YAAY;IACZ,aAAa;IACb,MAAM;IACN,cAAc;IACd,kBAAkB;CACV,CAAC;AAEX
|
|
1
|
+
{"version":3,"file":"install.js","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const OPENCODE_PLUGIN_FILENAME = "agentdeck.js";
|
|
2
|
+
/** OpenCode's global config root: `$XDG_CONFIG_HOME/opencode` falling back
|
|
3
|
+
* to `~/.config/opencode`. Plugins load from the `plugins/` subdirectory
|
|
4
|
+
* (current OpenCode releases; older builds also scanned `plugin/`). */
|
|
5
|
+
export declare function opencodePluginPath(): string;
|
|
6
|
+
export interface OpenCodeInstallOptions {
|
|
7
|
+
/** Override the plugin file path (tests + non-default homes). */
|
|
8
|
+
pluginPath?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface OpenCodeInstallResult {
|
|
11
|
+
installed: boolean;
|
|
12
|
+
/** Human-readable reason when `installed === false`. */
|
|
13
|
+
reason?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Assemble the plugin source. Exported so tests can assert schema
|
|
16
|
+
* regressions (event names, payload fields, managed-session guard)
|
|
17
|
+
* without driving the file installer. */
|
|
18
|
+
export declare function opencodePluginSource(): string;
|
|
19
|
+
/** Install (or refresh) the AgentDeck OpenCode plugin. Idempotent: writes
|
|
20
|
+
* only when the on-disk content differs from the current template. Safe to
|
|
21
|
+
* call from setup, `agentdeck opencode`, and `agentdeck daemon install`. */
|
|
22
|
+
export declare function installOpenCodeHooksIfNeeded(opts?: OpenCodeInstallOptions): OpenCodeInstallResult;
|
|
23
|
+
/** Remove the AgentDeck OpenCode plugin. Idempotent — no-op when absent. */
|
|
24
|
+
export declare function uninstallOpenCodeHooks(opts?: OpenCodeInstallOptions): void;
|
|
25
|
+
//# sourceMappingURL=opencode-install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-install.d.ts","sourceRoot":"","sources":["../src/opencode-install.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,wBAAwB,iBAAiB,CAAC;AAEvD;;wEAEwE;AACxE,wBAAgB,kBAAkB,IAAI,MAAM,CAI3C;AAED,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAUD;;0CAE0C;AAC1C,wBAAgB,oBAAoB,IAAI,MAAM,CAwJ7C;AAeD;;6EAE6E;AAC7E,wBAAgB,4BAA4B,CAAC,IAAI,GAAE,sBAA2B,GAAG,qBAAqB,CAerG;AAED,4EAA4E;AAC5E,wBAAgB,sBAAsB,CAAC,IAAI,GAAE,sBAA2B,GAAG,IAAI,CAK9E"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// opencode-install.ts — Install the AgentDeck observation plugin into
|
|
2
|
+
// OpenCode's global plugin directory so standalone `opencode` sessions
|
|
3
|
+
// report turn boundaries to the daemon as opencode_* lifecycle hooks.
|
|
4
|
+
//
|
|
5
|
+
// Why a plugin and not PTY parsing or an SSE attach: OpenCode has no
|
|
6
|
+
// lifecycle-hook config (unlike Claude Code / Codex), and a standalone
|
|
7
|
+
// `opencode` TUI does not listen on any TCP port (the embedded server
|
|
8
|
+
// only exists when launched with `--port`, i.e. managed sessions), so
|
|
9
|
+
// the documented plugin API is the only stable, update-resilient way to
|
|
10
|
+
// observe sessions the user starts outside `agentdeck opencode`. This is
|
|
11
|
+
// the hooks-first direction: no terminal-output parsing anywhere.
|
|
12
|
+
//
|
|
13
|
+
// The plugin file is wholly AgentDeck-owned (header sentinel + version).
|
|
14
|
+
// Install is idempotent: re-running writes only when content changed.
|
|
15
|
+
// Managed sessions are excluded inside the plugin itself via the
|
|
16
|
+
// AGENTDECK_PORT env marker (see plugin source below) — their session
|
|
17
|
+
// bridge already ingests the richer SSE stream, and double-posting would
|
|
18
|
+
// double-count every turn.
|
|
19
|
+
//
|
|
20
|
+
// Counterpart consumers:
|
|
21
|
+
// - bridge/src/daemon-server.ts /hooks/ handler (opencode_* → shared
|
|
22
|
+
// observed-session pipeline: APME run/turn + chat_start/chat_response)
|
|
23
|
+
// - apple .../DaemonServer.swift ignores opencode_* events (timeline
|
|
24
|
+
// parity on the Swift daemon is a documented follow-up).
|
|
25
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from 'fs';
|
|
26
|
+
import { dirname, join } from 'path';
|
|
27
|
+
import { homedir } from 'os';
|
|
28
|
+
export const OPENCODE_PLUGIN_FILENAME = 'agentdeck.js';
|
|
29
|
+
/** OpenCode's global config root: `$XDG_CONFIG_HOME/opencode` falling back
|
|
30
|
+
* to `~/.config/opencode`. Plugins load from the `plugins/` subdirectory
|
|
31
|
+
* (current OpenCode releases; older builds also scanned `plugin/`). */
|
|
32
|
+
export function opencodePluginPath() {
|
|
33
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
34
|
+
const configRoot = xdg && xdg.trim() ? xdg : join(homedir(), '.config');
|
|
35
|
+
return join(configRoot, 'opencode', 'plugins', OPENCODE_PLUGIN_FILENAME);
|
|
36
|
+
}
|
|
37
|
+
/** Honour `AGENTDECK_NO_OPENCODE_HOOKS=1` from the environment (same
|
|
38
|
+
* opt-out contract as AGENTDECK_NO_CODEX_HOOKS). */
|
|
39
|
+
function envOptOut() {
|
|
40
|
+
return process.env.AGENTDECK_NO_OPENCODE_HOOKS === '1';
|
|
41
|
+
}
|
|
42
|
+
const PLUGIN_VERSION = 1;
|
|
43
|
+
/** Assemble the plugin source. Exported so tests can assert schema
|
|
44
|
+
* regressions (event names, payload fields, managed-session guard)
|
|
45
|
+
* without driving the file installer. */
|
|
46
|
+
export function opencodePluginSource() {
|
|
47
|
+
return `// agentdeck-opencode-plugin v${PLUGIN_VERSION} — AUTO-GENERATED by AgentDeck.
|
|
48
|
+
// Do not edit: reinstalls overwrite this file. Remove it (or set
|
|
49
|
+
// AGENTDECK_NO_OPENCODE_HOOKS=1 before installing) to opt out.
|
|
50
|
+
//
|
|
51
|
+
// Forwards OpenCode lifecycle events to the AgentDeck daemon as
|
|
52
|
+
// opencode_* hooks — the same observed-session pipeline Claude Code and
|
|
53
|
+
// Codex use — so standalone \`opencode\` sessions appear on the AgentDeck
|
|
54
|
+
// timeline with the same prompt → response turn shape. Every POST is
|
|
55
|
+
// fire-and-forget with a hard timeout; OpenCode never blocks on AgentDeck.
|
|
56
|
+
|
|
57
|
+
import { readFileSync } from "node:fs";
|
|
58
|
+
import { homedir } from "node:os";
|
|
59
|
+
import { join } from "node:path";
|
|
60
|
+
|
|
61
|
+
const PORT_TTL_MS = 60000;
|
|
62
|
+
let cachedPort = null;
|
|
63
|
+
let cachedAt = 0;
|
|
64
|
+
|
|
65
|
+
async function resolvePort() {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
if (cachedPort && now - cachedAt < PORT_TTL_MS) return cachedPort;
|
|
68
|
+
const home = homedir();
|
|
69
|
+
const candidates = [];
|
|
70
|
+
for (const f of [
|
|
71
|
+
join(home, ".agentdeck", "daemon.json"),
|
|
72
|
+
join(home, "Library/Containers/bound.serendipity.agent.deck/Data/Library/Application Support/AgentDeck/daemon.json"),
|
|
73
|
+
join(home, "Library/Group Containers/group.bound.serendipity.agent.deck/daemon.json"),
|
|
74
|
+
]) {
|
|
75
|
+
try {
|
|
76
|
+
const d = JSON.parse(readFileSync(f, "utf-8"));
|
|
77
|
+
const p = d.httpPort || d.port;
|
|
78
|
+
if (typeof p === "number" && p > 0 && !candidates.includes(p)) candidates.push(p);
|
|
79
|
+
} catch { /* missing/malformed — try next */ }
|
|
80
|
+
}
|
|
81
|
+
if (!candidates.includes(9120)) candidates.push(9120);
|
|
82
|
+
for (const p of candidates) {
|
|
83
|
+
try {
|
|
84
|
+
const r = await fetch("http://127.0.0.1:" + p + "/health", { signal: AbortSignal.timeout(300) });
|
|
85
|
+
if (r.ok) { cachedPort = p; cachedAt = now; return p; }
|
|
86
|
+
} catch { /* dead port — try next */ }
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function post(event, body) {
|
|
92
|
+
resolvePort().then((port) => {
|
|
93
|
+
if (!port) return;
|
|
94
|
+
return fetch("http://127.0.0.1:" + port + "/hooks/" + event, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "Content-Type": "application/json" },
|
|
97
|
+
body: JSON.stringify(body),
|
|
98
|
+
signal: AbortSignal.timeout(800),
|
|
99
|
+
});
|
|
100
|
+
}).catch(() => { /* daemon down — observation is best-effort */ });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const AgentDeckObserver = async ({ directory }) => {
|
|
104
|
+
// Managed \`agentdeck opencode\` sessions already stream structured SSE
|
|
105
|
+
// events through their session bridge; posting hooks too would
|
|
106
|
+
// double-count every turn. AGENTDECK_PORT marks a managed PTY.
|
|
107
|
+
if (process.env.AGENTDECK_PORT) return {};
|
|
108
|
+
|
|
109
|
+
const cwd = directory;
|
|
110
|
+
const announced = new Set(); // sessionIDs whose opencode_session_start was posted
|
|
111
|
+
const userMsgs = new Map(); // user messageID -> sessionID
|
|
112
|
+
const promptSent = new Set(); // user messageIDs whose prompt_submit was posted
|
|
113
|
+
const promptTimers = new Map();// user messageID -> flush timer
|
|
114
|
+
const responses = new Map(); // sessionID -> latest assistant text
|
|
115
|
+
const startedAt = new Map(); // sessionID -> turn start ms
|
|
116
|
+
const toolPhase = new Map(); // tool callID -> "start" | "end"
|
|
117
|
+
|
|
118
|
+
function announce(sessionID) {
|
|
119
|
+
if (!sessionID || announced.has(sessionID)) return;
|
|
120
|
+
announced.add(sessionID);
|
|
121
|
+
post("opencode_session_start", { session_id: sessionID, cwd });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function sendPrompt(messageID, sessionID, prompt) {
|
|
125
|
+
if (promptSent.has(messageID)) return;
|
|
126
|
+
promptSent.add(messageID);
|
|
127
|
+
const t = promptTimers.get(messageID);
|
|
128
|
+
if (t) { clearTimeout(t); promptTimers.delete(messageID); }
|
|
129
|
+
if (!startedAt.has(sessionID)) startedAt.set(sessionID, Date.now());
|
|
130
|
+
post("opencode_user_prompt_submit", { session_id: sessionID, prompt: prompt || "", cwd });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
event: async ({ event }) => {
|
|
135
|
+
try {
|
|
136
|
+
const type = event.type;
|
|
137
|
+
const props = event.properties || {};
|
|
138
|
+
if (type === "message.updated") {
|
|
139
|
+
const info = props.info || {};
|
|
140
|
+
const sessionID = info.sessionID;
|
|
141
|
+
announce(sessionID);
|
|
142
|
+
if (info.role === "user" && info.id) {
|
|
143
|
+
userMsgs.set(info.id, sessionID);
|
|
144
|
+
// Prompt text usually streams via message.part.updated moments
|
|
145
|
+
// later; flush immediately when the info carries it, otherwise
|
|
146
|
+
// wait briefly so the chat_start row shows the real prompt.
|
|
147
|
+
const direct = typeof info.text === "string" ? info.text
|
|
148
|
+
: (typeof info.content === "string" ? info.content : "");
|
|
149
|
+
if (direct.trim()) {
|
|
150
|
+
sendPrompt(info.id, sessionID, direct.trim());
|
|
151
|
+
} else if (!promptTimers.has(info.id) && !promptSent.has(info.id)) {
|
|
152
|
+
const timer = setTimeout(() => sendPrompt(info.id, sessionID, ""), 800);
|
|
153
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
154
|
+
promptTimers.set(info.id, timer);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} else if (type === "message.part.updated") {
|
|
158
|
+
const part = props.part || {};
|
|
159
|
+
const sessionID = part.sessionID;
|
|
160
|
+
announce(sessionID);
|
|
161
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
162
|
+
const owner = userMsgs.get(part.messageID);
|
|
163
|
+
if (owner) sendPrompt(part.messageID, owner, part.text.trim());
|
|
164
|
+
else responses.set(sessionID, part.text);
|
|
165
|
+
} else if (part.type === "tool" && part.callID) {
|
|
166
|
+
const status = (part.state && part.state.status) || "running";
|
|
167
|
+
const phase = toolPhase.get(part.callID);
|
|
168
|
+
if (status === "completed" || status === "error") {
|
|
169
|
+
if (phase !== "end") {
|
|
170
|
+
toolPhase.set(part.callID, "end");
|
|
171
|
+
post("opencode_tool_end", { session_id: sessionID, tool_name: part.tool || "tool", cwd });
|
|
172
|
+
}
|
|
173
|
+
} else if (!phase) {
|
|
174
|
+
toolPhase.set(part.callID, "start");
|
|
175
|
+
post("opencode_tool_start", { session_id: sessionID, tool_name: part.tool || "tool", cwd });
|
|
176
|
+
}
|
|
177
|
+
if (toolPhase.size > 512) toolPhase.clear();
|
|
178
|
+
}
|
|
179
|
+
} else if (type === "session.idle") {
|
|
180
|
+
const sessionID = props.sessionID;
|
|
181
|
+
announce(sessionID);
|
|
182
|
+
post("opencode_stop", {
|
|
183
|
+
session_id: sessionID,
|
|
184
|
+
last_assistant_message: responses.get(sessionID) || "",
|
|
185
|
+
cwd,
|
|
186
|
+
});
|
|
187
|
+
responses.delete(sessionID);
|
|
188
|
+
startedAt.delete(sessionID);
|
|
189
|
+
for (const [mid, sid] of userMsgs) {
|
|
190
|
+
if (sid === sessionID) { userMsgs.delete(mid); promptSent.delete(mid); }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch { /* never throw into OpenCode's event loop */ }
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
function writeTextAtomic(text, path) {
|
|
200
|
+
const dir = dirname(path);
|
|
201
|
+
try {
|
|
202
|
+
if (!existsSync(dir))
|
|
203
|
+
mkdirSync(dir, { recursive: true });
|
|
204
|
+
const tmp = `${path}.agentdeck.tmp`;
|
|
205
|
+
writeFileSync(tmp, text, 'utf-8');
|
|
206
|
+
renameSync(tmp, path);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Install (or refresh) the AgentDeck OpenCode plugin. Idempotent: writes
|
|
214
|
+
* only when the on-disk content differs from the current template. Safe to
|
|
215
|
+
* call from setup, `agentdeck opencode`, and `agentdeck daemon install`. */
|
|
216
|
+
export function installOpenCodeHooksIfNeeded(opts = {}) {
|
|
217
|
+
if (envOptOut())
|
|
218
|
+
return { installed: false, reason: 'AGENTDECK_NO_OPENCODE_HOOKS=1' };
|
|
219
|
+
const path = opts.pluginPath ?? opencodePluginPath();
|
|
220
|
+
const desired = opencodePluginSource();
|
|
221
|
+
try {
|
|
222
|
+
if (existsSync(path) && readFileSync(path, 'utf-8') === desired) {
|
|
223
|
+
return { installed: true };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch { /* unreadable — rewrite below */ }
|
|
227
|
+
if (writeTextAtomic(desired, path)) {
|
|
228
|
+
return { installed: true };
|
|
229
|
+
}
|
|
230
|
+
return { installed: false, reason: `write failed: ${path}` };
|
|
231
|
+
}
|
|
232
|
+
/** Remove the AgentDeck OpenCode plugin. Idempotent — no-op when absent. */
|
|
233
|
+
export function uninstallOpenCodeHooks(opts = {}) {
|
|
234
|
+
const path = opts.pluginPath ?? opencodePluginPath();
|
|
235
|
+
try {
|
|
236
|
+
if (existsSync(path))
|
|
237
|
+
unlinkSync(path);
|
|
238
|
+
}
|
|
239
|
+
catch { /* best-effort */ }
|
|
240
|
+
}
|
|
241
|
+
//# sourceMappingURL=opencode-install.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-install.js","sourceRoot":"","sources":["../src/opencode-install.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentdeck/hooks",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"private": false,
|
|
4
5
|
"type": "module",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "https://github.com/puritysb/AgentDeck.git",
|
|
9
|
-
"directory": "hooks"
|
|
10
|
-
},
|
|
11
6
|
"files": [
|
|
12
7
|
"dist",
|
|
13
8
|
"!dist/__tests__"
|