@ramarivera/coding-agent-langfuse 0.1.28 → 0.1.30

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/README.md CHANGED
@@ -34,3 +34,50 @@ npx @ramarivera/coding-agent-langfuse@latest \
34
34
 
35
35
  The importer is fail-fast: the first failed OTLP POST stops the run, prints the
36
36
  real network cause, and preserves local state so reruns resume cleanly.
37
+
38
+ ## Follow as a host service
39
+
40
+ Install a live follower directly from npm. The generated service keeps inference
41
+ outside any gateway: agents keep calling their normal providers, while this tool
42
+ tails local histories and posts Langfuse OTLP traces.
43
+
44
+ Preview the service without touching the host:
45
+
46
+ ```sh
47
+ npx @ramarivera/coding-agent-langfuse@latest service print \
48
+ --platform linux \
49
+ --agents codex,pi \
50
+ --endpoint https://langfuse.ai.roxasroot.net/otel/v1/traces
51
+ ```
52
+
53
+ Install and start it on the current host:
54
+
55
+ ```sh
56
+ npx @ramarivera/coding-agent-langfuse@latest service install \
57
+ --agents codex,pi \
58
+ --endpoint https://langfuse.ai.roxasroot.net/otel/v1/traces
59
+ ```
60
+
61
+ The service installer supports:
62
+
63
+ - macOS: LaunchAgent under `~/Library/LaunchAgents`
64
+ - Linux: systemd user unit under `~/.config/systemd/user`
65
+ - Windows: scheduled task installer script under `%APPDATA%\\coding-agent-langfuse`
66
+
67
+ Use `--dry-run` to print the exact file and commands, `--no-start` to only write
68
+ the service file, and `service uninstall` to remove the service registration.
69
+
70
+ ## Backfill windows
71
+
72
+ Backfill only a timeframe when repairing a host or replaying a recent window:
73
+
74
+ ```sh
75
+ npx @ramarivera/coding-agent-langfuse@latest \
76
+ --agents claude,codex,grok,pi,opencode \
77
+ --since 2026-05-31T00:00:00Z \
78
+ --until 2026-06-01T00:00:00Z \
79
+ --endpoint https://langfuse.ai.roxasroot.net/otel/v1/traces
80
+ ```
81
+
82
+ Deduplication is state-file based and keyed by agent, session id, and source
83
+ record id. Reuse the same `--state` path for repeat repairs on a host.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const moduleUrl = new URL("../dist/backfill.js", import.meta.url);
2
+ const moduleUrl = new URL("../dist/cli.js", import.meta.url);
3
3
  const { main } = await import(moduleUrl.href);
4
4
 
5
5
  await main(process.argv.slice(2));
@@ -60,6 +60,8 @@ type FollowSummary = RunSummary & {
60
60
  iterations: number;
61
61
  follow: true;
62
62
  };
63
+ declare const allAgents: AgentName[];
64
+ declare const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
63
65
  declare function parseArgs(argv: string[]): BackfillOptions;
64
66
  declare function codexEvents(homeDir: string): BackfillEvent[];
65
67
  declare function claudeEvents(homeDir: string): BackfillEvent[];
@@ -78,4 +80,4 @@ declare function discoverEvents(options: BackfillOptions): BackfillEvent[];
78
80
  declare function run(options: BackfillOptions): Promise<RunSummary>;
79
81
  declare function follow(options: BackfillOptions): Promise<FollowSummary>;
80
82
  declare function main(argv?: string[]): Promise<RunSummary | FollowSummary>;
81
- export { type BackfillEvent, type BackfillOptions, claudeEvents, codexEvents, discoverEvents, fingerprint, follow, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
83
+ export { type BackfillEvent, type BackfillOptions, type AgentName, allAgents, claudeEvents, codexEvents, defaultEndpoint, discoverEvents, fingerprint, follow, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
package/dist/backfill.js CHANGED
@@ -1203,10 +1203,7 @@ function toOtlp(events, options = {}) {
1203
1203
  const sortedEvents = [...traceEventsForSession].sort((a, b) => a.startMs - b.startMs);
1204
1204
  const traceStartMs = sortedEvents[0]?.startMs ?? Date.now();
1205
1205
  const traceEndMs = Math.max(...sortedEvents.map((event) => event.endMs ?? event.startMs + 1), traceStartMs + 1);
1206
- const firstInputEvent = sortedEvents.find((event) => event.input !== undefined);
1207
- const lastOutputEvent = [...sortedEvents]
1208
- .reverse()
1209
- .find((event) => event.output !== undefined);
1206
+ const shouldEmitRootSpan = sortedEvents.some((event) => event.recordId === "session");
1210
1207
  const rootAttributes = [
1211
1208
  attr("service.name", `agent.${first.agent}`),
1212
1209
  attr("deployment.environment", "local"),
@@ -1226,8 +1223,6 @@ function toOtlp(events, options = {}) {
1226
1223
  attr("langfuse.trace.metadata.machine", currentHost),
1227
1224
  attr("langfuse.trace.metadata.source_path", first.sourcePath),
1228
1225
  attr("langfuse.trace.metadata.cwd", first.cwd),
1229
- attr("langfuse.trace.input", firstInputEvent?.input),
1230
- attr("langfuse.trace.output", lastOutputEvent?.output),
1231
1226
  attr("langfuse.observation.metadata.agent", first.agent),
1232
1227
  attr("langfuse.observation.metadata.host", currentHost),
1233
1228
  attr("langfuse.observation.metadata.machine", currentHost),
@@ -1286,13 +1281,6 @@ function toOtlp(events, options = {}) {
1286
1281
  attr("agent.record_id", event.recordId),
1287
1282
  attr("agent.original_start_time", new Date(event.startMs).toISOString()),
1288
1283
  attr("agent.original_end_time", event.endMs === undefined ? undefined : new Date(event.endMs).toISOString()),
1289
- attr("langfuse.trace.metadata.agent", event.agent),
1290
- attr("langfuse.trace.metadata.host", currentHost),
1291
- attr("langfuse.trace.metadata.machine", currentHost),
1292
- attr("langfuse.trace.metadata.source_path", event.sourcePath),
1293
- attr("langfuse.trace.metadata.cwd", event.cwd),
1294
- attr("langfuse.trace.metadata.model", event.model),
1295
- attr("langfuse.trace.metadata.provider", event.provider),
1296
1284
  attr("langfuse.observation.metadata.agent", event.agent),
1297
1285
  attr("langfuse.observation.metadata.host", currentHost),
1298
1286
  attr("langfuse.observation.metadata.machine", currentHost),
@@ -1303,8 +1291,6 @@ function toOtlp(events, options = {}) {
1303
1291
  attr("langfuse.observation.metadata.model", modelName ?? event.model),
1304
1292
  attr("langfuse.observation.metadata.provider", event.provider),
1305
1293
  attr("langfuse.observation.metadata.cost_source", cost?.source),
1306
- attr("langfuse.trace.input", event.input),
1307
- attr("langfuse.trace.output", event.output),
1308
1294
  attr("langfuse.observation.input", event.input),
1309
1295
  attr("langfuse.observation.output", event.output),
1310
1296
  attr("source.path", event.sourcePath),
@@ -1331,7 +1317,7 @@ function toOtlp(events, options = {}) {
1331
1317
  status: { code: 1 },
1332
1318
  };
1333
1319
  });
1334
- return [rootSpan, ...childSpans];
1320
+ return shouldEmitRootSpan ? [rootSpan, ...childSpans] : childSpans;
1335
1321
  });
1336
1322
  return {
1337
1323
  resourceSpans: [
@@ -1607,4 +1593,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
1607
1593
  process.exit(1);
1608
1594
  }
1609
1595
  }
1610
- export { claudeEvents, codexEvents, discoverEvents, fingerprint, follow, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
1596
+ export { allAgents, claudeEvents, codexEvents, defaultEndpoint, discoverEvents, fingerprint, follow, grokEvents, main, opencodeEvents, parseArgs, piEvents, run, toOtlp, };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ declare function main(argv?: string[]): Promise<void>;
3
+ export { main };
package/dist/cli.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { main as backfillMain } from "./backfill.js";
3
+ import { serviceMain } from "./service.js";
4
+ async function main(argv = process.argv.slice(2)) {
5
+ if (argv[0] === "service") {
6
+ await serviceMain(argv.slice(1));
7
+ return;
8
+ }
9
+ await backfillMain(argv);
10
+ }
11
+ await main();
12
+ export { main };
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { codexEvents, discoverEvents, fingerprint, parseArgs, piEvents, run, toOtlp, } from "./backfill.js";
2
+ export { buildServicePlan, parseServiceArgs, serviceMain, } from "./service.js";
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
1
  export { codexEvents, discoverEvents, fingerprint, parseArgs, piEvents, run, toOtlp, } from "./backfill.js";
2
+ export { buildServicePlan, parseServiceArgs, serviceMain, } from "./service.js";
@@ -0,0 +1,35 @@
1
+ type AgentName = "claude" | "codex" | "grok" | "opencode" | "pi";
2
+ type ServicePlatform = "darwin" | "linux" | "win32";
3
+ type ServiceAction = "install" | "uninstall" | "print";
4
+ type ServiceOptions = {
5
+ action: ServiceAction;
6
+ platform: ServicePlatform;
7
+ agents: AgentName[];
8
+ endpoint: string;
9
+ statePath: string;
10
+ homeDir: string;
11
+ name: string;
12
+ packageSpec: string;
13
+ batchSize: number;
14
+ pollIntervalMs: number;
15
+ postDelayMs: number;
16
+ since?: string;
17
+ dryRun: boolean;
18
+ start: boolean;
19
+ workingDirectory: string;
20
+ pathEnv: string;
21
+ };
22
+ type ServicePlan = {
23
+ platform: ServicePlatform;
24
+ action: ServiceAction;
25
+ name: string;
26
+ path: string;
27
+ content?: string;
28
+ command: string[];
29
+ postInstallCommands: string[][];
30
+ uninstallCommands: string[][];
31
+ };
32
+ declare function parseServiceArgs(argv: string[]): ServiceOptions;
33
+ declare function buildServicePlan(options: ServiceOptions): ServicePlan;
34
+ declare function serviceMain(argv?: string[]): Promise<ServicePlan>;
35
+ export { type ServiceOptions, type ServicePlan, buildServicePlan, parseServiceArgs, serviceMain, };
@@ -0,0 +1,422 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync, } from "node:fs";
3
+ import { homedir, platform as osPlatform } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ const defaultPackageSpec = "@ramarivera/coding-agent-langfuse@latest";
6
+ const defaultEndpoint = "https://langfuse.ai.roxasroot.net/otel/v1/traces";
7
+ const allAgents = ["claude", "codex", "grok", "opencode", "pi"];
8
+ function serviceUsage() {
9
+ return `Usage:
10
+ coding-agent-langfuse service install [options]
11
+ coding-agent-langfuse service print [options]
12
+ coding-agent-langfuse service uninstall [options]
13
+
14
+ Service options:
15
+ --platform NAME Target platform: darwin, linux, win32 (default: current)
16
+ --name NAME Service name/label/unit name
17
+ --agents LIST Comma-separated agents: claude,codex,grok,opencode,pi
18
+ --endpoint URL OTLP HTTP traces endpoint (default: ${defaultEndpoint})
19
+ --state PATH Dedupe state file
20
+ --home PATH Home directory to scan (default: current user home)
21
+ --package-spec SPEC npx package spec (default: ${defaultPackageSpec})
22
+ --batch-size N OTLP spans per POST (default: 10)
23
+ --poll-interval-ms N Delay between --follow scans (default: 5000)
24
+ --post-delay-ms N Delay after each successful OTLP POST (default: 0)
25
+ --since ISO_OR_MS Optional lower bound for events the follower may send
26
+ --working-directory DIR Directory the service starts in (default: --home)
27
+ --path VALUE PATH value injected into the service environment
28
+ --dry-run Print the service plan without writing or running commands
29
+ --no-start Write the service but do not enable/start it
30
+ --help Show this help
31
+ `;
32
+ }
33
+ function parseServiceArgs(argv) {
34
+ const action = parseServiceAction(argv[0]);
35
+ if (argv.includes("--help") || argv.includes("-h")) {
36
+ console.log(serviceUsage());
37
+ process.exit(0);
38
+ }
39
+ let platform = currentServicePlatform();
40
+ let agents = [...allAgents];
41
+ let endpoint = process.env.LANGFUSE_BACKFILL_ENDPOINT ?? defaultEndpoint;
42
+ let homeDir = process.env.HOME ?? homedir();
43
+ let statePath = "";
44
+ let name = "";
45
+ let packageSpec = defaultPackageSpec;
46
+ let batchSize = 10;
47
+ let pollIntervalMs = 5_000;
48
+ let postDelayMs = 0;
49
+ let since;
50
+ let dryRun = false;
51
+ let start = true;
52
+ let workingDirectory = "";
53
+ let pathEnv = "";
54
+ for (let i = 1; i < argv.length; i++) {
55
+ const arg = argv[i];
56
+ const next = () => {
57
+ const value = argv[++i];
58
+ if (!value)
59
+ throw new Error(`Missing value for ${arg}`);
60
+ return value;
61
+ };
62
+ if (arg === "--platform") {
63
+ platform = parsePlatform(next());
64
+ }
65
+ else if (arg === "--name") {
66
+ name = next();
67
+ }
68
+ else if (arg === "--agents") {
69
+ agents = parseAgents(next());
70
+ }
71
+ else if (arg === "--endpoint") {
72
+ endpoint = next();
73
+ }
74
+ else if (arg === "--state") {
75
+ statePath = next();
76
+ }
77
+ else if (arg === "--home") {
78
+ homeDir = next();
79
+ }
80
+ else if (arg === "--package-spec") {
81
+ packageSpec = next();
82
+ }
83
+ else if (arg === "--batch-size") {
84
+ batchSize = parsePositiveInt(arg, next());
85
+ }
86
+ else if (arg === "--poll-interval-ms") {
87
+ pollIntervalMs = parsePositiveInt(arg, next());
88
+ }
89
+ else if (arg === "--post-delay-ms") {
90
+ postDelayMs = parseNonNegativeInt(arg, next());
91
+ }
92
+ else if (arg === "--since") {
93
+ since = next();
94
+ }
95
+ else if (arg === "--working-directory") {
96
+ workingDirectory = next();
97
+ }
98
+ else if (arg === "--path") {
99
+ pathEnv = next();
100
+ }
101
+ else if (arg === "--dry-run") {
102
+ dryRun = true;
103
+ }
104
+ else if (arg === "--no-start") {
105
+ start = false;
106
+ }
107
+ else {
108
+ throw new Error(`Unknown service argument '${arg}'`);
109
+ }
110
+ }
111
+ if (agents.length === 0)
112
+ throw new Error("--agents must include at least one agent");
113
+ name ||= defaultServiceName(agents, platform);
114
+ workingDirectory ||= homeDir;
115
+ pathEnv ||= defaultPathEnv(homeDir, platform);
116
+ statePath ||= join(homeDir, ".local/state/coding-agent-langfuse", `${name}.json`);
117
+ return {
118
+ action,
119
+ platform,
120
+ agents,
121
+ endpoint,
122
+ statePath,
123
+ homeDir,
124
+ name,
125
+ packageSpec,
126
+ batchSize,
127
+ pollIntervalMs,
128
+ postDelayMs,
129
+ since,
130
+ dryRun,
131
+ start,
132
+ workingDirectory,
133
+ pathEnv,
134
+ };
135
+ }
136
+ function buildServicePlan(options) {
137
+ const command = buildFollowCommand(options);
138
+ if (options.platform === "darwin") {
139
+ const path = join(options.homeDir, "Library/LaunchAgents", `${options.name}.plist`);
140
+ return {
141
+ platform: options.platform,
142
+ action: options.action,
143
+ name: options.name,
144
+ path,
145
+ content: renderLaunchdPlist(options, command),
146
+ command,
147
+ postInstallCommands: options.start
148
+ ? [
149
+ ["launchctl", "bootstrap", `gui/${process.getuid?.() ?? 501}`, path],
150
+ ["launchctl", "kickstart", "-k", `gui/${process.getuid?.() ?? 501}/${options.name}`],
151
+ ]
152
+ : [],
153
+ uninstallCommands: [
154
+ ["launchctl", "bootout", `gui/${process.getuid?.() ?? 501}`, path],
155
+ ],
156
+ };
157
+ }
158
+ if (options.platform === "linux") {
159
+ const path = join(options.homeDir, ".config/systemd/user", `${options.name}.service`);
160
+ return {
161
+ platform: options.platform,
162
+ action: options.action,
163
+ name: options.name,
164
+ path,
165
+ content: renderSystemdUnit(options, command),
166
+ command,
167
+ postInstallCommands: options.start
168
+ ? [
169
+ ["systemctl", "--user", "daemon-reload"],
170
+ ["systemctl", "--user", "enable", "--now", `${options.name}.service`],
171
+ ]
172
+ : [["systemctl", "--user", "daemon-reload"]],
173
+ uninstallCommands: [
174
+ ["systemctl", "--user", "disable", "--now", `${options.name}.service`],
175
+ ["systemctl", "--user", "daemon-reload"],
176
+ ],
177
+ };
178
+ }
179
+ const path = join(process.env.APPDATA ?? join(options.homeDir, "AppData/Roaming"), "coding-agent-langfuse", `${options.name}.ps1`);
180
+ const taskName = `CodingAgentLangfuse-${options.name}`;
181
+ return {
182
+ platform: options.platform,
183
+ action: options.action,
184
+ name: options.name,
185
+ path,
186
+ content: renderWindowsScript(command),
187
+ command,
188
+ postInstallCommands: options.start
189
+ ? [
190
+ [
191
+ "powershell.exe",
192
+ "-NoProfile",
193
+ "-ExecutionPolicy",
194
+ "Bypass",
195
+ "-File",
196
+ path,
197
+ "-Install",
198
+ "-TaskName",
199
+ taskName,
200
+ ],
201
+ ]
202
+ : [],
203
+ uninstallCommands: [
204
+ [
205
+ "powershell.exe",
206
+ "-NoProfile",
207
+ "-Command",
208
+ `Unregister-ScheduledTask -TaskName ${powershellString(taskName)} -Confirm:$false -ErrorAction SilentlyContinue`,
209
+ ],
210
+ ],
211
+ };
212
+ }
213
+ async function serviceMain(argv = process.argv.slice(2)) {
214
+ const options = parseServiceArgs(argv);
215
+ const plan = buildServicePlan(options);
216
+ if (options.action === "print" || options.dryRun) {
217
+ console.log(JSON.stringify(plan, null, 2));
218
+ return plan;
219
+ }
220
+ if (options.action === "install") {
221
+ if (!plan.content)
222
+ throw new Error("Service plan is missing content");
223
+ mkdirSync(dirname(plan.path), { recursive: true });
224
+ writeFileSync(plan.path, plan.content);
225
+ runCommands(plan.postInstallCommands);
226
+ console.log(JSON.stringify(plan, null, 2));
227
+ return plan;
228
+ }
229
+ runCommands(plan.uninstallCommands, { ignoreFailure: true });
230
+ if (existsSync(plan.path))
231
+ rmSync(plan.path);
232
+ console.log(JSON.stringify(plan, null, 2));
233
+ return plan;
234
+ }
235
+ function buildFollowCommand(options) {
236
+ const command = [
237
+ options.platform === "win32" ? "npx.cmd" : "npx",
238
+ "--yes",
239
+ options.packageSpec,
240
+ "--agents",
241
+ options.agents.join(","),
242
+ "--endpoint",
243
+ options.endpoint,
244
+ "--state",
245
+ options.statePath,
246
+ "--home",
247
+ options.homeDir,
248
+ "--batch-size",
249
+ String(options.batchSize),
250
+ "--poll-interval-ms",
251
+ String(options.pollIntervalMs),
252
+ "--post-delay-ms",
253
+ String(options.postDelayMs),
254
+ "--follow",
255
+ ];
256
+ if (options.since)
257
+ command.push("--since", options.since);
258
+ return command;
259
+ }
260
+ function renderSystemdUnit(options, command) {
261
+ return `[Unit]
262
+ Description=Coding Agent Langfuse follower (${options.agents.join(",")})
263
+ After=network-online.target
264
+ Wants=network-online.target
265
+
266
+ [Service]
267
+ Type=simple
268
+ ExecStart=${systemdCommand(command)}
269
+ Restart=always
270
+ RestartSec=15
271
+ StartLimitIntervalSec=60
272
+ StartLimitBurst=10
273
+ WorkingDirectory=${systemdQuote(options.workingDirectory)}
274
+ Environment=${systemdQuote(`PATH=${options.pathEnv}`)}
275
+ Environment=LANGFUSE_BACKFILL_ENDPOINT=${options.endpoint}
276
+
277
+ [Install]
278
+ WantedBy=default.target
279
+ `;
280
+ }
281
+ function renderLaunchdPlist(options, command) {
282
+ return `<?xml version="1.0" encoding="UTF-8"?>
283
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
284
+ <plist version="1.0">
285
+ <dict>
286
+ <key>Label</key>
287
+ <string>${escapeXml(options.name)}</string>
288
+ <key>ProgramArguments</key>
289
+ <array>
290
+ ${command.map((part) => ` <string>${escapeXml(part)}</string>`).join("\n")}
291
+ </array>
292
+ <key>EnvironmentVariables</key>
293
+ <dict>
294
+ <key>PATH</key>
295
+ <string>${escapeXml(options.pathEnv)}</string>
296
+ <key>LANGFUSE_BACKFILL_ENDPOINT</key>
297
+ <string>${escapeXml(options.endpoint)}</string>
298
+ </dict>
299
+ <key>WorkingDirectory</key>
300
+ <string>${escapeXml(options.workingDirectory)}</string>
301
+ <key>RunAtLoad</key>
302
+ <true/>
303
+ <key>KeepAlive</key>
304
+ <true/>
305
+ <key>StandardOutPath</key>
306
+ <string>${escapeXml(join(options.homeDir, "Library/Logs", `${options.name}.out.log`))}</string>
307
+ <key>StandardErrorPath</key>
308
+ <string>${escapeXml(join(options.homeDir, "Library/Logs", `${options.name}.err.log`))}</string>
309
+ </dict>
310
+ </plist>
311
+ `;
312
+ }
313
+ function renderWindowsScript(command) {
314
+ const commandArray = command.map(powershellString).join(", ");
315
+ return `param(
316
+ [switch]$Install,
317
+ [string]$TaskName = "CodingAgentLangfuse"
318
+ )
319
+
320
+ $Command = @(${commandArray})
321
+ $Action = New-ScheduledTaskAction -Execute $Command[0] -Argument (($Command | Select-Object -Skip 1) -join " ")
322
+ $Trigger = New-ScheduledTaskTrigger -AtLogOn
323
+ $Settings = New-ScheduledTaskSettingsSet -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)
324
+
325
+ if ($Install) {
326
+ Register-ScheduledTask -TaskName $TaskName -Action $Action -Trigger $Trigger -Settings $Settings -Force | Out-Null
327
+ Start-ScheduledTask -TaskName $TaskName
328
+ }
329
+ `;
330
+ }
331
+ function parseServiceAction(value) {
332
+ if (value === "install" || value === "uninstall" || value === "print")
333
+ return value;
334
+ throw new Error(`Expected service action install, uninstall, or print; got '${value ?? ""}'`);
335
+ }
336
+ function parsePlatform(value) {
337
+ if (value === "darwin" || value === "linux" || value === "win32")
338
+ return value;
339
+ throw new Error(`Unsupported platform '${value}'`);
340
+ }
341
+ function currentServicePlatform() {
342
+ return parsePlatform(osPlatform());
343
+ }
344
+ function parseAgents(value) {
345
+ return value.split(",").map((item) => {
346
+ const agent = item.trim();
347
+ if (!allAgents.includes(agent))
348
+ throw new Error(`Unknown agent '${item}'`);
349
+ return agent;
350
+ });
351
+ }
352
+ function defaultServiceName(agents, platform) {
353
+ const suffix = agents.join("-");
354
+ return platform === "darwin"
355
+ ? `net.roxasroot.coding-agent-langfuse.${suffix}`
356
+ : `coding-agent-langfuse-${suffix}`;
357
+ }
358
+ function defaultPathEnv(homeDir, platform) {
359
+ if (platform === "win32") {
360
+ return [
361
+ "%APPDATA%\\npm",
362
+ "%ProgramFiles%\\nodejs",
363
+ "%SystemRoot%\\System32",
364
+ "%SystemRoot%",
365
+ ].join(";");
366
+ }
367
+ return [
368
+ join(homeDir, ".local/share/mise/shims"),
369
+ join(homeDir, ".local/bin"),
370
+ "/opt/homebrew/bin",
371
+ "/usr/local/bin",
372
+ "/usr/bin",
373
+ "/bin",
374
+ "/usr/sbin",
375
+ "/sbin",
376
+ ].join(":");
377
+ }
378
+ function parsePositiveInt(flag, value) {
379
+ const parsed = Number.parseInt(value, 10);
380
+ if (!Number.isFinite(parsed) || parsed < 1) {
381
+ throw new Error(`${flag} must be a positive integer`);
382
+ }
383
+ return parsed;
384
+ }
385
+ function parseNonNegativeInt(flag, value) {
386
+ const parsed = Number.parseInt(value, 10);
387
+ if (!Number.isFinite(parsed) || parsed < 0) {
388
+ throw new Error(`${flag} must be a non-negative integer`);
389
+ }
390
+ return parsed;
391
+ }
392
+ function runCommands(commands, options = {}) {
393
+ for (const command of commands) {
394
+ try {
395
+ execFileSync(command[0], command.slice(1), { stdio: "inherit" });
396
+ }
397
+ catch (error) {
398
+ if (!options.ignoreFailure)
399
+ throw error;
400
+ }
401
+ }
402
+ }
403
+ function systemdCommand(command) {
404
+ return ["/usr/bin/env", ...command].map(systemdQuote).join(" ");
405
+ }
406
+ function systemdQuote(value) {
407
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value))
408
+ return value;
409
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
410
+ }
411
+ function powershellString(value) {
412
+ return `'${value.replaceAll("'", "''")}'`;
413
+ }
414
+ function escapeXml(value) {
415
+ return value
416
+ .replaceAll("&", "&amp;")
417
+ .replaceAll("<", "&lt;")
418
+ .replaceAll(">", "&gt;")
419
+ .replaceAll('"', "&quot;")
420
+ .replaceAll("'", "&apos;");
421
+ }
422
+ export { buildServicePlan, parseServiceArgs, serviceMain, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramarivera/coding-agent-langfuse",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Universal coding-agent Langfuse backfiller and live OTLP helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "exports": {
13
13
  ".": "./dist/index.js",
14
- "./backfill": "./dist/backfill.js"
14
+ "./backfill": "./dist/backfill.js",
15
+ "./service": "./dist/service.js"
15
16
  },
16
17
  "files": [
17
18
  "bin",
@@ -23,7 +24,7 @@
23
24
  "build": "tsc -p tsconfig.build.json",
24
25
  "check": "tsc --noEmit",
25
26
  "test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types --test test/**/*.test.ts",
26
- "test:e2e": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types --test e2e/test/**/*.test.ts",
27
+ "test:e2e": "npm run build && node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --experimental-strip-types --test e2e/test/**/*.test.ts",
27
28
  "pack:dry-run": "npm pack --dry-run",
28
29
  "prepack": "npm run build"
29
30
  },