@xpufx/paseo-top 0.4.4 → 0.4.7
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/OVERVIEW.md +18 -0
- package/client/host-ui.tsx +184 -0
- package/client/multi-host-view.tsx +57 -43
- package/client/multi-host.ts +36 -13
- package/client/pill-labels.ts +94 -1
- package/client/pill.tsx +692 -616
- package/client/resources-query.ts +1 -1
- package/client/settings-events.ts +1 -1
- package/client/settings-ui.tsx +86 -9
- package/client/surface.tsx +308 -276
- package/client/telemetry-copy.ts +8 -2
- package/client/telemetry.tsx +372 -321
- package/client/turn-panel.tsx +126 -104
- package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +9 -3
- package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
- package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
- package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
- package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
- package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
- package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
- package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
- package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
- package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
- package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +51 -18
- package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
- package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
- package/client/vendor/paseo-plugin-helper/{surface.tsx → lifecycle/surface.tsx} +6 -6
- package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
- package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
- package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
- package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +50 -58
- package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
- package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
- package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
- package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
- package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
- package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
- package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
- package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
- package/client/vendor/permission-audit/client.tsx +706 -0
- package/client/vendor/permission-audit/index.tsx +1 -0
- package/index.client.tsx +58 -37
- package/index.server.ts +256 -234
- package/npm-shrinkwrap.json +44 -0
- package/package.json +8 -4
- package/paseo-plugin.json +2 -0
- package/screenshots/1-paseo-top-modal-system.jpg +0 -0
- package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
- package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
- package/screenshots/4-paseo-top-modal-about.jpg +0 -0
- package/screenshots/paseo-top-dashboard.jpg +0 -0
- package/screenshots/paseo-top-general-view.jpg +0 -0
- package/server/resources.ts +98 -0
- package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
- package/server/vendor/paseo-plugin-helper/index.ts +2 -0
- package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
- package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
- package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
- package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
- package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
- package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
- package/server/vendor/permission-audit/index.ts +1 -0
- package/server/vendor/permission-audit/server.ts +839 -0
- package/shared/helper-version.ts +1 -1
- package/shared/multi-host.ts +153 -159
- package/shared/resources.ts +145 -0
- package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
- package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
- package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
- package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
- package/shared/vendor/permission-audit/filter.ts +61 -0
- package/shared/vendor/permission-audit/index.ts +2 -0
- package/shared/vendor/permission-audit/shared.ts +119 -0
- package/shared/version.ts +1 -1
- package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
- package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
- package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
- package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
- package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
- package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
- package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
- package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
- package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
- package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
- package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
- package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
- package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
- package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
- package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
- package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
- package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
- package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
- package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
- package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
- package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/index.ts +0 -30
- package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
- package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
- package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
- package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
- package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
- package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
- package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
- package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
- package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
- package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
- package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
- package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
- package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
- package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
- package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
- package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
- /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
- /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
- /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
|
@@ -7,7 +7,7 @@ export interface PluginStorageOptions<T> {
|
|
|
7
7
|
defaultData?: T;
|
|
8
8
|
/**
|
|
9
9
|
* Base directory override. When provided, storage is located at path.join(baseDir, pluginId).
|
|
10
|
-
* When omitted, defaults to path.join(
|
|
10
|
+
* When omitted, defaults to path.join(resolvePaseoHome(), namespace ?? "plugin-data/xpufx", pluginId).
|
|
11
11
|
*/
|
|
12
12
|
baseDir?: string;
|
|
13
13
|
/**
|
|
@@ -27,6 +27,40 @@ export interface StorageStats {
|
|
|
27
27
|
lastModified: Date | null;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
const AGENT_MUX_PROFILE_HOME_RE = /(?:^|[\\/])\.agent-mux[\\/]profiles[\\/]/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the real user home even when `HOME` points into an agent-mux profile.
|
|
34
|
+
* A profile session has `HOME=~/.agent-mux/profiles/<provider>/<profile>` while
|
|
35
|
+
* the live Paseo state stays on the host home. agent-mux exports `REAL_HOME` for
|
|
36
|
+
* every profile session, so prefer it; otherwise strip the profile prefix so
|
|
37
|
+
* paths under `~/.paseo` still resolve to the host (#1158, #973).
|
|
38
|
+
*/
|
|
39
|
+
export function resolveHostHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
40
|
+
const home = (env.HOME || os.homedir() || "").trim();
|
|
41
|
+
if (!home || !AGENT_MUX_PROFILE_HOME_RE.test(home)) {
|
|
42
|
+
return home;
|
|
43
|
+
}
|
|
44
|
+
const realHome = (env.REAL_HOME || "").trim();
|
|
45
|
+
if (realHome) return realHome;
|
|
46
|
+
const marker = home.match(AGENT_MUX_PROFILE_HOME_RE);
|
|
47
|
+
if (marker && typeof marker.index === "number" && marker.index > 0) {
|
|
48
|
+
return home.slice(0, marker.index);
|
|
49
|
+
}
|
|
50
|
+
return home;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the canonical `~/.paseo` directory. `PASEO_HOME` is the daemon's own
|
|
55
|
+
* declaration of that root, so prefer it; otherwise anchor to the host home
|
|
56
|
+
* rather than an agent-mux profile (#1158).
|
|
57
|
+
*/
|
|
58
|
+
export function resolvePaseoHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
59
|
+
const paseoHome = (env.PASEO_HOME || "").trim();
|
|
60
|
+
if (paseoHome) return paseoHome;
|
|
61
|
+
return path.join(resolveHostHome(env), ".paseo");
|
|
62
|
+
}
|
|
63
|
+
|
|
30
64
|
export const DEFAULT_NAMESPACE_README = `# Paseo Plugins Storage (xpufx)
|
|
31
65
|
|
|
32
66
|
This directory is managed by \`paseo-plugin-helper\` to store persistent settings, cached metrics, and state for plugins.
|
|
@@ -60,7 +94,7 @@ export class PluginStorage<T extends Record<string, any>> {
|
|
|
60
94
|
this.namespaceDir = options.baseDir;
|
|
61
95
|
this.pluginDir = path.join(options.baseDir, pluginId);
|
|
62
96
|
} else {
|
|
63
|
-
this.namespaceDir = path.join(
|
|
97
|
+
this.namespaceDir = path.join(resolvePaseoHome(), namespace);
|
|
64
98
|
this.pluginDir = path.join(this.namespaceDir, pluginId);
|
|
65
99
|
}
|
|
66
100
|
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { FeatureModule, FeatureDisposer } from "../../../shared/vendor/paseo-plugin-helper/features";
|
|
3
|
+
import type { PluginRpcContract } from "../../../shared/vendor/paseo-plugin-helper/rpc";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Raw usage structure as emitted by various Paseo agent engines and providers.
|
|
7
|
+
*/
|
|
8
|
+
export interface RawTurnUsage {
|
|
9
|
+
inputTokens?: number;
|
|
10
|
+
outputTokens?: number;
|
|
11
|
+
cachedTokens?: number;
|
|
12
|
+
cachedInputTokens?: number;
|
|
13
|
+
reasoningTokens?: number;
|
|
14
|
+
contextWindowUsedTokens?: number;
|
|
15
|
+
contextWindowMaxTokens?: number;
|
|
16
|
+
totalCostUsd?: number;
|
|
17
|
+
/** Claude Code SDK nested iterations array */
|
|
18
|
+
iterations?: Array<{
|
|
19
|
+
input_tokens?: number;
|
|
20
|
+
cache_creation_input_tokens?: number;
|
|
21
|
+
cache_read_input_tokens?: number;
|
|
22
|
+
output_tokens?: number;
|
|
23
|
+
}>;
|
|
24
|
+
/** OpenCode / ACP tokens object */
|
|
25
|
+
tokens?: {
|
|
26
|
+
input?: number;
|
|
27
|
+
output?: number;
|
|
28
|
+
reasoning?: number;
|
|
29
|
+
cache_read?: number;
|
|
30
|
+
};
|
|
31
|
+
[key: string]: unknown;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RawTurnOutcome {
|
|
35
|
+
kind: "completed" | "failed" | "canceled" | string;
|
|
36
|
+
error?: { message: string; code?: string };
|
|
37
|
+
reason?: string;
|
|
38
|
+
stopReason?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RawTurnData {
|
|
42
|
+
turnId?: string | null;
|
|
43
|
+
agentId: string;
|
|
44
|
+
repo?: string | null;
|
|
45
|
+
role?: string | null;
|
|
46
|
+
model?: string | null;
|
|
47
|
+
provider?: string | null;
|
|
48
|
+
outcome?: RawTurnOutcome;
|
|
49
|
+
durationMs?: number;
|
|
50
|
+
timeline?: readonly unknown[];
|
|
51
|
+
liveUsage?: RawTurnUsage | null;
|
|
52
|
+
gitBefore?: { insertions: number; deletions: number; filesChanged: number } | null;
|
|
53
|
+
gitAfter?: { insertions: number; deletions: number; filesChanged: number } | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const NormalizedTurnTelemetrySchema = z.object({
|
|
57
|
+
turnId: z.string().nullable(),
|
|
58
|
+
agentId: z.string(),
|
|
59
|
+
repo: z.string().nullable(),
|
|
60
|
+
role: z.string().nullable(),
|
|
61
|
+
model: z.string().nullable(),
|
|
62
|
+
provider: z.string().nullable(),
|
|
63
|
+
timestamp: z.string(),
|
|
64
|
+
durationMs: z.number(),
|
|
65
|
+
inputTokens: z.number(),
|
|
66
|
+
outputTokens: z.number(),
|
|
67
|
+
cachedTokens: z.number(),
|
|
68
|
+
reasoningTokens: z.number(),
|
|
69
|
+
totalTokens: z.number(),
|
|
70
|
+
tokensPerSecond: z.number(),
|
|
71
|
+
contextUsedTokens: z.number(),
|
|
72
|
+
contextMaxTokens: z.number(),
|
|
73
|
+
contextUtilizationPct: z.number(),
|
|
74
|
+
costUsd: z.number(),
|
|
75
|
+
toolCallsCount: z.number(),
|
|
76
|
+
toolErrorsCount: z.number(),
|
|
77
|
+
finishReason: z.string(),
|
|
78
|
+
stopReason: z.string().optional(),
|
|
79
|
+
errorMessage: z.string().optional(),
|
|
80
|
+
gitInsertions: z.number(),
|
|
81
|
+
gitDeletions: z.number(),
|
|
82
|
+
gitFilesChanged: z.number(),
|
|
83
|
+
isInterrupted: z.boolean(),
|
|
84
|
+
isSubscriptionZeroCost: z.boolean().optional(),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export type NormalizedTurnTelemetry = z.infer<typeof NormalizedTurnTelemetrySchema>;
|
|
88
|
+
|
|
89
|
+
function finite(val: unknown, fallback = 0): number {
|
|
90
|
+
return typeof val === "number" && Number.isFinite(val) ? val : fallback;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function round2(val: number): number {
|
|
94
|
+
return Math.round(val * 100) / 100;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Normalizes vendor/provider quirks into a canonical TurnTelemetry record:
|
|
99
|
+
* - Claude nested iterations array accumulation
|
|
100
|
+
* - OpenCode isolated reasoning tokens
|
|
101
|
+
* - ACP asynchronous context meter readings
|
|
102
|
+
* - Interrupted / canceled turn normalization (prevents division by zero)
|
|
103
|
+
* - Subscription $0 cost normalization
|
|
104
|
+
*/
|
|
105
|
+
export function normalizeTurnTelemetry(
|
|
106
|
+
raw: RawTurnData,
|
|
107
|
+
now = Date.now(),
|
|
108
|
+
): NormalizedTurnTelemetry {
|
|
109
|
+
const usage = raw.liveUsage;
|
|
110
|
+
let inputTokens = 0;
|
|
111
|
+
let outputTokens = 0;
|
|
112
|
+
let cachedTokens = 0;
|
|
113
|
+
let reasoningTokens = 0;
|
|
114
|
+
let contextUsedTokens = 0;
|
|
115
|
+
let contextMaxTokens = 0;
|
|
116
|
+
let costUsd = 0;
|
|
117
|
+
|
|
118
|
+
if (usage) {
|
|
119
|
+
if (Array.isArray(usage.iterations) && usage.iterations.length > 0) {
|
|
120
|
+
// Claude nested iterations normalization
|
|
121
|
+
let iterInput = 0;
|
|
122
|
+
let iterOutput = 0;
|
|
123
|
+
let iterCache = 0;
|
|
124
|
+
for (const iter of usage.iterations) {
|
|
125
|
+
iterInput += finite(iter.input_tokens);
|
|
126
|
+
iterCache += finite(iter.cache_read_input_tokens) + finite(iter.cache_creation_input_tokens);
|
|
127
|
+
iterOutput += finite(iter.output_tokens);
|
|
128
|
+
}
|
|
129
|
+
inputTokens = iterInput;
|
|
130
|
+
cachedTokens = iterCache;
|
|
131
|
+
outputTokens = iterOutput;
|
|
132
|
+
} else {
|
|
133
|
+
inputTokens = finite(usage.inputTokens ?? usage.tokens?.input);
|
|
134
|
+
outputTokens = finite(usage.outputTokens ?? usage.tokens?.output);
|
|
135
|
+
cachedTokens = finite(usage.cachedTokens ?? usage.cachedInputTokens ?? usage.tokens?.cache_read);
|
|
136
|
+
reasoningTokens = finite(usage.reasoningTokens ?? usage.tokens?.reasoning);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (reasoningTokens === 0 && usage.tokens?.reasoning) {
|
|
140
|
+
reasoningTokens = finite(usage.tokens.reasoning);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
contextUsedTokens = finite(usage.contextWindowUsedTokens);
|
|
144
|
+
contextMaxTokens = finite(usage.contextWindowMaxTokens);
|
|
145
|
+
costUsd = finite(usage.totalCostUsd);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Count timeline tool calls and tool errors if timeline is provided
|
|
149
|
+
let toolCallsCount = 0;
|
|
150
|
+
let toolErrorsCount = 0;
|
|
151
|
+
if (Array.isArray(raw.timeline)) {
|
|
152
|
+
for (const item of raw.timeline) {
|
|
153
|
+
if (!item || typeof item !== "object") continue;
|
|
154
|
+
const type = (item as any).type;
|
|
155
|
+
if (type === "tool_call" || type === "tool_use" || (item as any).toolCall) {
|
|
156
|
+
toolCallsCount++;
|
|
157
|
+
if ((item as any).isError || (item as any).error || (item as any).status === "error") {
|
|
158
|
+
toolErrorsCount++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Interrupted / canceled normalization
|
|
165
|
+
const kind = raw.outcome?.kind ?? "completed";
|
|
166
|
+
const stopReason = raw.outcome?.stopReason ?? (raw.outcome as any)?.reason;
|
|
167
|
+
const isInterrupted =
|
|
168
|
+
kind === "canceled" ||
|
|
169
|
+
stopReason === "cancelled" ||
|
|
170
|
+
stopReason === "interrupted" ||
|
|
171
|
+
raw.outcome?.reason === "canceled" ||
|
|
172
|
+
raw.outcome?.reason === "interrupted";
|
|
173
|
+
|
|
174
|
+
let finishReason = isInterrupted ? "interrupted" : kind;
|
|
175
|
+
if (kind === "failed") finishReason = "failed";
|
|
176
|
+
|
|
177
|
+
const durationMs = Math.max(0, finite(raw.durationMs));
|
|
178
|
+
const durationSec = durationMs / 1000;
|
|
179
|
+
|
|
180
|
+
// Generation velocity (TPS): sum output + reasoning (OpenCode isolation rule)
|
|
181
|
+
// Interrupted turns with 0 duration or incomplete payloads yield 0 TPS to avoid skewed averages
|
|
182
|
+
const generationTokens = outputTokens + reasoningTokens;
|
|
183
|
+
const tokensPerSecond =
|
|
184
|
+
!isInterrupted && durationSec > 0.05 && generationTokens > 0
|
|
185
|
+
? round2(generationTokens / durationSec)
|
|
186
|
+
: 0;
|
|
187
|
+
|
|
188
|
+
const contextUtilizationPct =
|
|
189
|
+
contextMaxTokens > 0 ? round2(Math.min(100, (contextUsedTokens / contextMaxTokens) * 100)) : 0;
|
|
190
|
+
|
|
191
|
+
// Git churn diff
|
|
192
|
+
let gitInsertions = 0;
|
|
193
|
+
let gitDeletions = 0;
|
|
194
|
+
let gitFilesChanged = 0;
|
|
195
|
+
if (raw.gitAfter) {
|
|
196
|
+
const before = raw.gitBefore;
|
|
197
|
+
gitInsertions = Math.max(0, raw.gitAfter.insertions - (before?.insertions ?? 0));
|
|
198
|
+
gitDeletions = Math.max(0, raw.gitAfter.deletions - (before?.deletions ?? 0));
|
|
199
|
+
gitFilesChanged = raw.gitAfter.filesChanged;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const isSubscriptionZeroCost = costUsd === 0 && (inputTokens > 0 || outputTokens > 0);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
turnId: raw.turnId ?? null,
|
|
206
|
+
agentId: raw.agentId,
|
|
207
|
+
repo: raw.repo ?? null,
|
|
208
|
+
role: raw.role ?? null,
|
|
209
|
+
model: raw.model ?? null,
|
|
210
|
+
provider: raw.provider ?? null,
|
|
211
|
+
timestamp: new Date(now).toISOString(),
|
|
212
|
+
durationMs,
|
|
213
|
+
inputTokens,
|
|
214
|
+
outputTokens,
|
|
215
|
+
cachedTokens,
|
|
216
|
+
reasoningTokens,
|
|
217
|
+
totalTokens: inputTokens + outputTokens + cachedTokens + reasoningTokens,
|
|
218
|
+
tokensPerSecond,
|
|
219
|
+
contextUsedTokens,
|
|
220
|
+
contextMaxTokens,
|
|
221
|
+
contextUtilizationPct,
|
|
222
|
+
costUsd: round2(costUsd),
|
|
223
|
+
toolCallsCount,
|
|
224
|
+
toolErrorsCount,
|
|
225
|
+
finishReason,
|
|
226
|
+
stopReason: stopReason ? String(stopReason) : undefined,
|
|
227
|
+
errorMessage: raw.outcome?.error?.message,
|
|
228
|
+
gitInsertions,
|
|
229
|
+
gitDeletions,
|
|
230
|
+
gitFilesChanged,
|
|
231
|
+
isInterrupted,
|
|
232
|
+
isSubscriptionZeroCost,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 1. Front Desk Suitability Index (FDSI)
|
|
238
|
+
* FDSI = Conversational Conciseness / (TTFT Latency * (Turn Locks + 1) * Token Burn Ratio)
|
|
239
|
+
*/
|
|
240
|
+
export interface FDSIInput {
|
|
241
|
+
conciseness: number;
|
|
242
|
+
ttftLatencyMs: number;
|
|
243
|
+
turnLocks: number;
|
|
244
|
+
tokenBurnRatio: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function calculateFDSI(input: FDSIInput): number {
|
|
248
|
+
const conciseness = Math.max(0, input.conciseness);
|
|
249
|
+
const ttftSec = Math.max(0.001, input.ttftLatencyMs / 1000);
|
|
250
|
+
const locksFactor = Math.max(1, input.turnLocks + 1);
|
|
251
|
+
const burnRatio = Math.max(1.0, input.tokenBurnRatio);
|
|
252
|
+
|
|
253
|
+
const denominator = ttftSec * locksFactor * burnRatio;
|
|
254
|
+
return round2(conciseness / denominator);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 2. Worker Implementation ROI (Cost & Churn per Merged PR)
|
|
259
|
+
* Worker ROI = (Net Deliverable Value / (Total Tokens * Wall Clock Time * (Rework Rounds + 1))) / ChurnRatio
|
|
260
|
+
*/
|
|
261
|
+
export interface WorkerROIInput {
|
|
262
|
+
netDeliverableValue: number;
|
|
263
|
+
totalTokens: number;
|
|
264
|
+
wallClockTimeMs: number;
|
|
265
|
+
reworkRounds: number;
|
|
266
|
+
linesAdded: number;
|
|
267
|
+
linesDeleted: number;
|
|
268
|
+
netChanges: number;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function calculateWorkerROI(input: WorkerROIInput): number {
|
|
272
|
+
const deliverable = Math.max(0, input.netDeliverableValue);
|
|
273
|
+
const tokens = Math.max(1, input.totalTokens);
|
|
274
|
+
const wallClockSec = Math.max(1, input.wallClockTimeMs / 1000);
|
|
275
|
+
const rework = Math.max(1, input.reworkRounds + 1);
|
|
276
|
+
|
|
277
|
+
const netChanges = Math.max(1, Math.abs(input.netChanges));
|
|
278
|
+
const totalChurn = Math.max(netChanges, input.linesAdded + input.linesDeleted);
|
|
279
|
+
const churnRatio = Math.max(1.0, totalChurn / netChanges);
|
|
280
|
+
|
|
281
|
+
const rawRoi = (deliverable * 1_000_000) / (tokens * wallClockSec * rework);
|
|
282
|
+
return round2(rawRoi / churnRatio);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 3. Orchestrator Audit Vigilance Score
|
|
287
|
+
* Vigilance = (Flaws Caught in Pre-Flight + False-Positive Claims Debunked) / Total PRs Audited
|
|
288
|
+
*/
|
|
289
|
+
export interface OrchestratorVigilanceInput {
|
|
290
|
+
flawsCaught: number;
|
|
291
|
+
falsePositiveClaimsDebunked: number;
|
|
292
|
+
totalPRsAudited: number;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function calculateOrchestratorVigilance(input: OrchestratorVigilanceInput): number {
|
|
296
|
+
const totalAudits = Math.max(1, input.totalPRsAudited);
|
|
297
|
+
const signals = Math.max(0, input.flawsCaught + input.falsePositiveClaimsDebunked);
|
|
298
|
+
return round2(signals / totalAudits);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 4. Tool Thrashing / Loop Index
|
|
303
|
+
* Frequency of non-zero exit codes (rc != 0) followed immediately by another command
|
|
304
|
+
* without intervening file inspections or edits.
|
|
305
|
+
*/
|
|
306
|
+
export interface ToolCallTrace {
|
|
307
|
+
command?: string;
|
|
308
|
+
exitCode?: number;
|
|
309
|
+
isInspectionOrEdit?: boolean;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function calculateToolThrashingIndex(traces: readonly ToolCallTrace[]): number {
|
|
313
|
+
if (traces.length < 2) return 0;
|
|
314
|
+
|
|
315
|
+
let thrashedSequences = 0;
|
|
316
|
+
let totalCommandTransitions = 0;
|
|
317
|
+
|
|
318
|
+
for (let i = 0; i < traces.length - 1; i++) {
|
|
319
|
+
const curr = traces[i]!;
|
|
320
|
+
const next = traces[i + 1]!;
|
|
321
|
+
|
|
322
|
+
if (curr.exitCode !== undefined) {
|
|
323
|
+
totalCommandTransitions++;
|
|
324
|
+
if (curr.exitCode !== 0 && !curr.isInspectionOrEdit && !next.isInspectionOrEdit) {
|
|
325
|
+
thrashedSequences++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (totalCommandTransitions === 0) return 0;
|
|
331
|
+
return round2((thrashedSequences / totalCommandTransitions) * 100);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 5. Context Decay & Amnesia Risk
|
|
336
|
+
* Correlation between context utilization (> 75%) and subsequent turn failures, tool loops, or repetitive questions.
|
|
337
|
+
*/
|
|
338
|
+
export interface ContextTurnTrace {
|
|
339
|
+
contextUtilizationPct: number;
|
|
340
|
+
failed: boolean;
|
|
341
|
+
hasToolLoop?: boolean;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function calculateContextDecayRisk(turns: readonly ContextTurnTrace[]): number {
|
|
345
|
+
if (turns.length === 0) return 0;
|
|
346
|
+
|
|
347
|
+
const highContextTurns = turns.filter((t) => t.contextUtilizationPct > 75);
|
|
348
|
+
if (highContextTurns.length === 0) return 0;
|
|
349
|
+
|
|
350
|
+
const failuresUnderHighContext = highContextTurns.filter(
|
|
351
|
+
(t) => t.failed || t.hasToolLoop,
|
|
352
|
+
).length;
|
|
353
|
+
|
|
354
|
+
return round2((failuresUnderHighContext / highContextTurns.length) * 100);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface TurnTelemetryTapOptions {
|
|
358
|
+
onRecord?: (record: NormalizedTurnTelemetry) => void | Promise<void>;
|
|
359
|
+
onTurnStarted?: (event: any, context: any) => void;
|
|
360
|
+
onTurnEnded?: (event: any, context: any) => void | Promise<void>;
|
|
361
|
+
context?: any;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Sets up in-process turn telemetry listeners on the Paseo plugin server (#933).
|
|
366
|
+
*/
|
|
367
|
+
export function setupTurnTelemetryTap(
|
|
368
|
+
server: any,
|
|
369
|
+
options?: TurnTelemetryTapOptions,
|
|
370
|
+
): FeatureDisposer {
|
|
371
|
+
const turnStartTimes = new Map<string, number>();
|
|
372
|
+
const lastKnownUsage = new Map<string, RawTurnUsage>();
|
|
373
|
+
const unsubs: Array<() => void> = [];
|
|
374
|
+
|
|
375
|
+
if (typeof server?.on === "function") {
|
|
376
|
+
// 1. Turn started
|
|
377
|
+
try {
|
|
378
|
+
const unsubStart = server.on("agent.turn_started", (event: any, ctx: any) => {
|
|
379
|
+
if (!event?.agent?.id) return;
|
|
380
|
+
turnStartTimes.set(event.agent.id, Date.now());
|
|
381
|
+
if (event.agent.lastUsage) {
|
|
382
|
+
lastKnownUsage.set(event.agent.id, event.agent.lastUsage);
|
|
383
|
+
}
|
|
384
|
+
options?.onTurnStarted?.(event, ctx);
|
|
385
|
+
});
|
|
386
|
+
if (typeof unsubStart === "function") unsubs.push(unsubStart);
|
|
387
|
+
} catch {}
|
|
388
|
+
|
|
389
|
+
// 2. Usage updated
|
|
390
|
+
try {
|
|
391
|
+
const unsubUsage = server.on("agent.usage_updated", (event: any) => {
|
|
392
|
+
if (!event?.agent?.id) return;
|
|
393
|
+
const usage = event.usage ?? event.agent?.lastUsage;
|
|
394
|
+
if (usage) {
|
|
395
|
+
lastKnownUsage.set(event.agent.id, usage);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
if (typeof unsubUsage === "function") unsubs.push(unsubUsage);
|
|
399
|
+
} catch {}
|
|
400
|
+
|
|
401
|
+
// 3. Turn ended
|
|
402
|
+
try {
|
|
403
|
+
const unsubEnd = server.on("agent.turn_ended", async (event: any, ctx: any) => {
|
|
404
|
+
if (!event?.agent?.id) return;
|
|
405
|
+
const agentId = event.agent.id;
|
|
406
|
+
const startTime = turnStartTimes.get(agentId);
|
|
407
|
+
const durationMs = startTime ? Date.now() - startTime : undefined;
|
|
408
|
+
turnStartTimes.delete(agentId);
|
|
409
|
+
|
|
410
|
+
const cachedUsage = lastKnownUsage.get(agentId);
|
|
411
|
+
const effectiveUsage = (event.agent as any)?.lastUsage ?? cachedUsage ?? null;
|
|
412
|
+
|
|
413
|
+
const rawData: RawTurnData = {
|
|
414
|
+
turnId: event.turnId ?? null,
|
|
415
|
+
agentId,
|
|
416
|
+
repo: (event.agent as any)?.repo ?? null,
|
|
417
|
+
role: (event.agent as any)?.role ?? null,
|
|
418
|
+
model: event.agent.model ?? null,
|
|
419
|
+
provider: event.agent.provider ?? null,
|
|
420
|
+
outcome: event.outcome ?? { kind: "completed" },
|
|
421
|
+
durationMs,
|
|
422
|
+
timeline: event.timeline,
|
|
423
|
+
liveUsage: effectiveUsage,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const record = normalizeTurnTelemetry(rawData);
|
|
428
|
+
if (options?.onRecord) {
|
|
429
|
+
await options.onRecord(record);
|
|
430
|
+
}
|
|
431
|
+
if (options?.onTurnEnded) {
|
|
432
|
+
await options.onTurnEnded(event, ctx);
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
// Best effort
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
if (typeof unsubEnd === "function") unsubs.push(unsubEnd);
|
|
439
|
+
} catch {}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
let disposed = false;
|
|
443
|
+
return () => {
|
|
444
|
+
if (disposed) return;
|
|
445
|
+
disposed = true;
|
|
446
|
+
for (const unsub of unsubs) {
|
|
447
|
+
try {
|
|
448
|
+
unsub();
|
|
449
|
+
} catch {}
|
|
450
|
+
}
|
|
451
|
+
turnStartTimes.clear();
|
|
452
|
+
lastKnownUsage.clear();
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export interface TimelineTelemetryFeatureOptions<TServerContext = any, TClientContext = any>
|
|
457
|
+
extends TurnTelemetryTapOptions {
|
|
458
|
+
id?: string;
|
|
459
|
+
contracts?: Record<string, PluginRpcContract>;
|
|
460
|
+
serverTap?: (server: TServerContext) => FeatureDisposer | void;
|
|
461
|
+
clientRenderer?: (client: TClientContext) => FeatureDisposer | void;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Creates a reusable FeatureModule for timeline telemetry (#933).
|
|
466
|
+
*/
|
|
467
|
+
export function createTimelineTelemetryFeature<TServerContext = any, TClientContext = any>(
|
|
468
|
+
options: TimelineTelemetryFeatureOptions<TServerContext, TClientContext> = {},
|
|
469
|
+
): FeatureModule<TServerContext, TClientContext> {
|
|
470
|
+
return {
|
|
471
|
+
id: options.id || "timeline-telemetry",
|
|
472
|
+
contracts: options.contracts,
|
|
473
|
+
contributeServer(server: TServerContext) {
|
|
474
|
+
if (options.serverTap) {
|
|
475
|
+
return options.serverTap(server);
|
|
476
|
+
}
|
|
477
|
+
return setupTurnTelemetryTap(server, options);
|
|
478
|
+
},
|
|
479
|
+
contributeClient(client: TClientContext) {
|
|
480
|
+
if (options.clientRenderer) {
|
|
481
|
+
return options.clientRenderer(client);
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
4
3
|
import { PluginStorage } from "./storage";
|
|
5
4
|
import { createPluginLogger } from "./logger";
|
|
@@ -13,7 +12,6 @@ import {
|
|
|
13
12
|
activeForgeForDirectory,
|
|
14
13
|
forgeCapabilityFromRepo,
|
|
15
14
|
forgeContextContract,
|
|
16
|
-
forgeForgeContextContract,
|
|
17
15
|
openIssuesContract,
|
|
18
16
|
searchIssuesContract,
|
|
19
17
|
issueDetailContract,
|
|
@@ -624,18 +622,20 @@ export async function resolveForgeToken(
|
|
|
624
622
|
} catch {}
|
|
625
623
|
}
|
|
626
624
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
625
|
+
// Read ONLY from the caller's own plugin scope — never fall back to 'forges'.
|
|
626
|
+
if (options?.storagePluginId) {
|
|
627
|
+
try {
|
|
628
|
+
const storage = new PluginStorage<{ tokensByHost?: Record<string, string> }>(
|
|
629
|
+
options.storagePluginId,
|
|
630
|
+
"settings.json",
|
|
631
|
+
);
|
|
632
|
+
const data = await storage.readAsync();
|
|
633
|
+
const token = data?.tokensByHost?.[host];
|
|
634
|
+
if (typeof token === "string" && token.trim()) {
|
|
635
|
+
return token.trim();
|
|
636
|
+
}
|
|
637
|
+
} catch {}
|
|
638
|
+
}
|
|
639
639
|
|
|
640
640
|
if (process.env.FORGEJO_TOKEN?.trim()) {
|
|
641
641
|
return process.env.FORGEJO_TOKEN.trim();
|
|
@@ -644,24 +644,6 @@ export async function resolveForgeToken(
|
|
|
644
644
|
return process.env.GITEA_TOKEN.trim();
|
|
645
645
|
}
|
|
646
646
|
|
|
647
|
-
try {
|
|
648
|
-
const teaConfigPath = join(homedir(), ".config", "tea", "config.yml");
|
|
649
|
-
const content = await readFile(teaConfigPath, "utf8");
|
|
650
|
-
const lines = content.split("\n");
|
|
651
|
-
let currentLoginMatches = false;
|
|
652
|
-
for (const line of lines) {
|
|
653
|
-
if (line.includes(host) || line.includes("https://" + host)) {
|
|
654
|
-
currentLoginMatches = true;
|
|
655
|
-
} else if (line.trim().startsWith("- name:")) {
|
|
656
|
-
currentLoginMatches = line.includes(host);
|
|
657
|
-
}
|
|
658
|
-
if (currentLoginMatches && line.trim().startsWith("token:")) {
|
|
659
|
-
const token = line.replace(/.*token:\s*/, "").trim();
|
|
660
|
-
if (token) return token;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
} catch {}
|
|
664
|
-
|
|
665
647
|
return undefined;
|
|
666
648
|
}
|
|
667
649
|
|
|
@@ -672,45 +654,15 @@ export async function resolveDefaultForgeHost(): Promise<string> {
|
|
|
672
654
|
if (process.env.GITEA_HOST?.trim()) {
|
|
673
655
|
return process.env.GITEA_HOST.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
674
656
|
}
|
|
675
|
-
try {
|
|
676
|
-
const teaConfigPath = join(homedir(), ".config", "tea", "config.yml");
|
|
677
|
-
const content = await readFile(teaConfigPath, "utf8");
|
|
678
|
-
const lines = content.split("\n");
|
|
679
|
-
let defaultLoginUrl = "";
|
|
680
|
-
let firstLoginUrl = "";
|
|
681
|
-
let inDefaultLogin = false;
|
|
682
|
-
for (const line of lines) {
|
|
683
|
-
if (line.trim().startsWith("- name:")) {
|
|
684
|
-
inDefaultLogin = false;
|
|
685
|
-
}
|
|
686
|
-
if (line.trim().startsWith("default: true")) {
|
|
687
|
-
inDefaultLogin = true;
|
|
688
|
-
}
|
|
689
|
-
if (line.trim().startsWith("url:") || line.trim().startsWith("ssh_host:")) {
|
|
690
|
-
const urlOrHost = line.replace(/.*(?:url|ssh_host):\s*/, "").trim();
|
|
691
|
-
if (urlOrHost) {
|
|
692
|
-
const host = urlOrHost.replace(/^https?:\/\//, "").replace(/\/+$/, "");
|
|
693
|
-
if (inDefaultLogin && !defaultLoginUrl) {
|
|
694
|
-
defaultLoginUrl = host;
|
|
695
|
-
}
|
|
696
|
-
if (!firstLoginUrl) {
|
|
697
|
-
firstLoginUrl = host;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
if (defaultLoginUrl) return defaultLoginUrl;
|
|
703
|
-
if (firstLoginUrl) return firstLoginUrl;
|
|
704
|
-
} catch {}
|
|
705
657
|
|
|
706
658
|
return "forge.mrs.uppidi.com";
|
|
707
659
|
}
|
|
708
660
|
|
|
709
661
|
export async function storedForgeSelection(
|
|
710
662
|
directory: string | undefined,
|
|
711
|
-
storagePluginId
|
|
663
|
+
storagePluginId?: string,
|
|
712
664
|
): Promise<string | undefined> {
|
|
713
|
-
if (!directory) return undefined;
|
|
665
|
+
if (!directory || !storagePluginId) return undefined;
|
|
714
666
|
try {
|
|
715
667
|
const storage = new PluginStorage<Record<string, any>>(storagePluginId, "settings.json");
|
|
716
668
|
const settings = await storage.readAsync();
|
|
@@ -781,8 +733,8 @@ export interface TicketHandlers {
|
|
|
781
733
|
}
|
|
782
734
|
|
|
783
735
|
export function createTicketHandlers(options: TicketHandlerOptions = {}): TicketHandlers {
|
|
784
|
-
const storagePluginId = options.storagePluginId
|
|
785
|
-
const log = createPluginLogger(storagePluginId, { banner: false });
|
|
736
|
+
const storagePluginId = options.storagePluginId; // no 'forges' fallback
|
|
737
|
+
const log = createPluginLogger(storagePluginId || "tickets", { banner: false });
|
|
786
738
|
const guard = options.guard ?? new ForgeGuard();
|
|
787
739
|
|
|
788
740
|
function logAt(level: GuardLogLevel, message: string, context: Record<string, unknown>): void {
|
|
@@ -1271,7 +1223,6 @@ export function registerTicketHandlers(
|
|
|
1271
1223
|
server.handle(openIssuesContract, (input: OpenIssuesInput) => handlers.handleOpenIssues(input));
|
|
1272
1224
|
server.handle(searchIssuesContract, (input: SearchIssuesInput) => handlers.handleSearchIssues(input));
|
|
1273
1225
|
server.handle(forgeContextContract, (input: ForgeContextInput) => handlers.handleForgeContext(input));
|
|
1274
|
-
server.handle(forgeForgeContextContract, (input: ForgeContextInput) => handlers.handleForgeContext(input));
|
|
1275
1226
|
server.handle(issueDetailContract, (input: IssueDetailInput) => handlers.handleIssueDetail(input));
|
|
1276
1227
|
server.handle(setLabelContract, (input: SetLabelInput) => handlers.handleSetLabel(input));
|
|
1277
1228
|
server.handle(addCommentContract, (input: AddCommentInput) => handlers.handleAddComment(input));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./server.js";
|