incremnt 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -1
- package/package.json +2 -1
- package/src/ask-answer-verifier.js +857 -0
- package/src/ask-coach.js +2634 -0
- package/src/ask-replay.js +358 -0
- package/src/auth.js +169 -15
- package/src/contract.js +160 -3
- package/src/format.js +28 -2
- package/src/lib.js +205 -17
- package/src/mcp.js +88 -24
- package/src/openrouter.js +242 -19
- package/src/plan-changeset.js +132 -0
- package/src/program-draft.js +230 -0
- package/src/prompt-changelog.js +90 -0
- package/src/promptfoo-evals.js +10 -4
- package/src/promptfoo-langfuse-scores.js +55 -0
- package/src/queries.js +992 -987
- package/src/remote.js +465 -12
- package/src/score-context.js +14 -7
- package/src/score-prelude.js +113 -0
- package/src/service-url.js +9 -0
- package/src/summary-evals.js +677 -42
- package/src/sync-service.js +1259 -352
- package/src/transport.js +119 -3
package/src/remote.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { askMissingObservationFollowUpContext, askObservationFollowUpContext, askRoutedContext } from './ask-coach.js';
|
|
4
|
+
import { contractVersion, writeCommandSchema } from './contract.js';
|
|
2
5
|
import { readSnapshot } from './local.js';
|
|
3
|
-
import {
|
|
6
|
+
import { sanitizeHistory } from './prompt-security.js';
|
|
7
|
+
import { executeCoachReadTool as executeLocalCoachReadTool, executeReadCommand, shouldKeepCurrentCoachObservation } from './queries.js';
|
|
4
8
|
import { resolveServiceUrl } from './service-url.js';
|
|
5
9
|
|
|
6
10
|
function notImplementedError() {
|
|
@@ -21,6 +25,111 @@ function authenticationFailedError() {
|
|
|
21
25
|
return error;
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
function insufficientScopeError(message = 'This agent token is read-only. Create or use a write-capable agent token for this command.') {
|
|
29
|
+
const error = new Error(message);
|
|
30
|
+
error.code = 'INSUFFICIENT_SCOPE';
|
|
31
|
+
// Structured remedy so an agent can act instead of retry-looping. Minting a
|
|
32
|
+
// write token requires a human login, so this is an escalation, not something
|
|
33
|
+
// the agent can self-correct.
|
|
34
|
+
error.requiredAccess = 'write';
|
|
35
|
+
error.requiresHuman = true;
|
|
36
|
+
error.remedy = 'A write-capable agent token is required. Minting one needs a human login: run `incremnt login`, then `incremnt agents create --access write`.';
|
|
37
|
+
return error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function remoteContractMismatchError(remoteVersion) {
|
|
41
|
+
const parsedRemoteVersion = Number(remoteVersion);
|
|
42
|
+
const remoteLabel = remoteVersion == null ? 'missing' : `v${remoteVersion}`;
|
|
43
|
+
const action = Number.isFinite(parsedRemoteVersion) && parsedRemoteVersion > contractVersion
|
|
44
|
+
? 'Update the incremnt CLI, then run incremnt login again.'
|
|
45
|
+
: 'Run incremnt login again after the service updates.';
|
|
46
|
+
const error = new Error(`Remote incremnt sync service is incompatible with this CLI. Expected contract v${contractVersion}, got ${remoteLabel}. ${action}`);
|
|
47
|
+
error.code = 'REMOTE_CONTRACT_MISMATCH';
|
|
48
|
+
return error;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function remoteTimeoutError(timeoutMs = REMOTE_ASK_PLAN_TIMEOUT_MS) {
|
|
52
|
+
const error = new Error(`Remote Ask planning timed out after ${Math.round(timeoutMs / 1000)} seconds.`);
|
|
53
|
+
error.code = 'REMOTE_TIMEOUT';
|
|
54
|
+
return error;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assertValidAskPlanQuestion(question) {
|
|
58
|
+
if (!question || typeof question !== 'string' || question.trim().length === 0) {
|
|
59
|
+
const error = new Error('question is required');
|
|
60
|
+
error.code = 'VALIDATION_ERROR';
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
if (question.length > 500) {
|
|
64
|
+
const error = new Error('question must be 500 characters or fewer');
|
|
65
|
+
error.code = 'VALIDATION_ERROR';
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const requiredAccessByWriteCommandId = new Map(
|
|
71
|
+
writeCommandSchema.map((command) => [command.id, command.requiredAccess ?? 'write'])
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
function requiredAccessForCommand(commandId) {
|
|
75
|
+
return requiredAccessByWriteCommandId.get(commandId) ?? 'read';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fail-closed gate for the write dispatch path: anything routed as a write
|
|
79
|
+
// command must be write-scoped UNLESS the contract explicitly marks it
|
|
80
|
+
// 'read'-access. An unknown id (e.g. a future write handler missing its schema
|
|
81
|
+
// entry) defaults to requiring write, so a read-only token can never slip
|
|
82
|
+
// through the client gate.
|
|
83
|
+
function writeCommandRequiresWriteScope(commandId) {
|
|
84
|
+
return (requiredAccessByWriteCommandId.get(commandId) ?? 'write') === 'write';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function ensureCompatibleRemoteContract(sessionState, transportOptions = {}) {
|
|
88
|
+
const baseUrl = sessionState.session?.transport?.baseUrl;
|
|
89
|
+
let remoteVersion = sessionState.session?.transport?.contractVersion;
|
|
90
|
+
if (sessionState.session?.transport?.uncheckedContract && transportOptions.resolveContract) {
|
|
91
|
+
const remoteContract = await transportOptions.resolveContract();
|
|
92
|
+
sessionState.session.transport = {
|
|
93
|
+
...(sessionState.session.transport ?? {}),
|
|
94
|
+
contractVersion: remoteContract.contractVersion,
|
|
95
|
+
capabilities: remoteContract.capabilities ?? null,
|
|
96
|
+
uncheckedContract: false
|
|
97
|
+
};
|
|
98
|
+
remoteVersion = remoteContract.contractVersion;
|
|
99
|
+
}
|
|
100
|
+
if (!baseUrl) return;
|
|
101
|
+
if (remoteVersion == null) {
|
|
102
|
+
throw remoteContractMismatchError(remoteVersion);
|
|
103
|
+
}
|
|
104
|
+
if (Number(remoteVersion) !== contractVersion) {
|
|
105
|
+
throw remoteContractMismatchError(remoteVersion);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function throwForbiddenResponse(response, sessionState, commandId) {
|
|
110
|
+
const payload = await response.json().catch(() => null);
|
|
111
|
+
const requiredAccess = requiredAccessForCommand(commandId);
|
|
112
|
+
const auth = sessionState.session?.auth;
|
|
113
|
+
const isAgentToken = auth?.type === 'agent-token';
|
|
114
|
+
// A token we know to be write-capable hit a genuine authorization failure, not
|
|
115
|
+
// a scope problem — never mask it as INSUFFICIENT_SCOPE.
|
|
116
|
+
const knownWriteCapable = isAgentToken && auth?.access === 'write';
|
|
117
|
+
// Trust the server's explicit code. Only synthesise a scope error when the
|
|
118
|
+
// server returned no code at all (older/edge paths) for an agent token of
|
|
119
|
+
// unknown-or-read scope hitting a write command — there a 403 can only be a
|
|
120
|
+
// scope problem (the service runs its scope check before any ownership check).
|
|
121
|
+
if (
|
|
122
|
+
payload?.code === 'INSUFFICIENT_SCOPE'
|
|
123
|
+
|| (!payload?.code && isAgentToken && !knownWriteCapable && requiredAccess === 'write')
|
|
124
|
+
) {
|
|
125
|
+
throw insufficientScopeError(payload?.error);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const error = new Error(payload?.error ?? `Remote request forbidden (HTTP ${response.status}).`);
|
|
129
|
+
error.code = payload?.code ?? 'REMOTE_FORBIDDEN';
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
|
|
24
133
|
const remoteCommandHandlers = {
|
|
25
134
|
'session-insights': executeRemoteRead,
|
|
26
135
|
'session-show': executeRemoteRead,
|
|
@@ -29,8 +138,11 @@ const remoteCommandHandlers = {
|
|
|
29
138
|
'program-list': executeRemoteRead,
|
|
30
139
|
'program-summary': executeRemoteRead,
|
|
31
140
|
'program-detail': executeRemoteRead,
|
|
141
|
+
'program-progress': executeRemoteRead,
|
|
142
|
+
'exercise-progress-summary': executeRemoteRead,
|
|
32
143
|
'cycle-summary-list': executeRemoteRead,
|
|
33
144
|
'cycle-summary-show': executeRemoteRead,
|
|
145
|
+
'cycle-progression-summary': executeRemoteRead,
|
|
34
146
|
'planned-vs-actual': executeRemoteRead,
|
|
35
147
|
'why-did-this-change': executeRemoteRead,
|
|
36
148
|
'goals-list': executeRemoteRead,
|
|
@@ -38,6 +150,7 @@ const remoteCommandHandlers = {
|
|
|
38
150
|
'health-summary': executeRemoteRead,
|
|
39
151
|
'health-ai': executeRemoteRead,
|
|
40
152
|
'training-load': executeRemoteRead,
|
|
153
|
+
'training-profile': executeRemoteRead,
|
|
41
154
|
'ask-history': executeRemoteRead,
|
|
42
155
|
'ask-show': executeRemoteRead,
|
|
43
156
|
'program-share-fetch': executeRemoteRead,
|
|
@@ -46,6 +159,21 @@ const remoteCommandHandlers = {
|
|
|
46
159
|
'coach-observations-current': executeRemoteRead
|
|
47
160
|
};
|
|
48
161
|
|
|
162
|
+
const REMOTE_ASK_PLAN_TIMEOUT_MS = 30_000;
|
|
163
|
+
|
|
164
|
+
async function fetchWithTimeout(url, init = {}, timeoutMs = REMOTE_ASK_PLAN_TIMEOUT_MS) {
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
167
|
+
try {
|
|
168
|
+
return await fetch(url, {
|
|
169
|
+
...init,
|
|
170
|
+
signal: controller.signal
|
|
171
|
+
});
|
|
172
|
+
} finally {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
49
177
|
async function executeRemoteRead(options, sessionState, normalizedCommand) {
|
|
50
178
|
const baseUrl = sessionState.session?.transport?.baseUrl;
|
|
51
179
|
if (baseUrl) {
|
|
@@ -81,7 +209,7 @@ async function executeRemoteHttpRead(baseUrl, sessionState, normalizedCommand, o
|
|
|
81
209
|
}
|
|
82
210
|
|
|
83
211
|
if (response.status === 403) {
|
|
84
|
-
|
|
212
|
+
await throwForbiddenResponse(response, sessionState, normalizedCommand);
|
|
85
213
|
}
|
|
86
214
|
|
|
87
215
|
if (response.status === 404) {
|
|
@@ -122,6 +250,13 @@ function endpointForCommand(baseUrl, normalizedCommand, options) {
|
|
|
122
250
|
return resolveServiceUrl(baseUrl, '/cli/programs/current');
|
|
123
251
|
case 'program-detail':
|
|
124
252
|
return resolveServiceUrl(baseUrl, options.id ? `/cli/programs/${encodeURIComponent(options.id)}` : '/cli/programs/active');
|
|
253
|
+
case 'program-progress': {
|
|
254
|
+
const url = resolveServiceUrl(baseUrl, '/cli/programs/progress');
|
|
255
|
+
if (options['program-id']) url.searchParams.set('program-id', options['program-id']);
|
|
256
|
+
if (options.since) url.searchParams.set('since', options.since);
|
|
257
|
+
if (options.limitExercises) url.searchParams.set('limitExercises', options.limitExercises);
|
|
258
|
+
return url;
|
|
259
|
+
}
|
|
125
260
|
case 'cycle-summary-list': {
|
|
126
261
|
const cyclesUrl = resolveServiceUrl(baseUrl, '/cli/cycles');
|
|
127
262
|
if (options['program-id']) {
|
|
@@ -131,11 +266,25 @@ function endpointForCommand(baseUrl, normalizedCommand, options) {
|
|
|
131
266
|
}
|
|
132
267
|
case 'cycle-summary-show':
|
|
133
268
|
return resolveServiceUrl(baseUrl, `/cli/cycles/${encodeURIComponent(options.id)}`);
|
|
269
|
+
case 'cycle-progression-summary': {
|
|
270
|
+
const url = resolveServiceUrl(baseUrl, '/cli/cycles/progress');
|
|
271
|
+
if (options['program-id']) url.searchParams.set('program-id', options['program-id']);
|
|
272
|
+
if (options.limit) url.searchParams.set('limit', options.limit);
|
|
273
|
+
return url;
|
|
274
|
+
}
|
|
134
275
|
case 'exercise-history': {
|
|
135
276
|
const historyUrl = resolveServiceUrl(baseUrl, '/cli/exercises/history');
|
|
136
277
|
historyUrl.searchParams.set('name', options.name ?? options.exercise);
|
|
137
278
|
return historyUrl;
|
|
138
279
|
}
|
|
280
|
+
case 'exercise-progress-summary': {
|
|
281
|
+
const url = resolveServiceUrl(baseUrl, '/cli/exercises/progress');
|
|
282
|
+
if (options.name ?? options.exercise) url.searchParams.set('name', options.name ?? options.exercise);
|
|
283
|
+
if (options.since) url.searchParams.set('since', options.since);
|
|
284
|
+
if (options['program-id']) url.searchParams.set('program-id', options['program-id']);
|
|
285
|
+
if (options.limit) url.searchParams.set('limit', options.limit);
|
|
286
|
+
return url;
|
|
287
|
+
}
|
|
139
288
|
case 'records':
|
|
140
289
|
return resolveServiceUrl(baseUrl, '/cli/records');
|
|
141
290
|
case 'goals-list':
|
|
@@ -153,6 +302,11 @@ function endpointForCommand(baseUrl, normalizedCommand, options) {
|
|
|
153
302
|
return resolveServiceUrl(baseUrl, '/cli/health/ai');
|
|
154
303
|
case 'training-load':
|
|
155
304
|
return resolveServiceUrl(baseUrl, '/cli/training-load');
|
|
305
|
+
case 'training-profile': {
|
|
306
|
+
const url = resolveServiceUrl(baseUrl, '/cli/training-profile');
|
|
307
|
+
if (options.since) url.searchParams.set('since', options.since);
|
|
308
|
+
return url;
|
|
309
|
+
}
|
|
156
310
|
case 'ask-history': {
|
|
157
311
|
const askUrl = resolveServiceUrl(baseUrl, '/cli/ask/history');
|
|
158
312
|
if (options.limit) {
|
|
@@ -223,7 +377,8 @@ async function executeRemoteCoachReadTool(toolName, input, sessionState) {
|
|
|
223
377
|
body: JSON.stringify(input ?? {})
|
|
224
378
|
});
|
|
225
379
|
|
|
226
|
-
if (response.status === 401
|
|
380
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
381
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'coach-read-tool');
|
|
227
382
|
if (response.status === 404) {
|
|
228
383
|
const error = new Error(`Unknown coach read tool: ${toolName}`);
|
|
229
384
|
error.code = 'REMOTE_NOT_FOUND';
|
|
@@ -244,6 +399,72 @@ async function executeRemoteCoachReadTool(toolName, input, sessionState) {
|
|
|
244
399
|
return executeLocalCoachReadTool(snapshot, toolName, input);
|
|
245
400
|
}
|
|
246
401
|
|
|
402
|
+
async function executeRemoteAskPlan(input, sessionState) {
|
|
403
|
+
const baseUrl = sessionState.session?.transport?.baseUrl;
|
|
404
|
+
if (baseUrl) {
|
|
405
|
+
const endpoint = resolveServiceUrl(baseUrl, '/cli/ask/plan');
|
|
406
|
+
let response;
|
|
407
|
+
try {
|
|
408
|
+
response = await fetchWithTimeout(endpoint, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: {
|
|
411
|
+
'Content-Type': 'application/json',
|
|
412
|
+
Authorization: `Bearer ${sessionState.session?.auth?.accessToken ?? ''}`
|
|
413
|
+
},
|
|
414
|
+
body: JSON.stringify(input ?? {})
|
|
415
|
+
});
|
|
416
|
+
} catch (error) {
|
|
417
|
+
if (error?.name === 'AbortError') throw remoteTimeoutError();
|
|
418
|
+
throw error;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
422
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'plan-ask-interaction');
|
|
423
|
+
if (!response.ok) {
|
|
424
|
+
const payload = await response.json().catch(() => null);
|
|
425
|
+
const error = new Error(payload?.error ?? `Unexpected error from incremnt sync service (HTTP ${response.status}).`);
|
|
426
|
+
error.code = 'REMOTE_HTTP_ERROR';
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
return response.json();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const fixturePath = sessionState.session?.transport?.fixturePath;
|
|
433
|
+
if (!fixturePath) throw notImplementedError();
|
|
434
|
+
assertValidAskPlanQuestion(input?.question);
|
|
435
|
+
const snapshot = await readSnapshot(fixturePath);
|
|
436
|
+
const exclude = new Set(String(input?.exclude ?? '').split(',').map((item) => item.trim()).filter(Boolean));
|
|
437
|
+
if (input?.coachObservation) {
|
|
438
|
+
const observationId = String(input.coachObservation.id ?? '').trim();
|
|
439
|
+
const snapshotObservation = (snapshot.coachObservations ?? [])
|
|
440
|
+
.find((observation) => (
|
|
441
|
+
String(observation?.id ?? '') === observationId &&
|
|
442
|
+
shouldKeepCurrentCoachObservation(observation, { includeOutcomeHistory: true })
|
|
443
|
+
));
|
|
444
|
+
const routedContext = snapshotObservation
|
|
445
|
+
? askObservationFollowUpContext(snapshot, input?.question, snapshotObservation, {
|
|
446
|
+
exclude,
|
|
447
|
+
intent: input.coachObservation.intent
|
|
448
|
+
})
|
|
449
|
+
: askMissingObservationFollowUpContext(snapshot, input?.question, input.coachObservation, {
|
|
450
|
+
exclude,
|
|
451
|
+
intent: input.coachObservation.intent
|
|
452
|
+
});
|
|
453
|
+
return {
|
|
454
|
+
contextBundle: routedContext.contextBundle,
|
|
455
|
+
metadata: routedContext.metadata
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
const routedContext = askRoutedContext(snapshot, input?.question, {
|
|
459
|
+
exclude,
|
|
460
|
+
history: sanitizeHistory(input?.history)
|
|
461
|
+
});
|
|
462
|
+
return {
|
|
463
|
+
contextBundle: routedContext.contextBundle,
|
|
464
|
+
metadata: routedContext.metadata
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
247
468
|
// Build the shape of a write request without executing it.
|
|
248
469
|
// Returns { method, url, body }. body is a parsed JS object or null.
|
|
249
470
|
// Used by the --dry-run path. Keep endpoint and body changes in sync with the
|
|
@@ -353,6 +574,48 @@ export async function buildWriteRequest(commandId, options, sessionState) {
|
|
|
353
574
|
body: {}
|
|
354
575
|
};
|
|
355
576
|
}
|
|
577
|
+
case 'coach-observations-outcome': {
|
|
578
|
+
if (!options.id) {
|
|
579
|
+
const error = new Error('--id is required for coach observations outcome.');
|
|
580
|
+
error.code = 'MISSING_OPTION';
|
|
581
|
+
throw error;
|
|
582
|
+
}
|
|
583
|
+
if (!options.status) {
|
|
584
|
+
const error = new Error('--status is required for coach observations outcome.');
|
|
585
|
+
error.code = 'MISSING_OPTION';
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
const body = { outcomeStatus: options.status };
|
|
589
|
+
if (options.observedAt) body.outcomeObservedAt = options.observedAt;
|
|
590
|
+
if (options.notes) body.outcomeNotes = options.notes;
|
|
591
|
+
if (options.linkedFollowupObservationId) {
|
|
592
|
+
body.linkedFollowupObservationId = options.linkedFollowupObservationId;
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
method: 'POST',
|
|
596
|
+
url: resolveServiceUrl(baseUrl, `/cli/coach-observations/${encodeURIComponent(options.id)}/outcome`).toString(),
|
|
597
|
+
body
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
case 'coach-observations-feedback': {
|
|
601
|
+
if (!options.id) {
|
|
602
|
+
const error = new Error('--id is required for coach observations feedback.');
|
|
603
|
+
error.code = 'MISSING_OPTION';
|
|
604
|
+
throw error;
|
|
605
|
+
}
|
|
606
|
+
if (!options.status) {
|
|
607
|
+
const error = new Error('--status is required for coach observations feedback.');
|
|
608
|
+
error.code = 'MISSING_OPTION';
|
|
609
|
+
throw error;
|
|
610
|
+
}
|
|
611
|
+
const body = { feedbackStatus: options.status };
|
|
612
|
+
if (options.feedbackAt) body.feedbackAt = options.feedbackAt;
|
|
613
|
+
return {
|
|
614
|
+
method: 'POST',
|
|
615
|
+
url: resolveServiceUrl(baseUrl, `/cli/coach-observations/${encodeURIComponent(options.id)}/feedback`).toString(),
|
|
616
|
+
body
|
|
617
|
+
};
|
|
618
|
+
}
|
|
356
619
|
default: {
|
|
357
620
|
const error = new Error(`Command ${commandId} does not support --dry-run.`);
|
|
358
621
|
error.code = 'UNSUPPORTED_DRY_RUN';
|
|
@@ -386,7 +649,8 @@ const remoteWriteCommandHandlers = {
|
|
|
386
649
|
body: JSON.stringify(proposal)
|
|
387
650
|
});
|
|
388
651
|
|
|
389
|
-
if (response.status === 401
|
|
652
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
653
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'programs-propose');
|
|
390
654
|
if (!response.ok) {
|
|
391
655
|
const payload = await response.json().catch(() => null);
|
|
392
656
|
const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
|
|
@@ -412,7 +676,8 @@ const remoteWriteCommandHandlers = {
|
|
|
412
676
|
}
|
|
413
677
|
});
|
|
414
678
|
|
|
415
|
-
if (response.status === 401
|
|
679
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
680
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'programs-proposals');
|
|
416
681
|
if (!response.ok) {
|
|
417
682
|
const payload = await response.json().catch(() => null);
|
|
418
683
|
const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
|
|
@@ -443,7 +708,8 @@ const remoteWriteCommandHandlers = {
|
|
|
443
708
|
body: JSON.stringify({ status: 'dismissed' })
|
|
444
709
|
});
|
|
445
710
|
|
|
446
|
-
if (response.status === 401
|
|
711
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
712
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'proposal-dismiss');
|
|
447
713
|
if (response.status === 404) {
|
|
448
714
|
const error = new Error(`Proposal not found: ${options.id}`);
|
|
449
715
|
error.code = 'REMOTE_NOT_FOUND';
|
|
@@ -476,7 +742,8 @@ const remoteWriteCommandHandlers = {
|
|
|
476
742
|
}
|
|
477
743
|
});
|
|
478
744
|
|
|
479
|
-
if (response.status === 401
|
|
745
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
746
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'program-share-create');
|
|
480
747
|
if (response.status === 404) {
|
|
481
748
|
const error = new Error(`Program not found: ${options['program-id']}`);
|
|
482
749
|
error.code = 'REMOTE_NOT_FOUND';
|
|
@@ -507,7 +774,8 @@ const remoteWriteCommandHandlers = {
|
|
|
507
774
|
}
|
|
508
775
|
});
|
|
509
776
|
|
|
510
|
-
if (response.status === 401
|
|
777
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
778
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'program-share-list');
|
|
511
779
|
if (!response.ok) {
|
|
512
780
|
const payload = await response.json().catch(() => null);
|
|
513
781
|
const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
|
|
@@ -544,7 +812,8 @@ const remoteWriteCommandHandlers = {
|
|
|
544
812
|
body: JSON.stringify(body)
|
|
545
813
|
});
|
|
546
814
|
|
|
547
|
-
if (response.status === 401
|
|
815
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
816
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'increment-score-upload');
|
|
548
817
|
if (!response.ok) {
|
|
549
818
|
const payload = await response.json().catch(() => null);
|
|
550
819
|
const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
|
|
@@ -572,7 +841,8 @@ const remoteWriteCommandHandlers = {
|
|
|
572
841
|
}
|
|
573
842
|
});
|
|
574
843
|
|
|
575
|
-
if (response.status === 401
|
|
844
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
845
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'program-share-revoke');
|
|
576
846
|
if (response.status === 404) {
|
|
577
847
|
const error = new Error(`Program share not found: ${options['share-id']}`);
|
|
578
848
|
error.code = 'REMOTE_NOT_FOUND';
|
|
@@ -608,7 +878,8 @@ const remoteWriteCommandHandlers = {
|
|
|
608
878
|
body: JSON.stringify(body)
|
|
609
879
|
});
|
|
610
880
|
|
|
611
|
-
if (response.status === 401
|
|
881
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
882
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'coach-observations-seen');
|
|
612
883
|
if (response.status === 404) {
|
|
613
884
|
const error = new Error(`Coach observation not found: ${options.id}`);
|
|
614
885
|
error.code = 'REMOTE_NOT_FOUND';
|
|
@@ -642,7 +913,54 @@ const remoteWriteCommandHandlers = {
|
|
|
642
913
|
body: JSON.stringify({})
|
|
643
914
|
});
|
|
644
915
|
|
|
645
|
-
if (response.status === 401
|
|
916
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
917
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'coach-observations-dismiss');
|
|
918
|
+
if (response.status === 404) {
|
|
919
|
+
const error = new Error(`Coach observation not found: ${options.id}`);
|
|
920
|
+
error.code = 'REMOTE_NOT_FOUND';
|
|
921
|
+
throw error;
|
|
922
|
+
}
|
|
923
|
+
if (!response.ok) {
|
|
924
|
+
const payload = await response.json().catch(() => null);
|
|
925
|
+
const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
|
|
926
|
+
error.code = 'REMOTE_HTTP_ERROR';
|
|
927
|
+
throw error;
|
|
928
|
+
}
|
|
929
|
+
return response.json();
|
|
930
|
+
},
|
|
931
|
+
|
|
932
|
+
'coach-observations-outcome': async (options, sessionState) => {
|
|
933
|
+
const baseUrl = sessionState.session?.transport?.baseUrl;
|
|
934
|
+
if (!baseUrl) throw notImplementedError();
|
|
935
|
+
if (!options.id) {
|
|
936
|
+
const error = new Error('--id is required for coach observations outcome.');
|
|
937
|
+
error.code = 'MISSING_OPTION';
|
|
938
|
+
throw error;
|
|
939
|
+
}
|
|
940
|
+
if (!options.status) {
|
|
941
|
+
const error = new Error('--status is required for coach observations outcome.');
|
|
942
|
+
error.code = 'MISSING_OPTION';
|
|
943
|
+
throw error;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const body = { outcomeStatus: options.status };
|
|
947
|
+
if (options.observedAt) body.outcomeObservedAt = options.observedAt;
|
|
948
|
+
if (options.notes) body.outcomeNotes = options.notes;
|
|
949
|
+
if (options.linkedFollowupObservationId) {
|
|
950
|
+
body.linkedFollowupObservationId = options.linkedFollowupObservationId;
|
|
951
|
+
}
|
|
952
|
+
const endpoint = resolveServiceUrl(baseUrl, `/cli/coach-observations/${encodeURIComponent(options.id)}/outcome`);
|
|
953
|
+
const response = await fetch(endpoint, {
|
|
954
|
+
method: 'POST',
|
|
955
|
+
headers: {
|
|
956
|
+
'Content-Type': 'application/json',
|
|
957
|
+
Authorization: `Bearer ${sessionState.session?.auth?.accessToken ?? ''}`
|
|
958
|
+
},
|
|
959
|
+
body: JSON.stringify(body)
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
963
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'coach-observations-outcome');
|
|
646
964
|
if (response.status === 404) {
|
|
647
965
|
const error = new Error(`Coach observation not found: ${options.id}`);
|
|
648
966
|
error.code = 'REMOTE_NOT_FOUND';
|
|
@@ -655,13 +973,133 @@ const remoteWriteCommandHandlers = {
|
|
|
655
973
|
throw error;
|
|
656
974
|
}
|
|
657
975
|
return response.json();
|
|
976
|
+
},
|
|
977
|
+
|
|
978
|
+
'coach-observations-feedback': async (options, sessionState) => {
|
|
979
|
+
const baseUrl = sessionState.session?.transport?.baseUrl;
|
|
980
|
+
if (!baseUrl) throw notImplementedError();
|
|
981
|
+
if (!options.id) {
|
|
982
|
+
const error = new Error('--id is required for coach observations feedback.');
|
|
983
|
+
error.code = 'MISSING_OPTION';
|
|
984
|
+
throw error;
|
|
985
|
+
}
|
|
986
|
+
if (!options.status) {
|
|
987
|
+
const error = new Error('--status is required for coach observations feedback.');
|
|
988
|
+
error.code = 'MISSING_OPTION';
|
|
989
|
+
throw error;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const body = { feedbackStatus: options.status };
|
|
993
|
+
if (options.feedbackAt) body.feedbackAt = options.feedbackAt;
|
|
994
|
+
const endpoint = resolveServiceUrl(baseUrl, `/cli/coach-observations/${encodeURIComponent(options.id)}/feedback`);
|
|
995
|
+
const response = await fetch(endpoint, {
|
|
996
|
+
method: 'POST',
|
|
997
|
+
headers: {
|
|
998
|
+
'Content-Type': 'application/json',
|
|
999
|
+
Authorization: `Bearer ${sessionState.session?.auth?.accessToken ?? ''}`
|
|
1000
|
+
},
|
|
1001
|
+
body: JSON.stringify(body)
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
1005
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'coach-observations-feedback');
|
|
1006
|
+
if (response.status === 404) {
|
|
1007
|
+
const error = new Error(`Coach observation not found: ${options.id}`);
|
|
1008
|
+
error.code = 'REMOTE_NOT_FOUND';
|
|
1009
|
+
throw error;
|
|
1010
|
+
}
|
|
1011
|
+
if (!response.ok) {
|
|
1012
|
+
const payload = await response.json().catch(() => null);
|
|
1013
|
+
const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
|
|
1014
|
+
error.code = 'REMOTE_HTTP_ERROR';
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
return response.json();
|
|
1018
|
+
},
|
|
1019
|
+
|
|
1020
|
+
'ask': async (options, sessionState) => {
|
|
1021
|
+
const baseUrl = sessionState.session?.transport?.baseUrl;
|
|
1022
|
+
if (!baseUrl) throw notImplementedError();
|
|
1023
|
+
const question = String(options.question ?? '').trim();
|
|
1024
|
+
if (!question) {
|
|
1025
|
+
const error = new Error('--question is required for ask.');
|
|
1026
|
+
error.code = 'MISSING_OPTION';
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
if (question.length > 500) {
|
|
1030
|
+
const error = new Error('--question must be 500 characters or fewer.');
|
|
1031
|
+
error.code = 'INVALID_OPTION';
|
|
1032
|
+
throw error;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
let history;
|
|
1036
|
+
if (options.history) {
|
|
1037
|
+
try {
|
|
1038
|
+
history = JSON.parse(options.history);
|
|
1039
|
+
} catch {
|
|
1040
|
+
const error = new Error('--history must be valid JSON (array of {role, content}).');
|
|
1041
|
+
error.code = 'INVALID_OPTION';
|
|
1042
|
+
throw error;
|
|
1043
|
+
}
|
|
1044
|
+
if (!Array.isArray(history)) {
|
|
1045
|
+
const error = new Error('--history must be a JSON array.');
|
|
1046
|
+
error.code = 'INVALID_OPTION';
|
|
1047
|
+
throw error;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const providedConversationId = String(
|
|
1052
|
+
options['conversation-id'] ?? options.conversationId ?? ''
|
|
1053
|
+
).trim();
|
|
1054
|
+
const conversationId = providedConversationId || randomUUID();
|
|
1055
|
+
|
|
1056
|
+
const body = { question, conversationId };
|
|
1057
|
+
if (history) body.history = history;
|
|
1058
|
+
if (options.exclude) body.exclude = options.exclude;
|
|
1059
|
+
if (options.tone) body.tone = options.tone;
|
|
1060
|
+
|
|
1061
|
+
const endpoint = resolveServiceUrl(baseUrl, '/cli/ask');
|
|
1062
|
+
const response = await fetch(endpoint, {
|
|
1063
|
+
method: 'POST',
|
|
1064
|
+
headers: {
|
|
1065
|
+
'Content-Type': 'application/json',
|
|
1066
|
+
Authorization: `Bearer ${sessionState.session?.auth?.accessToken ?? ''}`
|
|
1067
|
+
},
|
|
1068
|
+
body: JSON.stringify(body)
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
if (response.status === 401) throw authenticationFailedError();
|
|
1072
|
+
if (response.status === 403) await throwForbiddenResponse(response, sessionState, 'ask');
|
|
1073
|
+
if (!response.ok) {
|
|
1074
|
+
const payload = await response.json().catch(() => null);
|
|
1075
|
+
const error = new Error(payload?.error ?? `Unexpected error (HTTP ${response.status}).`);
|
|
1076
|
+
error.code = 'REMOTE_HTTP_ERROR';
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
1079
|
+
const payload = await response.json();
|
|
1080
|
+
const responseConversationId = typeof payload?.conversationId === 'string'
|
|
1081
|
+
? payload.conversationId.trim()
|
|
1082
|
+
: '';
|
|
1083
|
+
return {
|
|
1084
|
+
...payload,
|
|
1085
|
+
conversationId: responseConversationId || conversationId
|
|
1086
|
+
};
|
|
658
1087
|
}
|
|
659
1088
|
};
|
|
660
1089
|
|
|
661
1090
|
export function createRemoteTransport(sessionState, transportOptions = {}) {
|
|
1091
|
+
const auth = sessionState.session?.auth ?? {};
|
|
1092
|
+
const credential = {
|
|
1093
|
+
source: transportOptions.credentialSource ?? auth.source ?? 'session',
|
|
1094
|
+
type: auth.type ?? (auth.accessToken ? 'session' : null),
|
|
1095
|
+
access: auth.access ?? null,
|
|
1096
|
+
expiresAt: auth.expiresAt ?? null,
|
|
1097
|
+
canRefresh: Boolean(auth.accessToken && auth.type !== 'agent-token' && sessionState.session?.transport?.baseUrl)
|
|
1098
|
+
};
|
|
662
1099
|
return {
|
|
663
1100
|
kind: 'remote',
|
|
664
1101
|
account: sessionState.session?.account ?? null,
|
|
1102
|
+
credential,
|
|
665
1103
|
bootstrap: Boolean(sessionState.session?.transport?.fixturePath || sessionState.session?.transport?.baseUrl),
|
|
666
1104
|
source: sessionState.session?.transport?.baseUrl
|
|
667
1105
|
? 'http'
|
|
@@ -672,10 +1110,12 @@ export function createRemoteTransport(sessionState, transportOptions = {}) {
|
|
|
672
1110
|
contractVersion: sessionState.session?.transport?.contractVersion ?? null,
|
|
673
1111
|
capabilities: sessionState.session?.transport?.capabilities ?? null,
|
|
674
1112
|
fixturePath: sessionState.session?.transport?.fixturePath ?? null,
|
|
1113
|
+
contractUnchecked: Boolean(sessionState.session?.transport?.uncheckedContract),
|
|
675
1114
|
async executeReadCommand(normalizedCommand, options = {}) {
|
|
676
1115
|
if (transportOptions.expired) {
|
|
677
1116
|
throw expiredSessionError();
|
|
678
1117
|
}
|
|
1118
|
+
await ensureCompatibleRemoteContract(sessionState, transportOptions);
|
|
679
1119
|
|
|
680
1120
|
const handler = remoteCommandHandlers[normalizedCommand];
|
|
681
1121
|
if (!handler) {
|
|
@@ -690,13 +1130,26 @@ export function createRemoteTransport(sessionState, transportOptions = {}) {
|
|
|
690
1130
|
if (transportOptions.expired) {
|
|
691
1131
|
throw expiredSessionError();
|
|
692
1132
|
}
|
|
1133
|
+
await ensureCompatibleRemoteContract(sessionState, transportOptions);
|
|
693
1134
|
|
|
694
1135
|
return executeRemoteCoachReadTool(toolName, input, sessionState);
|
|
695
1136
|
},
|
|
1137
|
+
async planAskInteraction(input = {}) {
|
|
1138
|
+
if (transportOptions.expired) {
|
|
1139
|
+
throw expiredSessionError();
|
|
1140
|
+
}
|
|
1141
|
+
await ensureCompatibleRemoteContract(sessionState, transportOptions);
|
|
1142
|
+
|
|
1143
|
+
return executeRemoteAskPlan(input, sessionState);
|
|
1144
|
+
},
|
|
696
1145
|
async executeWriteCommand(normalizedCommand, options = {}) {
|
|
697
1146
|
if (transportOptions.expired) {
|
|
698
1147
|
throw expiredSessionError();
|
|
699
1148
|
}
|
|
1149
|
+
if (credential.type === 'agent-token' && credential.access === 'read' && writeCommandRequiresWriteScope(normalizedCommand)) {
|
|
1150
|
+
throw insufficientScopeError();
|
|
1151
|
+
}
|
|
1152
|
+
await ensureCompatibleRemoteContract(sessionState, transportOptions);
|
|
700
1153
|
|
|
701
1154
|
const handler = remoteWriteCommandHandlers[normalizedCommand];
|
|
702
1155
|
if (!handler) {
|