@jmoyers/harness 0.1.8 → 0.1.10
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 +33 -155
- package/package.json +5 -1
- package/packages/harness-ai/src/anthropic-client.ts +99 -0
- package/packages/harness-ai/src/anthropic-protocol.ts +581 -0
- package/packages/harness-ai/src/anthropic-provider.ts +82 -0
- package/packages/harness-ai/src/async-iterable-stream.ts +65 -0
- package/packages/harness-ai/src/index.ts +36 -0
- package/packages/harness-ai/src/json-parse.ts +66 -0
- package/packages/harness-ai/src/sse.ts +80 -0
- package/packages/harness-ai/src/stream-object.ts +96 -0
- package/packages/harness-ai/src/stream-text.ts +1340 -0
- package/packages/harness-ai/src/types.ts +330 -0
- package/packages/harness-ai/src/ui-stream.ts +217 -0
- package/scripts/codex-live-mux-runtime.ts +123 -7
- package/scripts/control-plane-daemon.ts +20 -3
- package/scripts/harness.ts +566 -133
- package/src/cli/gateway-record.ts +16 -1
- package/src/control-plane/agent-realtime-api.ts +4 -0
- package/src/control-plane/prompt/agent-prompt-extractor.ts +191 -0
- package/src/control-plane/prompt/extractors/claude-prompt-extractor.ts +53 -0
- package/src/control-plane/prompt/extractors/codex-prompt-extractor.ts +50 -0
- package/src/control-plane/prompt/extractors/cursor-prompt-extractor.ts +56 -0
- package/src/control-plane/prompt/session-prompt-engine.ts +69 -0
- package/src/control-plane/prompt/thread-title-namer.ts +290 -0
- package/src/control-plane/stream-command-parser.ts +12 -0
- package/src/control-plane/stream-protocol.ts +109 -0
- package/src/control-plane/stream-server-command.ts +14 -0
- package/src/control-plane/stream-server-session-runtime.ts +12 -0
- package/src/control-plane/stream-server.ts +485 -19
- package/src/mux/input-shortcuts.ts +9 -0
- package/src/mux/live-mux/critique-review.ts +5 -1
- package/src/mux/live-mux/git-parsing.ts +24 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
- package/src/mux/render-frame.ts +1 -1
- package/src/pty/pty_host.ts +46 -1
- package/src/services/control-plane.ts +22 -0
- package/src/services/runtime-control-actions.ts +69 -0
- package/src/services/runtime-navigation-input.ts +4 -0
- package/src/services/runtime-rail-input.ts +4 -0
- package/src/services/runtime-workspace-actions.ts +5 -0
- package/src/ui/global-shortcut-input.ts +2 -0
|
@@ -6,6 +6,7 @@ export const DEFAULT_GATEWAY_PORT = 7777;
|
|
|
6
6
|
export const DEFAULT_GATEWAY_DB_PATH = '.harness/control-plane.sqlite';
|
|
7
7
|
export const DEFAULT_GATEWAY_RECORD_PATH = '.harness/gateway.json';
|
|
8
8
|
export const DEFAULT_GATEWAY_LOG_PATH = '.harness/gateway.log';
|
|
9
|
+
export const DEFAULT_GATEWAY_LOCK_PATH = '.harness/gateway.lock';
|
|
9
10
|
|
|
10
11
|
export interface GatewayRecord {
|
|
11
12
|
readonly version: number;
|
|
@@ -16,6 +17,7 @@ export interface GatewayRecord {
|
|
|
16
17
|
readonly stateDbPath: string;
|
|
17
18
|
readonly startedAt: string;
|
|
18
19
|
readonly workspaceRoot: string;
|
|
20
|
+
readonly gatewayRunId?: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
@@ -74,6 +76,13 @@ export function resolveGatewayLogPath(
|
|
|
74
76
|
return resolveHarnessRuntimePath(workspaceRoot, DEFAULT_GATEWAY_LOG_PATH, env);
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
export function resolveGatewayLockPath(
|
|
80
|
+
workspaceRoot: string,
|
|
81
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
82
|
+
): string {
|
|
83
|
+
return resolveHarnessRuntimePath(workspaceRoot, DEFAULT_GATEWAY_LOCK_PATH, env);
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
export function normalizeGatewayHost(
|
|
78
87
|
input: string | null | undefined,
|
|
79
88
|
fallback = DEFAULT_GATEWAY_HOST,
|
|
@@ -149,6 +158,9 @@ export function parseGatewayRecordText(text: string): GatewayRecord | null {
|
|
|
149
158
|
const workspaceRoot = readNonEmptyString(record['workspaceRoot']);
|
|
150
159
|
const authTokenRaw = record['authToken'];
|
|
151
160
|
const authToken = authTokenRaw === null ? null : readNonEmptyString(authTokenRaw);
|
|
161
|
+
const gatewayRunIdRaw = record['gatewayRunId'];
|
|
162
|
+
const gatewayRunId =
|
|
163
|
+
gatewayRunIdRaw === undefined ? undefined : readNonEmptyString(gatewayRunIdRaw);
|
|
152
164
|
|
|
153
165
|
if (
|
|
154
166
|
pid === null ||
|
|
@@ -157,10 +169,12 @@ export function parseGatewayRecordText(text: string): GatewayRecord | null {
|
|
|
157
169
|
stateDbPath === null ||
|
|
158
170
|
startedAt === null ||
|
|
159
171
|
workspaceRoot === null ||
|
|
160
|
-
(authToken === null && authTokenRaw !== null)
|
|
172
|
+
(authToken === null && authTokenRaw !== null) ||
|
|
173
|
+
(gatewayRunIdRaw !== undefined && gatewayRunId === null)
|
|
161
174
|
) {
|
|
162
175
|
return null;
|
|
163
176
|
}
|
|
177
|
+
const parsedGatewayRunId = gatewayRunId === null ? undefined : gatewayRunId;
|
|
164
178
|
|
|
165
179
|
return {
|
|
166
180
|
version,
|
|
@@ -171,6 +185,7 @@ export function parseGatewayRecordText(text: string): GatewayRecord | null {
|
|
|
171
185
|
stateDbPath,
|
|
172
186
|
startedAt,
|
|
173
187
|
workspaceRoot,
|
|
188
|
+
...(parsedGatewayRunId === undefined ? {} : { gatewayRunId: parsedGatewayRunId }),
|
|
174
189
|
};
|
|
175
190
|
}
|
|
176
191
|
|
|
@@ -54,6 +54,7 @@ interface AgentEventTypeMap {
|
|
|
54
54
|
'session.status': Extract<StreamObservedEvent, { type: 'session-status' }>;
|
|
55
55
|
'session.event': Extract<StreamObservedEvent, { type: 'session-event' }>;
|
|
56
56
|
'session.telemetry': Extract<StreamObservedEvent, { type: 'session-key-event' }>;
|
|
57
|
+
'session.prompt': Extract<StreamObservedEvent, { type: 'session-prompt-event' }>;
|
|
57
58
|
'session.control': Extract<StreamObservedEvent, { type: 'session-control' }>;
|
|
58
59
|
'session.output': Extract<StreamObservedEvent, { type: 'session-output' }>;
|
|
59
60
|
}
|
|
@@ -1155,6 +1156,9 @@ function mapObservedEventType(observed: StreamObservedEvent): AgentRealtimeEvent
|
|
|
1155
1156
|
if (observed.type === 'session-key-event') {
|
|
1156
1157
|
return 'session.telemetry';
|
|
1157
1158
|
}
|
|
1159
|
+
if (observed.type === 'session-prompt-event') {
|
|
1160
|
+
return 'session.prompt';
|
|
1161
|
+
}
|
|
1158
1162
|
if (observed.type === 'session-control') {
|
|
1159
1163
|
return 'session.control';
|
|
1160
1164
|
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type {
|
|
3
|
+
StreamPromptCaptureSource,
|
|
4
|
+
StreamPromptConfidence,
|
|
5
|
+
StreamSessionPromptRecord,
|
|
6
|
+
StreamTelemetrySource,
|
|
7
|
+
} from '../stream-protocol.ts';
|
|
8
|
+
|
|
9
|
+
export interface PromptFromNotifyInput {
|
|
10
|
+
readonly payload: Record<string, unknown>;
|
|
11
|
+
readonly observedAt: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface PromptFromTelemetryInput {
|
|
15
|
+
readonly source: StreamTelemetrySource;
|
|
16
|
+
readonly eventName: string | null;
|
|
17
|
+
readonly summary: string | null;
|
|
18
|
+
readonly payload: Record<string, unknown>;
|
|
19
|
+
readonly observedAt: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AgentPromptExtractor {
|
|
23
|
+
readonly agentType: string;
|
|
24
|
+
fromNotify(input: PromptFromNotifyInput): StreamSessionPromptRecord | null;
|
|
25
|
+
fromTelemetry(input: PromptFromTelemetryInput): StreamSessionPromptRecord | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readTrimmedString(value: unknown): string | null {
|
|
29
|
+
if (typeof value !== 'string') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeEventToken(value: string): string {
|
|
37
|
+
return value
|
|
38
|
+
.trim()
|
|
39
|
+
.toLowerCase()
|
|
40
|
+
.replace(/[^a-z0-9]+/g, '');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
44
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return value as Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stableSerialize(value: unknown, depth = 0): string {
|
|
51
|
+
if (depth > 4) {
|
|
52
|
+
return '[depth-limit]';
|
|
53
|
+
}
|
|
54
|
+
if (value === null) {
|
|
55
|
+
return 'null';
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === 'string') {
|
|
58
|
+
return JSON.stringify(value);
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
61
|
+
return String(value);
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
return `[${value
|
|
65
|
+
.slice(0, 24)
|
|
66
|
+
.map((entry) => stableSerialize(entry, depth + 1))
|
|
67
|
+
.join(',')}]`;
|
|
68
|
+
}
|
|
69
|
+
const record = asRecord(value);
|
|
70
|
+
if (record === null) {
|
|
71
|
+
return typeof value;
|
|
72
|
+
}
|
|
73
|
+
const keys = Object.keys(record).sort().slice(0, 24);
|
|
74
|
+
return `{${keys
|
|
75
|
+
.map((key) => `${JSON.stringify(key)}:${stableSerialize(record[key], depth + 1)}`)
|
|
76
|
+
.join(',')}}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hashPromptPayload(input: {
|
|
80
|
+
readonly text: string | null;
|
|
81
|
+
readonly providerEventName: string | null;
|
|
82
|
+
readonly payload: Record<string, unknown>;
|
|
83
|
+
}): string {
|
|
84
|
+
const hash = createHash('sha256');
|
|
85
|
+
hash.update(input.providerEventName ?? '');
|
|
86
|
+
hash.update('\n');
|
|
87
|
+
hash.update(input.text ?? '');
|
|
88
|
+
hash.update('\n');
|
|
89
|
+
hash.update(stableSerialize(input.payload));
|
|
90
|
+
return hash.digest('hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function lookupRecordValue(
|
|
94
|
+
record: Record<string, unknown>,
|
|
95
|
+
candidateKeys: readonly string[],
|
|
96
|
+
): string | null {
|
|
97
|
+
const normalizedKeys = new Set(candidateKeys.map((key) => key.toLowerCase()));
|
|
98
|
+
for (const [key, value] of Object.entries(record)) {
|
|
99
|
+
if (!normalizedKeys.has(key.toLowerCase())) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const text = readTrimmedString(value);
|
|
103
|
+
if (text !== null) {
|
|
104
|
+
return text;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function findPromptText(
|
|
111
|
+
value: unknown,
|
|
112
|
+
options: {
|
|
113
|
+
readonly keys: readonly string[];
|
|
114
|
+
readonly maxDepth?: number;
|
|
115
|
+
},
|
|
116
|
+
): string | null {
|
|
117
|
+
const maxDepth = options.maxDepth ?? 3;
|
|
118
|
+
const visited = new Set<unknown>();
|
|
119
|
+
const queue: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }];
|
|
120
|
+
while (queue.length > 0) {
|
|
121
|
+
const next = queue.shift();
|
|
122
|
+
if (next === undefined) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (next.depth > maxDepth) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (next.value !== null && typeof next.value === 'object') {
|
|
129
|
+
if (visited.has(next.value)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
visited.add(next.value);
|
|
133
|
+
}
|
|
134
|
+
if (typeof next.value === 'string') {
|
|
135
|
+
if (next.depth === 0) {
|
|
136
|
+
const text = readTrimmedString(next.value);
|
|
137
|
+
if (text !== null) {
|
|
138
|
+
return text;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const record = asRecord(next.value);
|
|
144
|
+
if (record !== null) {
|
|
145
|
+
const match = lookupRecordValue(record, options.keys);
|
|
146
|
+
if (match !== null) {
|
|
147
|
+
return match;
|
|
148
|
+
}
|
|
149
|
+
if (next.depth >= maxDepth) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
for (const nested of Object.values(record)) {
|
|
153
|
+
queue.push({ value: nested, depth: next.depth + 1 });
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (Array.isArray(next.value) && next.depth < maxDepth) {
|
|
158
|
+
for (const nested of next.value.slice(0, 24)) {
|
|
159
|
+
queue.push({ value: nested, depth: next.depth + 1 });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function providerPayloadKeys(payload: Record<string, unknown>): string[] {
|
|
167
|
+
return Object.keys(payload).sort().slice(0, 12);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function createPromptRecord(input: {
|
|
171
|
+
readonly text: string | null;
|
|
172
|
+
readonly confidence: StreamPromptConfidence;
|
|
173
|
+
readonly captureSource: StreamPromptCaptureSource;
|
|
174
|
+
readonly providerEventName: string | null;
|
|
175
|
+
readonly payload: Record<string, unknown>;
|
|
176
|
+
readonly observedAt: string;
|
|
177
|
+
}): StreamSessionPromptRecord {
|
|
178
|
+
return {
|
|
179
|
+
text: input.text,
|
|
180
|
+
hash: hashPromptPayload({
|
|
181
|
+
text: input.text,
|
|
182
|
+
providerEventName: input.providerEventName,
|
|
183
|
+
payload: input.payload,
|
|
184
|
+
}),
|
|
185
|
+
confidence: input.confidence,
|
|
186
|
+
captureSource: input.captureSource,
|
|
187
|
+
providerEventName: input.providerEventName,
|
|
188
|
+
providerPayloadKeys: providerPayloadKeys(input.payload),
|
|
189
|
+
observedAt: input.observedAt,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { StreamSessionPromptRecord } from '../../stream-protocol.ts';
|
|
2
|
+
import type {
|
|
3
|
+
AgentPromptExtractor,
|
|
4
|
+
PromptFromNotifyInput,
|
|
5
|
+
PromptFromTelemetryInput,
|
|
6
|
+
} from '../agent-prompt-extractor.ts';
|
|
7
|
+
import {
|
|
8
|
+
createPromptRecord,
|
|
9
|
+
findPromptText,
|
|
10
|
+
normalizeEventToken,
|
|
11
|
+
readTrimmedString,
|
|
12
|
+
} from '../agent-prompt-extractor.ts';
|
|
13
|
+
|
|
14
|
+
function fromNotify(input: PromptFromNotifyInput): StreamSessionPromptRecord | null {
|
|
15
|
+
const hookEventName =
|
|
16
|
+
readTrimmedString(input.payload['hook_event_name']) ??
|
|
17
|
+
readTrimmedString(input.payload['hookEventName']);
|
|
18
|
+
if (hookEventName === null) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const hookToken = normalizeEventToken(hookEventName);
|
|
22
|
+
if (hookToken !== 'userpromptsubmit') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const directPrompt =
|
|
26
|
+
readTrimmedString(input.payload['prompt']) ??
|
|
27
|
+
readTrimmedString(input.payload['user_prompt']) ??
|
|
28
|
+
readTrimmedString(input.payload['userPrompt']);
|
|
29
|
+
const fallbackPrompt =
|
|
30
|
+
directPrompt ??
|
|
31
|
+
findPromptText(input.payload, {
|
|
32
|
+
keys: ['prompt', 'user_prompt', 'userPrompt', 'text', 'input', 'query', 'message'],
|
|
33
|
+
maxDepth: 3,
|
|
34
|
+
});
|
|
35
|
+
return createPromptRecord({
|
|
36
|
+
text: fallbackPrompt,
|
|
37
|
+
confidence: directPrompt !== null ? 'high' : fallbackPrompt !== null ? 'medium' : 'low',
|
|
38
|
+
captureSource: 'hook-notify',
|
|
39
|
+
providerEventName: `claude.${hookToken}`,
|
|
40
|
+
payload: input.payload,
|
|
41
|
+
observedAt: input.observedAt,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fromTelemetry(_input: PromptFromTelemetryInput): StreamSessionPromptRecord | null {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const claudePromptExtractor: AgentPromptExtractor = {
|
|
50
|
+
agentType: 'claude',
|
|
51
|
+
fromNotify,
|
|
52
|
+
fromTelemetry,
|
|
53
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StreamSessionPromptRecord } from '../../stream-protocol.ts';
|
|
2
|
+
import type {
|
|
3
|
+
AgentPromptExtractor,
|
|
4
|
+
PromptFromNotifyInput,
|
|
5
|
+
PromptFromTelemetryInput,
|
|
6
|
+
} from '../agent-prompt-extractor.ts';
|
|
7
|
+
import {
|
|
8
|
+
createPromptRecord,
|
|
9
|
+
findPromptText,
|
|
10
|
+
readTrimmedString,
|
|
11
|
+
} from '../agent-prompt-extractor.ts';
|
|
12
|
+
|
|
13
|
+
function fromNotify(_input: PromptFromNotifyInput): StreamSessionPromptRecord | null {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fromTelemetry(input: PromptFromTelemetryInput): StreamSessionPromptRecord | null {
|
|
18
|
+
const normalizedEventName = (input.eventName ?? '').trim().toLowerCase();
|
|
19
|
+
if (normalizedEventName !== 'codex.user_prompt' && normalizedEventName !== 'user_prompt') {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (input.source !== 'otlp-log' && input.source !== 'history') {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const textFromPayload = findPromptText(input.payload, {
|
|
26
|
+
keys: ['prompt', 'user_prompt', 'userPrompt', 'message', 'text', 'content', 'input', 'body'],
|
|
27
|
+
maxDepth: 4,
|
|
28
|
+
});
|
|
29
|
+
const normalizedSummary = readTrimmedString(input.summary);
|
|
30
|
+
const textFromSummary =
|
|
31
|
+
normalizedSummary !== null && normalizedSummary.toLowerCase().startsWith('prompt:')
|
|
32
|
+
? readTrimmedString(normalizedSummary.slice('prompt:'.length))
|
|
33
|
+
: null;
|
|
34
|
+
const promptText = textFromPayload ?? textFromSummary;
|
|
35
|
+
const confidence = promptText === null ? 'low' : textFromPayload !== null ? 'high' : 'medium';
|
|
36
|
+
return createPromptRecord({
|
|
37
|
+
text: promptText,
|
|
38
|
+
confidence,
|
|
39
|
+
captureSource: input.source === 'history' ? 'history' : 'otlp-log',
|
|
40
|
+
providerEventName: input.eventName,
|
|
41
|
+
payload: input.payload,
|
|
42
|
+
observedAt: input.observedAt,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const codexPromptExtractor: AgentPromptExtractor = {
|
|
47
|
+
agentType: 'codex',
|
|
48
|
+
fromNotify,
|
|
49
|
+
fromTelemetry,
|
|
50
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { StreamSessionPromptRecord } from '../../stream-protocol.ts';
|
|
2
|
+
import type {
|
|
3
|
+
AgentPromptExtractor,
|
|
4
|
+
PromptFromNotifyInput,
|
|
5
|
+
PromptFromTelemetryInput,
|
|
6
|
+
} from '../agent-prompt-extractor.ts';
|
|
7
|
+
import {
|
|
8
|
+
createPromptRecord,
|
|
9
|
+
findPromptText,
|
|
10
|
+
normalizeEventToken,
|
|
11
|
+
readTrimmedString,
|
|
12
|
+
} from '../agent-prompt-extractor.ts';
|
|
13
|
+
|
|
14
|
+
function fromNotify(input: PromptFromNotifyInput): StreamSessionPromptRecord | null {
|
|
15
|
+
const hookEventName =
|
|
16
|
+
readTrimmedString(input.payload['hook_event_name']) ??
|
|
17
|
+
readTrimmedString(input.payload['hookEventName']) ??
|
|
18
|
+
readTrimmedString(input.payload['event_name']) ??
|
|
19
|
+
readTrimmedString(input.payload['eventName']) ??
|
|
20
|
+
readTrimmedString(input.payload['event']);
|
|
21
|
+
if (hookEventName === null) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const hookToken = normalizeEventToken(hookEventName);
|
|
25
|
+
if (hookToken !== 'beforesubmitprompt') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const directPrompt =
|
|
29
|
+
readTrimmedString(input.payload['prompt']) ??
|
|
30
|
+
readTrimmedString(input.payload['user_prompt']) ??
|
|
31
|
+
readTrimmedString(input.payload['userPrompt']);
|
|
32
|
+
const promptText =
|
|
33
|
+
directPrompt ??
|
|
34
|
+
findPromptText(input.payload, {
|
|
35
|
+
keys: ['prompt', 'user_prompt', 'userPrompt', 'text', 'input', 'query', 'message'],
|
|
36
|
+
maxDepth: 3,
|
|
37
|
+
});
|
|
38
|
+
return createPromptRecord({
|
|
39
|
+
text: promptText,
|
|
40
|
+
confidence: directPrompt !== null ? 'high' : promptText !== null ? 'medium' : 'low',
|
|
41
|
+
captureSource: 'hook-notify',
|
|
42
|
+
providerEventName: `cursor.${hookToken}`,
|
|
43
|
+
payload: input.payload,
|
|
44
|
+
observedAt: input.observedAt,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fromTelemetry(_input: PromptFromTelemetryInput): StreamSessionPromptRecord | null {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const cursorPromptExtractor: AgentPromptExtractor = {
|
|
53
|
+
agentType: 'cursor',
|
|
54
|
+
fromNotify,
|
|
55
|
+
fromTelemetry,
|
|
56
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { StreamSessionPromptRecord, StreamTelemetrySource } from '../stream-protocol.ts';
|
|
2
|
+
import type { AgentPromptExtractor } from './agent-prompt-extractor.ts';
|
|
3
|
+
import { claudePromptExtractor } from './extractors/claude-prompt-extractor.ts';
|
|
4
|
+
import { codexPromptExtractor } from './extractors/codex-prompt-extractor.ts';
|
|
5
|
+
import { cursorPromptExtractor } from './extractors/cursor-prompt-extractor.ts';
|
|
6
|
+
|
|
7
|
+
type SupportedAgentType = 'codex' | 'claude' | 'cursor';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_EXTRACTORS: Record<SupportedAgentType, AgentPromptExtractor> = {
|
|
10
|
+
codex: codexPromptExtractor,
|
|
11
|
+
claude: claudePromptExtractor,
|
|
12
|
+
cursor: cursorPromptExtractor,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function normalizeAgentType(value: string): SupportedAgentType | null {
|
|
16
|
+
const normalized = value.trim().toLowerCase();
|
|
17
|
+
if (normalized === 'codex' || normalized === 'claude' || normalized === 'cursor') {
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface PromptFromNotifyInput {
|
|
24
|
+
readonly agentType: string;
|
|
25
|
+
readonly payload: Record<string, unknown>;
|
|
26
|
+
readonly observedAt: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface PromptFromTelemetryInput {
|
|
30
|
+
readonly agentType: string;
|
|
31
|
+
readonly source: StreamTelemetrySource;
|
|
32
|
+
readonly eventName: string | null;
|
|
33
|
+
readonly summary: string | null;
|
|
34
|
+
readonly payload: Record<string, unknown>;
|
|
35
|
+
readonly observedAt: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class SessionPromptEngine {
|
|
39
|
+
private readonly extractors: Record<SupportedAgentType, AgentPromptExtractor>;
|
|
40
|
+
|
|
41
|
+
constructor(extractors: Record<SupportedAgentType, AgentPromptExtractor> = DEFAULT_EXTRACTORS) {
|
|
42
|
+
this.extractors = extractors;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
extractFromNotify(input: PromptFromNotifyInput): StreamSessionPromptRecord | null {
|
|
46
|
+
const agentType = normalizeAgentType(input.agentType);
|
|
47
|
+
if (agentType === null) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
return this.extractors[agentType].fromNotify({
|
|
51
|
+
payload: input.payload,
|
|
52
|
+
observedAt: input.observedAt,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
extractFromTelemetry(input: PromptFromTelemetryInput): StreamSessionPromptRecord | null {
|
|
57
|
+
const agentType = normalizeAgentType(input.agentType);
|
|
58
|
+
if (agentType === null) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return this.extractors[agentType].fromTelemetry({
|
|
62
|
+
source: input.source,
|
|
63
|
+
eventName: input.eventName,
|
|
64
|
+
summary: input.summary,
|
|
65
|
+
payload: input.payload,
|
|
66
|
+
observedAt: input.observedAt,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|