@loucompanion/forge-bridge 0.1.1-dev.af36c7fa102b → 0.1.1-dev.caea7216568f

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.
@@ -10,4 +10,5 @@ export interface ProviderCliRunResult {
10
10
  readonly summary: string;
11
11
  readonly spec?: RunnerDispatchSpec | null;
12
12
  readonly artifact?: RunnerDispatchArtifact | null;
13
+ readonly artifacts?: readonly RunnerDispatchArtifact[] | null;
13
14
  }
@@ -0,0 +1,25 @@
1
+ import type { RunOnceDispatchPacket, RunnerDispatchEvent } from "../session.types.js";
2
+ export type UsageSource = "provider_reported" | "cli_reported" | "estimated" | "unknown";
3
+ /**
4
+ * Normalized, provider-agnostic token usage accumulated for a dispatch. Cost is never computed
5
+ * here; the API recomputes cost from these tokens x model rates (ADR-0006).
6
+ */
7
+ export interface ProviderUsage {
8
+ readonly inputTokens: number;
9
+ readonly outputTokens: number;
10
+ readonly cacheReadTokens: number;
11
+ readonly cacheCreationTokens: number;
12
+ readonly reasoningOutputTokens: number;
13
+ readonly providerDurationMs: number | null;
14
+ readonly source: UsageSource;
15
+ }
16
+ export declare function emptyUsage(source?: UsageSource): ProviderUsage;
17
+ export declare function addUsage(base: ProviderUsage, next: Partial<ProviderUsage>): ProviderUsage;
18
+ export declare function totalTokens(usage: ProviderUsage): number;
19
+ export declare function hasUsage(usage: ProviderUsage | null): usage is ProviderUsage;
20
+ /**
21
+ * Builds the durable `provider_usage` event. Token numbers travel as a structured `data` JSON
22
+ * string (snake_case) that the API parses into a usage_event row.
23
+ */
24
+ export declare function buildProviderUsageEvent(dispatch: RunOnceDispatchPacket, usage: ProviderUsage): RunnerDispatchEvent;
25
+ export declare function readUsageNumber(source: Record<string, unknown>, key: string): number;
@@ -0,0 +1,64 @@
1
+ export function emptyUsage(source = "unknown") {
2
+ return {
3
+ inputTokens: 0,
4
+ outputTokens: 0,
5
+ cacheReadTokens: 0,
6
+ cacheCreationTokens: 0,
7
+ reasoningOutputTokens: 0,
8
+ providerDurationMs: null,
9
+ source
10
+ };
11
+ }
12
+ export function addUsage(base, next) {
13
+ return {
14
+ inputTokens: base.inputTokens + nonNegative(next.inputTokens),
15
+ outputTokens: base.outputTokens + nonNegative(next.outputTokens),
16
+ cacheReadTokens: base.cacheReadTokens + nonNegative(next.cacheReadTokens),
17
+ cacheCreationTokens: base.cacheCreationTokens + nonNegative(next.cacheCreationTokens),
18
+ reasoningOutputTokens: base.reasoningOutputTokens + nonNegative(next.reasoningOutputTokens),
19
+ providerDurationMs: next.providerDurationMs ?? base.providerDurationMs,
20
+ source: next.source ?? base.source
21
+ };
22
+ }
23
+ export function totalTokens(usage) {
24
+ return usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheCreationTokens;
25
+ }
26
+ export function hasUsage(usage) {
27
+ return usage !== null && totalTokens(usage) > 0;
28
+ }
29
+ /**
30
+ * Builds the durable `provider_usage` event. Token numbers travel as a structured `data` JSON
31
+ * string (snake_case) that the API parses into a usage_event row.
32
+ */
33
+ export function buildProviderUsageEvent(dispatch, usage) {
34
+ const data = {
35
+ provider: dispatch.execution.provider,
36
+ model: dispatch.execution.model,
37
+ lane: dispatch.execution.lane,
38
+ phase: dispatch.cycleRun.phase,
39
+ attempt: dispatch.cycleRun.attempt,
40
+ input_tokens: usage.inputTokens,
41
+ output_tokens: usage.outputTokens,
42
+ cache_read_tokens: usage.cacheReadTokens,
43
+ cache_creation_tokens: usage.cacheCreationTokens,
44
+ reasoning_output_tokens: usage.reasoningOutputTokens,
45
+ metered: dispatch.execution.authKind === "api_key",
46
+ usage_source: usage.source,
47
+ provider_duration_ms: usage.providerDurationMs
48
+ };
49
+ const total = totalTokens(usage);
50
+ return {
51
+ role: "runner",
52
+ kind: "checkpoint",
53
+ content: `Provider usage: ${total} tokens (${usage.inputTokens} in / ${usage.outputTokens} out) for ${dispatch.execution.model}.`,
54
+ eventType: "provider_usage",
55
+ data: JSON.stringify(data)
56
+ };
57
+ }
58
+ export function readUsageNumber(source, key) {
59
+ const value = source[key];
60
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
61
+ }
62
+ function nonNegative(value) {
63
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
64
+ }
@@ -50,28 +50,26 @@ export class BridgeSessionService {
50
50
  const role = dispatch.execution.role.toLowerCase();
51
51
  const isQa = role === "qa" || lane === "qa";
52
52
  const isConflictResolve = role === "conflict_resolve" || lane === "conflict_resolve";
53
+ const context = {
54
+ dispatch,
55
+ worktree,
56
+ emit,
57
+ signal
58
+ };
53
59
  const run = isQa
54
- ? await this.providerCli.runQa(dispatch, emit, signal)
60
+ ? await this.providerCli.runQa(context)
55
61
  : isConflictResolve
56
- ? await this.providerCli.runConflictResolve({
57
- dispatch,
58
- worktree,
59
- emit,
60
- signal
61
- })
62
- : await this.providerCli.runImplementation({
63
- dispatch,
64
- worktree,
65
- emit,
66
- signal
67
- });
62
+ ? await this.providerCli.runConflictResolve(context)
63
+ : await this.providerCli.runImplementation(context);
64
+ const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
68
65
  return redactSecrets({
69
66
  schemaVersion: 1,
70
67
  exitCode: 0,
71
68
  summary: run.summary,
72
69
  events,
73
70
  spec: run.spec ?? null,
74
- artifact: run.artifact ?? null
71
+ artifact: run.artifact ?? null,
72
+ artifacts: phaseArtifacts
75
73
  }, redactionSecrets);
76
74
  }
77
75
  finally {
@@ -18,6 +18,7 @@ export interface DispatchResolvedArtifact {
18
18
  readonly scope: string;
19
19
  readonly body: string;
20
20
  readonly contentHash: string;
21
+ readonly version: number;
21
22
  readonly lane?: string | null;
22
23
  readonly event?: string | null;
23
24
  }
@@ -86,6 +87,10 @@ export interface RunnerDispatchEvent {
86
87
  readonly kind: string;
87
88
  readonly content: string;
88
89
  readonly eventType: string;
90
+ /** Provider/runner-reported event time (ISO-8601) when available, preserved for accurate durations. */
91
+ readonly occurredAt?: string | null;
92
+ /** Optional structured telemetry payload (JSON string), e.g. token usage for provider_usage events. */
93
+ readonly data?: string | null;
89
94
  }
90
95
  export interface RunnerDispatchSpec {
91
96
  readonly repoPath: string;
@@ -101,6 +106,7 @@ export interface RunnerDispatchArtifact {
101
106
  readonly report: string;
102
107
  readonly producedBy: string;
103
108
  readonly status: string;
109
+ readonly phase?: string | null;
104
110
  }
105
111
  export interface RunOnceResultPacket {
106
112
  readonly schemaVersion: 1;
@@ -109,6 +115,7 @@ export interface RunOnceResultPacket {
109
115
  readonly events: readonly RunnerDispatchEvent[];
110
116
  readonly spec?: RunnerDispatchSpec | null;
111
117
  readonly artifact?: RunnerDispatchArtifact | null;
118
+ readonly artifacts?: readonly RunnerDispatchArtifact[] | null;
112
119
  }
113
120
  export interface RunnerEventLine {
114
121
  readonly type: "runner_event";
@@ -153,6 +153,7 @@ function parseArtifacts(value) {
153
153
  scope: requiredString(artifact, "scope"),
154
154
  body: optionalString(artifact, "body") ?? "",
155
155
  contentHash: requiredString(artifact, "contentHash"),
156
+ version: optionalPositiveInteger(artifact, "version") ?? 1,
156
157
  lane: optionalString(artifact, "lane"),
157
158
  event: optionalString(artifact, "event")
158
159
  };
@@ -188,6 +189,16 @@ function requiredBoolean(source, key) {
188
189
  }
189
190
  return value;
190
191
  }
192
+ function optionalPositiveInteger(source, key) {
193
+ const value = source[key];
194
+ if (value === undefined || value === null) {
195
+ return null;
196
+ }
197
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
198
+ throw new Error(`${key} must be a positive integer when provided.`);
199
+ }
200
+ return value;
201
+ }
191
202
  function requiredNumber(source, key) {
192
203
  const value = source[key];
193
204
  if (typeof value !== "number" || !Number.isFinite(value)) {
@@ -0,0 +1,9 @@
1
+ export interface CursorAgentLaunch {
2
+ readonly command: string;
3
+ readonly args: readonly string[];
4
+ }
5
+ export declare function isCursorAgentCommand(command: string): boolean;
6
+ export declare function resolveCursorAgentDirectLaunch(command: string, args: readonly string[]): Promise<CursorAgentLaunch | null>;
7
+ export declare function findLatestCursorAgentVersionDir(versionsRoot: string): Promise<string | null>;
8
+ export declare function compareCursorAgentVersionDirNames(left: string, right: string): number;
9
+ export declare function parseCursorAgentVersionSortKey(name: string): number;
@@ -0,0 +1,66 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ const cursorAgentVersionDirPattern = /^\d{4}\.\d{1,2}\.\d{1,2}-.+/;
4
+ export function isCursorAgentCommand(command) {
5
+ const normalized = command.trim().toLowerCase();
6
+ return normalized === "cursor-agent" || normalized === "agent";
7
+ }
8
+ export async function resolveCursorAgentDirectLaunch(command, args) {
9
+ if (!isCursorAgentCommand(command)) {
10
+ return null;
11
+ }
12
+ const localAppData = process.env.LOCALAPPDATA;
13
+ if (!localAppData) {
14
+ return null;
15
+ }
16
+ const versionDir = await findLatestCursorAgentVersionDir(path.join(localAppData, "cursor-agent", "versions"));
17
+ if (!versionDir) {
18
+ return null;
19
+ }
20
+ const nodePath = path.join(versionDir, "node.exe");
21
+ const indexPath = path.join(versionDir, "index.js");
22
+ if (!(await fileExists(nodePath)) || !(await fileExists(indexPath))) {
23
+ return null;
24
+ }
25
+ return {
26
+ command: nodePath,
27
+ args: [indexPath, ...args]
28
+ };
29
+ }
30
+ export async function findLatestCursorAgentVersionDir(versionsRoot) {
31
+ let entries;
32
+ try {
33
+ entries = await fs.readdir(versionsRoot);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ const latestName = entries
39
+ .filter((name) => cursorAgentVersionDirPattern.test(name))
40
+ .sort(compareCursorAgentVersionDirNames)
41
+ .at(-1);
42
+ return latestName ? path.join(versionsRoot, latestName) : null;
43
+ }
44
+ export function compareCursorAgentVersionDirNames(left, right) {
45
+ const leftKey = parseCursorAgentVersionSortKey(left);
46
+ const rightKey = parseCursorAgentVersionSortKey(right);
47
+ if (leftKey !== rightKey) {
48
+ return leftKey - rightKey;
49
+ }
50
+ return left.localeCompare(right);
51
+ }
52
+ export function parseCursorAgentVersionSortKey(name) {
53
+ const datePart = name.split("-")[0] ?? "";
54
+ const parts = datePart.split(".");
55
+ if (parts.length !== 3) {
56
+ return 0;
57
+ }
58
+ const year = parts[0] ?? "";
59
+ const month = (parts[1] ?? "").padStart(2, "0");
60
+ const day = (parts[2] ?? "").padStart(2, "0");
61
+ const numeric = Number(`${year}${month}${day}`);
62
+ return Number.isFinite(numeric) ? numeric : 0;
63
+ }
64
+ async function fileExists(inputPath) {
65
+ return fs.stat(inputPath).then((stat) => stat.isFile()).catch(() => false);
66
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const forgeBridgeVersion = "0.1.1-dev.af36c7fa102b";
1
+ export declare const forgeBridgeVersion = "0.1.1-dev.caea7216568f";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const forgeBridgeVersion = "0.1.1-dev.af36c7fa102b";
1
+ export const forgeBridgeVersion = "0.1.1-dev.caea7216568f";
@@ -29,10 +29,7 @@ export function selectLaneCommand(artifacts, lane, phase) {
29
29
  return artifacts.find((artifact) => normalizedArtifactKind(artifact.kind) === "command" && artifact.slug.trim().toLowerCase() === slug);
30
30
  }
31
31
  export function resolveCommandSlug(lane, phase) {
32
- const normalizedPhase = phase.trim().toLowerCase();
33
- if (normalizedPhase === "self_review") {
34
- return "self-review-prompt";
35
- }
32
+ const normalizedPhase = phase.trim().toLowerCase().replace(/-/g, "_");
36
33
  if (normalizedPhase === "rework") {
37
34
  return "rework-prompt";
38
35
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loucompanion/forge-bridge",
3
- "version": "0.1.1-dev.af36c7fa102b",
3
+ "version": "0.1.1-dev.caea7216568f",
4
4
  "description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",