@blastin-dev/clocktopus-cli 0.2.0 → 0.2.1

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.
Files changed (38) hide show
  1. package/README.md +89 -34
  2. package/dist/src/commands/agent/disable.d.ts +8 -1
  3. package/dist/src/commands/agent/disable.d.ts.map +1 -1
  4. package/dist/src/commands/agent/disable.js +79 -45
  5. package/dist/src/commands/agent/doctor.d.ts.map +1 -1
  6. package/dist/src/commands/agent/doctor.js +146 -75
  7. package/dist/src/commands/agent/hook.d.ts +4 -1
  8. package/dist/src/commands/agent/hook.d.ts.map +1 -1
  9. package/dist/src/commands/agent/hook.js +152 -15
  10. package/dist/src/commands/agent/setup.d.ts +17 -10
  11. package/dist/src/commands/agent/setup.d.ts.map +1 -1
  12. package/dist/src/commands/agent/setup.js +208 -69
  13. package/dist/src/commands/agent/status.d.ts.map +1 -1
  14. package/dist/src/commands/agent/status.js +46 -24
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +18 -4
  17. package/dist/src/lib/agent-config.d.ts +18 -3
  18. package/dist/src/lib/agent-config.d.ts.map +1 -1
  19. package/dist/src/lib/agent-config.js +44 -19
  20. package/dist/src/lib/agent-hook-state.d.ts +9 -1
  21. package/dist/src/lib/agent-hook-state.d.ts.map +1 -1
  22. package/dist/src/lib/agent-hook-state.js +21 -2
  23. package/dist/src/lib/agents.d.ts +115 -0
  24. package/dist/src/lib/agents.d.ts.map +1 -0
  25. package/dist/src/lib/agents.js +245 -0
  26. package/dist/src/lib/codex-config.d.ts +166 -0
  27. package/dist/src/lib/codex-config.d.ts.map +1 -0
  28. package/dist/src/lib/codex-config.js +441 -0
  29. package/dist/src/lib/codex-config.test.d.ts +2 -0
  30. package/dist/src/lib/codex-config.test.d.ts.map +1 -0
  31. package/dist/src/lib/codex-config.test.js +359 -0
  32. package/dist/src/lib/opencode-config.d.ts +108 -0
  33. package/dist/src/lib/opencode-config.d.ts.map +1 -0
  34. package/dist/src/lib/opencode-config.js +330 -0
  35. package/dist/src/lib/opencode-config.test.d.ts +2 -0
  36. package/dist/src/lib/opencode-config.test.d.ts.map +1 -0
  37. package/dist/src/lib/opencode-config.test.js +140 -0
  38. package/package.json +2 -1
@@ -0,0 +1,359 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { parse as parseToml } from "smol-toml";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { applyCodexHooks, applyCodexOtel, buildCodexOtel, CodexConfigParseError, readCodexConfig, readCodexHooks, readCodexHookTrust, readCodexTelemetry, removeCodexHooks, removeCodexOtel, spliceOtelSection, writeCodexConfig, writeCodexHooks, } from "./codex-config";
7
+ /**
8
+ * These tests exist because this module edits two files we do not own.
9
+ * `~/.codex/config.toml` holds the user's model, sandbox policy, MCP
10
+ * servers and project trust decisions — hand-written and usually commented
11
+ * — and `hooks.json` may already hold hooks that have nothing to do with
12
+ * us. A merge bug here destroys work unrelated to Clocktopus.
13
+ */
14
+ const INSTALL = {
15
+ token: "ctop_agt_testtoken",
16
+ endpoint: "https://otel.example.com",
17
+ hookCommand: "clocktopus agent hook --provider codex_cli",
18
+ };
19
+ /** A config.toml with the user's own settings, comments and layout. */
20
+ const USER_CONFIG = `# my codex setup
21
+ model = "gpt-5.6-sol"
22
+ approval_policy = "on-request"
23
+
24
+ [sandbox_workspace_write]
25
+ network_access = true # needed for npm
26
+
27
+ [mcp_servers.linear]
28
+ command = "npx"
29
+ args = ["-y", "linear-mcp"]
30
+ `;
31
+ let dir;
32
+ let configPath;
33
+ let hooksPath;
34
+ beforeEach(() => {
35
+ dir = mkdtempSync(join(tmpdir(), "clocktopus-codex-"));
36
+ configPath = join(dir, "config.toml");
37
+ hooksPath = join(dir, "hooks.json");
38
+ });
39
+ afterEach(() => {
40
+ rmSync(dir, { recursive: true, force: true });
41
+ });
42
+ describe("buildCodexOtel", () => {
43
+ it("puts the receiver path on the endpoint, because Codex does not add one", () => {
44
+ const otel = buildCodexOtel(INSTALL);
45
+ const http = otel.exporter["otlp-http"];
46
+ // Codex POSTs to the configured URL verbatim — unlike the OTel SDK, it
47
+ // does not append `/v1/logs`. Omitting the path here would send every
48
+ // export to the receiver's root and 404 silently.
49
+ expect(http.endpoint).toBe("https://otel.example.com/v1/logs");
50
+ expect(http.protocol).toBe("json");
51
+ expect(http.headers.Authorization).toBe("Bearer ctop_agt_testtoken");
52
+ });
53
+ it("does not double the path when the endpoint already ends in a slash", () => {
54
+ const otel = buildCodexOtel({
55
+ ...INSTALL,
56
+ endpoint: "https://otel.example.com/",
57
+ });
58
+ const http = otel.exporter["otlp-http"];
59
+ expect(http.endpoint).toBe("https://otel.example.com/v1/logs");
60
+ });
61
+ it("turns prompt logging off explicitly rather than trusting the default", () => {
62
+ expect(buildCodexOtel(INSTALL).log_user_prompt).toBe(false);
63
+ });
64
+ });
65
+ describe("splicing [otel] into a hand-written config.toml", () => {
66
+ it("adds the section without disturbing comments or layout", () => {
67
+ const next = spliceOtelSection(USER_CONFIG, buildCodexOtel(INSTALL));
68
+ // Every line the user wrote survives, byte for byte. A round trip
69
+ // through a TOML serialiser would silently drop all three comments.
70
+ expect(next).toContain("# my codex setup");
71
+ expect(next).toContain("network_access = true # needed for npm");
72
+ expect(next).toContain('args = ["-y", "linear-mcp"]');
73
+ const parsed = parseToml(next);
74
+ expect(parsed.model).toBe("gpt-5.6-sol");
75
+ expect(parsed.mcp_servers).toEqual({
76
+ linear: { command: "npx", args: ["-y", "linear-mcp"] },
77
+ });
78
+ expect(parsed.otel.log_user_prompt).toBe(false);
79
+ });
80
+ it("replaces an existing [otel] section instead of appending a second", () => {
81
+ const withOtel = spliceOtelSection(USER_CONFIG, buildCodexOtel(INSTALL));
82
+ const twice = spliceOtelSection(withOtel, buildCodexOtel({ ...INSTALL, token: "ctop_agt_second" }));
83
+ // TOML rejects a duplicated table outright, so an append bug here would
84
+ // stop Codex booting at all.
85
+ expect(twice.match(/^\[otel\]/gm)).toHaveLength(1);
86
+ expect(twice).toContain("ctop_agt_second");
87
+ expect(twice).not.toContain("ctop_agt_testtoken");
88
+ expect(() => parseToml(twice)).not.toThrow();
89
+ });
90
+ it("keeps the user's own [otel] keys when merging ours in", () => {
91
+ const config = `${USER_CONFIG}
92
+ [otel]
93
+ environment = "staging"
94
+ `;
95
+ const parsed = parseToml(config);
96
+ const next = applyCodexOtel({ text: config, config: parsed }, INSTALL);
97
+ const result = parseToml(next);
98
+ expect(result.otel.environment).toBe("staging");
99
+ expect(result.otel.log_user_prompt).toBe(false);
100
+ });
101
+ it("removes only our keys, leaving the rest of [otel] behind", () => {
102
+ const config = `${USER_CONFIG}
103
+ [otel]
104
+ environment = "staging"
105
+ `;
106
+ const withOurs = applyCodexOtel({ text: config, config: parseToml(config) }, INSTALL);
107
+ const { nextText, removedKeys } = removeCodexOtel({
108
+ text: withOurs,
109
+ config: parseToml(withOurs),
110
+ });
111
+ expect(removedKeys.sort()).toEqual(["exporter", "log_user_prompt"]);
112
+ const parsed = parseToml(nextText);
113
+ expect(parsed.otel).toEqual({ environment: "staging" });
114
+ expect(parsed.model).toBe("gpt-5.6-sol");
115
+ });
116
+ it("drops [otel] entirely when nothing of the user's is left in it", () => {
117
+ const withOurs = spliceOtelSection(USER_CONFIG, buildCodexOtel(INSTALL));
118
+ const { nextText } = removeCodexOtel({
119
+ text: withOurs,
120
+ config: parseToml(withOurs),
121
+ });
122
+ expect(nextText).not.toContain("[otel]");
123
+ expect(parseToml(nextText)).toEqual(parseToml(USER_CONFIG));
124
+ });
125
+ it("survives an add/remove cycle without accumulating blank lines", () => {
126
+ let text = USER_CONFIG;
127
+ for (let i = 0; i < 3; i++) {
128
+ text = spliceOtelSection(text, buildCodexOtel(INSTALL));
129
+ text = removeCodexOtel({
130
+ text,
131
+ config: parseToml(text),
132
+ }).nextText;
133
+ }
134
+ expect(text).toBe(USER_CONFIG);
135
+ });
136
+ it("creates the section from nothing when the file does not exist yet", () => {
137
+ const next = spliceOtelSection("", buildCodexOtel(INSTALL));
138
+ expect(parseToml(next).otel).toBeDefined();
139
+ });
140
+ });
141
+ describe("writeCodexConfig — the guard on the splice", () => {
142
+ it("writes the edit and backs up what was there", () => {
143
+ writeFileSync(configPath, USER_CONFIG);
144
+ const current = readCodexConfig(configPath);
145
+ const { backupPath } = writeCodexConfig({
146
+ path: configPath,
147
+ previousText: current.text,
148
+ previousConfig: current.config,
149
+ nextText: applyCodexOtel(current, INSTALL),
150
+ });
151
+ expect(backupPath).toBe(`${configPath}.clocktopus-backup`);
152
+ expect(readFileSync(backupPath, "utf8")).toBe(USER_CONFIG);
153
+ expect(readCodexTelemetry(configPath).token).toBe(INSTALL.token);
154
+ });
155
+ it("refuses to write an edit that would change anything outside [otel]", () => {
156
+ writeFileSync(configPath, USER_CONFIG);
157
+ const current = readCodexConfig(configPath);
158
+ // Stands in for a splice that misread the file's layout. Rather than
159
+ // trusting the region scanner, the write verifies its own result — so a
160
+ // document it cannot handle costs the user nothing.
161
+ expect(() => writeCodexConfig({
162
+ path: configPath,
163
+ previousText: current.text,
164
+ previousConfig: current.config,
165
+ nextText: 'model = "gpt-5.6-sol"\n',
166
+ })).toThrow(/would have changed/);
167
+ expect(readFileSync(configPath, "utf8")).toBe(USER_CONFIG);
168
+ });
169
+ it("refuses to write anything that is not valid TOML", () => {
170
+ writeFileSync(configPath, USER_CONFIG);
171
+ const current = readCodexConfig(configPath);
172
+ expect(() => writeCodexConfig({
173
+ path: configPath,
174
+ previousText: current.text,
175
+ previousConfig: current.config,
176
+ nextText: "[[[nope",
177
+ })).toThrow(/did not produce valid TOML/);
178
+ expect(readFileSync(configPath, "utf8")).toBe(USER_CONFIG);
179
+ });
180
+ it("raises rather than overwriting a config.toml it cannot parse", () => {
181
+ // Far more likely a half-finished edit than something to clobber.
182
+ writeFileSync(configPath, "model = = =");
183
+ expect(() => readCodexConfig(configPath)).toThrow(CodexConfigParseError);
184
+ });
185
+ });
186
+ describe("readCodexTelemetry", () => {
187
+ it("strips the receiver path back off so the hook gets a base URL", () => {
188
+ writeFileSync(configPath, spliceOtelSection(USER_CONFIG, buildCodexOtel(INSTALL)));
189
+ const telemetry = readCodexTelemetry(configPath);
190
+ // The hook posts to `<endpoint>/v1/agent-session` and doctor to
191
+ // `<endpoint>/v1/verify`; leaving `/v1/logs` on would make both 404.
192
+ expect(telemetry.endpoint).toBe("https://otel.example.com");
193
+ expect(telemetry.token).toBe(INSTALL.token);
194
+ expect(telemetry.protocol).toBe("json");
195
+ });
196
+ it("reports nothing configured rather than throwing on a missing file", () => {
197
+ expect(readCodexTelemetry(configPath)).toMatchObject({
198
+ exists: false,
199
+ token: null,
200
+ endpoint: null,
201
+ });
202
+ });
203
+ });
204
+ describe("hooks.json", () => {
205
+ /** Reads one event's matcher groups back out with the shape Codex expects. */
206
+ const groups = (file, event) => file.hooks[event] ?? [];
207
+ const userHooks = () => ({
208
+ description: "my hooks",
209
+ hooks: {
210
+ SessionStart: [
211
+ { hooks: [{ type: "command", command: "echo user-session-start" }] },
212
+ ],
213
+ PreToolUse: [
214
+ { matcher: "shell", hooks: [{ type: "command", command: "echo pre" }] },
215
+ ],
216
+ },
217
+ });
218
+ it("appends our hook without removing the user's", () => {
219
+ const next = applyCodexHooks(userHooks(), INSTALL);
220
+ const start = groups(next, "SessionStart");
221
+ expect(start).toHaveLength(2);
222
+ expect(start[0].hooks[0].command).toBe("echo user-session-start");
223
+ expect(start[1].hooks[0].command).toBe(INSTALL.hookCommand);
224
+ expect(groups(next, "PreToolUse")).toEqual(userHooks().hooks.PreToolUse);
225
+ });
226
+ it("gives SessionEnd the 3s budget Codex clamps it to", () => {
227
+ const next = applyCodexHooks({}, INSTALL);
228
+ // Codex hard-clamps SessionEnd and prints a warning on every session
229
+ // start if we ask for more. 10s for SessionStart, which it honours.
230
+ expect(groups(next, "SessionStart")[0].hooks[0].timeout).toBe(10);
231
+ expect(groups(next, "SessionEnd")[0].hooks[0].timeout).toBe(3);
232
+ });
233
+ it("never writes an `async` key, on either event", () => {
234
+ // The regression this pins cost a real user their repository
235
+ // attribution. Codex 0.147 does not skip an async hook quietly-ish and
236
+ // carry on: it drops SessionStart entirely, so `cwd` — the only source
237
+ // of repository, branch and starting SHA anywhere in the pipeline —
238
+ // never arrives, while spend keeps flowing in unattributed. 0.148
239
+ // honours it on SessionStart but still forces SessionEnd synchronous
240
+ // and warns once per session.
241
+ //
242
+ // `agent hook` backgrounds itself instead, so this key has no upside on
243
+ // any version.
244
+ const next = applyCodexHooks({}, INSTALL);
245
+ for (const event of ["SessionStart", "SessionEnd"]) {
246
+ const entry = groups(next, event)[0].hooks[0];
247
+ expect(entry).not.toHaveProperty("async");
248
+ }
249
+ });
250
+ it("replaces our hook on re-run instead of installing a second", () => {
251
+ const once = applyCodexHooks(userHooks(), INSTALL);
252
+ const twice = applyCodexHooks(once, {
253
+ hookCommand: "/usr/local/bin/clocktopus agent hook --provider codex_cli",
254
+ });
255
+ // Two of our hooks would POST every SessionStart twice.
256
+ const start = groups(twice, "SessionStart");
257
+ expect(start).toHaveLength(2);
258
+ expect(start[1].hooks[0].command).toContain("/usr/local/bin/clocktopus");
259
+ });
260
+ it("removes ours and leaves the user's intact", () => {
261
+ const installed = applyCodexHooks(userHooks(), INSTALL);
262
+ const { file, removed } = removeCodexHooks(installed);
263
+ expect(removed).toBe(true);
264
+ expect(file).toMatchObject({ hooks: userHooks().hooks });
265
+ });
266
+ it("round-trips through disk and finds our command again", () => {
267
+ writeCodexHooks(applyCodexHooks(userHooks(), INSTALL), hooksPath);
268
+ const read = readCodexHooks(hooksPath);
269
+ expect(read.commands.SessionStart).toBe(INSTALL.hookCommand);
270
+ expect(read.commands.SessionEnd).toBe(INSTALL.hookCommand);
271
+ });
272
+ it("raises rather than overwriting a hooks.json it cannot parse", () => {
273
+ writeFileSync(hooksPath, "{ not json");
274
+ expect(() => readCodexHooks(hooksPath)).toThrow(CodexConfigParseError);
275
+ });
276
+ });
277
+ describe("readCodexHookTrust — Codex's per-entry trust records", () => {
278
+ /**
279
+ * The key format is taken verbatim from a real `~/.codex/config.toml`
280
+ * after Codex 0.147 prompted and the answer was persisted. Trust is
281
+ * recorded per hook *entry* — `<path>:<event>:<group>:<index>` — not per
282
+ * file, which an earlier version of this function assumed; matching the
283
+ * bare path found nothing and reported a correctly-approved install as
284
+ * still pending, forever.
285
+ */
286
+ const trustState = (entries) => `[hooks.state]\n${entries
287
+ .map((key) => `\n[hooks.state."${key}"]\ntrusted_hash = "sha256:c3ffbe52"\nenabled = true\n`)
288
+ .join("")}`;
289
+ it("reads a fully approved install", () => {
290
+ writeFileSync(configPath, trustState([
291
+ `${hooksPath}:session_start:0:0`,
292
+ `${hooksPath}:session_end:0:0`,
293
+ ]));
294
+ // `hooksPath` is the temp dir's file, so point the reader at it too.
295
+ process.env.CODEX_HOME = dir;
296
+ try {
297
+ expect(readCodexHookTrust(configPath)).toEqual({
298
+ trusted: ["SessionStart", "SessionEnd"],
299
+ untrusted: [],
300
+ });
301
+ }
302
+ finally {
303
+ delete process.env.CODEX_HOME;
304
+ }
305
+ });
306
+ it("spots the half-approved install that loses repository context", () => {
307
+ // Exactly the state an `async: true` SessionStart left behind on Codex
308
+ // 0.147: the hook was skipped outright, so it was never offered for
309
+ // approval, while SessionEnd was. Sessions close but never open.
310
+ writeFileSync(configPath, trustState([`${hooksPath}:session_end:0:0`]));
311
+ process.env.CODEX_HOME = dir;
312
+ try {
313
+ expect(readCodexHookTrust(configPath)).toEqual({
314
+ trusted: ["SessionEnd"],
315
+ untrusted: ["SessionStart"],
316
+ });
317
+ }
318
+ finally {
319
+ delete process.env.CODEX_HOME;
320
+ }
321
+ });
322
+ it("reads Codex 0.148's hash-only record, which omits `enabled`", () => {
323
+ // 0.148 persists `hooks.state."<key>".trusted_hash` and nothing else —
324
+ // its `HookStateToml` has no `enabled` field. Demanding `enabled = true`
325
+ // reported a just-approved install as still pending, and no amount of
326
+ // restarting Codex could clear it: it never prompts again once trusted.
327
+ writeFileSync(configPath, `[hooks.state."${hooksPath}:session_start:0:0"]\ntrusted_hash = "sha256:c3ffbe52"\n` +
328
+ `\n[hooks.state."${hooksPath}:session_end:0:0"]\ntrusted_hash = "sha256:8b152fa9"\nenabled = true\n`);
329
+ process.env.CODEX_HOME = dir;
330
+ try {
331
+ expect(readCodexHookTrust(configPath)).toEqual({
332
+ trusted: ["SessionStart", "SessionEnd"],
333
+ untrusted: [],
334
+ });
335
+ }
336
+ finally {
337
+ delete process.env.CODEX_HOME;
338
+ }
339
+ });
340
+ it("treats an entry the user explicitly disabled as untrusted", () => {
341
+ writeFileSync(configPath, `[hooks.state."${hooksPath}:session_start:0:0"]\ntrusted_hash = "sha256:c3ffbe52"\nenabled = false\n`);
342
+ process.env.CODEX_HOME = dir;
343
+ try {
344
+ expect(readCodexHookTrust(configPath).untrusted).toEqual([
345
+ "SessionStart",
346
+ "SessionEnd",
347
+ ]);
348
+ }
349
+ finally {
350
+ delete process.env.CODEX_HOME;
351
+ }
352
+ });
353
+ it("reports everything untrusted when there is no config at all", () => {
354
+ expect(readCodexHookTrust(join(dir, "missing.toml")).untrusted).toEqual([
355
+ "SessionStart",
356
+ "SessionEnd",
357
+ ]);
358
+ });
359
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Installs and reads back the OpenCode plugin that reports to Clocktopus.
3
+ *
4
+ * OpenCode is the odd one out. Claude Code and Codex both expose a *hook*
5
+ * — a command they run with JSON on stdin — so wiring them up is a matter
6
+ * of editing config. OpenCode instead exposes a plugin API: JavaScript
7
+ * loaded into its own process, handed a stream of typed events. So the
8
+ * integration is a file of ours rather than a config entry pointing at the
9
+ * CLI.
10
+ *
11
+ * The install is a single file. OpenCode auto-loads everything in
12
+ * `<config dir>/plugin/`, verified against 1.18.18, so nothing has to be
13
+ * added to `opencode.json` — which is worth having: that file is the
14
+ * user's model, provider and permission configuration, and not touching it
15
+ * removes a whole class of ways to break their setup. Uninstalling is
16
+ * deleting the file.
17
+ *
18
+ * ## Why a plugin and not OpenCode's own OpenTelemetry
19
+ *
20
+ * OpenCode has `experimental.openTelemetry`, which exports OTLP/JSON traces
21
+ * to `OTEL_EXPORTER_OTLP_ENDPOINT` — on the face of it exactly what we
22
+ * want, and a one-line install. It was measured and rejected:
23
+ *
24
+ * - **It ships the conversation.** The AI SDK spans carry `ai.prompt`,
25
+ * `ai.prompt.messages` and `ai.response.text` — the system prompt, every
26
+ * user message and the model's replies, verbatim, with no switch to turn
27
+ * them off. Codex's log stream leaks tool output; this leaks everything.
28
+ * - **It is enormous.** One prompt produced 230KB, almost all of it
29
+ * OpenCode's internal spans — SQLite queries, file reads, lock
30
+ * acquisitions — with the AI spans a rounding error inside it.
31
+ * - **It carries no cost and no repository**, so it could not do the one
32
+ * job the feature exists for.
33
+ *
34
+ * The plugin sees a better source than the traces do: `AssistantMessage`
35
+ * carries `cost`, a full token breakdown, the model, and `path.cwd`. It
36
+ * sends numbers and identifiers, and nothing a person wrote.
37
+ */
38
+ export declare const OPENCODE_PLUGIN_FILENAME = "clocktopus.js";
39
+ /**
40
+ * Bumped whenever `buildOpencodePlugin` changes what it emits.
41
+ *
42
+ * OpenCode is the only agent whose integration is *generated source* rather
43
+ * than a command string in a config file. Claude Code and Codex hold
44
+ * `clocktopus agent hook …`, which means whatever the installed CLI means,
45
+ * so upgrading the CLI upgrades them. This plugin does not: the file on
46
+ * disk stays exactly as the CLI that wrote it left it, and a fix shipped to
47
+ * the plugin body reaches nobody until they re-run `agent setup`.
48
+ *
49
+ * Stamping the generation is what makes that visible — `agent doctor`
50
+ * compares this number against the file and says so when they differ.
51
+ * Nothing auto-rewrites the file: it holds a token, and a command that
52
+ * quietly rewrites credentials is worse than one that tells you to.
53
+ *
54
+ * A plugin written before the stamp existed parses as `null`, which reads
55
+ * as stale — correct, since it predates every version that has one.
56
+ */
57
+ export declare const OPENCODE_PLUGIN_VERSION = 1;
58
+ export declare function opencodeConfigDir(): string;
59
+ export declare function opencodePluginPath(): string;
60
+ export type OpencodePluginConfig = {
61
+ endpoint: string;
62
+ token: string;
63
+ /**
64
+ * The hook as argv, not as a command line.
65
+ *
66
+ * `resolveHookCommand` returns a shell-quoted string because that is what
67
+ * `settings.json` and `hooks.json` want — their hosts run it through a
68
+ * shell. This plugin spawns it directly, so it needs the arguments
69
+ * already separated: splitting the string on spaces would tear a quoted
70
+ * path in two the moment anyone installs Node somewhere with a space in
71
+ * it, and the failure would be silent.
72
+ */
73
+ hookArgv: string[];
74
+ };
75
+ /**
76
+ * Splits a shell-quoted command line into argv.
77
+ *
78
+ * Only as clever as `resolveHookCommand` is: double quotes around
79
+ * whitespace, nothing else. It is fed that function's output, never a
80
+ * user's shell.
81
+ */
82
+ export declare function splitCommandLine(command: string): string[];
83
+ /**
84
+ * The plugin source, with this machine's configuration baked into one line.
85
+ *
86
+ * Generated rather than shipped as a file because the token has to be in
87
+ * it: OpenCode gives a plugin no way to read our environment, so the
88
+ * credentials live where every other agent's do — in that agent's config,
89
+ * written 0600.
90
+ */
91
+ export declare function buildOpencodePlugin(config: OpencodePluginConfig): string;
92
+ export declare function readOpencodePlugin(path?: string): {
93
+ path: string;
94
+ exists: boolean;
95
+ modifiedAt: Date | null;
96
+ config: OpencodePluginConfig | null;
97
+ /**
98
+ * The generation that wrote this file, or null when it carries no stamp —
99
+ * either a plugin from before stamping existed, or one hand-edited past
100
+ * recognition. Both mean the same thing to the caller: not current.
101
+ */
102
+ version: number | null;
103
+ };
104
+ export declare function writeOpencodePlugin(source: string, path?: string): {
105
+ backupPath: string | null;
106
+ };
107
+ export declare function removeOpencodePlugin(path?: string): boolean;
108
+ //# sourceMappingURL=opencode-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-config.d.ts","sourceRoot":"","sources":["../../../src/lib/opencode-config.ts"],"names":[],"mappings":"AAaA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,eAAO,MAAM,wBAAwB,kBAAkB,CAAC;AAQxD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wBAAgB,iBAAiB,IAAI,MAAM,CAK1C;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAI1D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAqLxE;AAED,wBAAgB,kBAAkB,CAAC,IAAI,SAAuB,GAAG;IAC/D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,oBAAoB,GAAG,IAAI,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,CA+CA;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,MAAM,EACd,IAAI,SAAuB,GAC1B;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAiB/B;AAED,wBAAgB,oBAAoB,CAAC,IAAI,SAAuB,GAAG,OAAO,CAIzE"}