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/contract.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const contractVersion =
|
|
1
|
+
export const contractVersion = 21;
|
|
2
2
|
|
|
3
3
|
export const capabilities = {
|
|
4
4
|
readOnly: false,
|
|
@@ -6,7 +6,9 @@ export const capabilities = {
|
|
|
6
6
|
remoteReads: true,
|
|
7
7
|
remoteWrites: true,
|
|
8
8
|
remoteAuthShell: true,
|
|
9
|
-
remoteBootstrap: true
|
|
9
|
+
remoteBootstrap: true,
|
|
10
|
+
agentTokens: true,
|
|
11
|
+
cliSessionRefresh: true
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
// Single source of truth for all read commands.
|
|
@@ -77,6 +79,18 @@ export const commandSchema = [
|
|
|
77
79
|
{ name: 'id', type: 'string', format: 'id', required: false, description: 'Program ID (defaults to active program)' }
|
|
78
80
|
]
|
|
79
81
|
},
|
|
82
|
+
{
|
|
83
|
+
command: 'programs progress',
|
|
84
|
+
id: 'program-progress',
|
|
85
|
+
description: 'Summarize active program progress using cycle and exercise evidence',
|
|
86
|
+
supportsFields: true,
|
|
87
|
+
agentNotes: 'Use for broad program progress questions. Returns compact first/best/latest exercise evidence plus cycle and training-load context.',
|
|
88
|
+
options: [
|
|
89
|
+
{ name: 'program-id', type: 'string', format: 'id', required: false, description: 'Program ID (defaults to active program)' },
|
|
90
|
+
{ name: 'since', type: 'string', required: false, description: 'Start date for progress window (YYYY-MM-DD)' },
|
|
91
|
+
{ name: 'limitExercises', type: 'number', required: false, description: 'Max exercise rows to return (default 10, max 50)' }
|
|
92
|
+
]
|
|
93
|
+
},
|
|
80
94
|
{
|
|
81
95
|
command: 'exercises history',
|
|
82
96
|
id: 'exercise-history',
|
|
@@ -87,6 +101,19 @@ export const commandSchema = [
|
|
|
87
101
|
{ name: 'name', type: 'string', required: true, description: 'Exercise name' }
|
|
88
102
|
]
|
|
89
103
|
},
|
|
104
|
+
{
|
|
105
|
+
command: 'exercises progress',
|
|
106
|
+
id: 'exercise-progress-summary',
|
|
107
|
+
description: 'Summarize exercise first/best/latest progress over a date window',
|
|
108
|
+
supportsFields: true,
|
|
109
|
+
agentNotes: 'Use for longitudinal progress questions such as "what improved since January". Omit --name to summarize active-program exercises.',
|
|
110
|
+
options: [
|
|
111
|
+
{ name: 'name', type: 'string', required: false, description: 'Exercise name to scope progress' },
|
|
112
|
+
{ name: 'since', type: 'string', required: false, description: 'Start date for progress window (YYYY-MM-DD)' },
|
|
113
|
+
{ name: 'program-id', type: 'string', format: 'id', required: false, description: 'Program ID used to scope exercise names' },
|
|
114
|
+
{ name: 'limit', type: 'number', required: false, description: 'Max exercise rows to return (default 12, max 50)' }
|
|
115
|
+
]
|
|
116
|
+
},
|
|
90
117
|
{
|
|
91
118
|
command: 'records',
|
|
92
119
|
id: 'records',
|
|
@@ -130,6 +157,16 @@ export const commandSchema = [
|
|
|
130
157
|
{ name: 'id', type: 'string', format: 'id', required: true, description: 'Cycle summary ID' }
|
|
131
158
|
]
|
|
132
159
|
},
|
|
160
|
+
{
|
|
161
|
+
command: 'cycles progress',
|
|
162
|
+
id: 'cycle-progression-summary',
|
|
163
|
+
description: 'Summarize cycle progression counts and adherence',
|
|
164
|
+
supportsFields: true,
|
|
165
|
+
options: [
|
|
166
|
+
{ name: 'program-id', type: 'string', format: 'id', required: false, description: 'Filter by program ID' },
|
|
167
|
+
{ name: 'limit', type: 'number', required: false, description: 'Max cycles to return (default 8, max 20)' }
|
|
168
|
+
]
|
|
169
|
+
},
|
|
133
170
|
{
|
|
134
171
|
command: 'health summary',
|
|
135
172
|
id: 'health-summary',
|
|
@@ -151,6 +188,15 @@ export const commandSchema = [
|
|
|
151
188
|
description: 'Training load analysis (7-day vs 28-day comparison, effort scores, readiness via ATL/CTL/TSB, per-type breakdown)',
|
|
152
189
|
options: []
|
|
153
190
|
},
|
|
191
|
+
{
|
|
192
|
+
command: 'training profile',
|
|
193
|
+
id: 'training-profile',
|
|
194
|
+
description: 'Summarize stable lifter profile evidence from logs and current program',
|
|
195
|
+
supportsFields: true,
|
|
196
|
+
options: [
|
|
197
|
+
{ name: 'since', type: 'string', required: false, description: 'Start date for profile window (YYYY-MM-DD)' }
|
|
198
|
+
]
|
|
199
|
+
},
|
|
154
200
|
{
|
|
155
201
|
command: 'ask history',
|
|
156
202
|
id: 'ask-history',
|
|
@@ -209,7 +255,7 @@ export const commandSchema = [
|
|
|
209
255
|
id: 'coach-observations-current',
|
|
210
256
|
description: 'List current persisted coach observations',
|
|
211
257
|
supportsFields: true,
|
|
212
|
-
agentNotes: 'Read-only persisted insight surface. Returns generated/seen observations with evidence and
|
|
258
|
+
agentNotes: 'Read-only persisted insight surface. Returns generated/seen observations with evidence, lifecycle status, outcome tracking, and user feedback status. Use IDs from this command for CLI seen/dismiss/outcome/feedback actions.',
|
|
213
259
|
options: [
|
|
214
260
|
{ name: 'limit', type: 'number', required: false, description: 'Max observations to return (default 5, max 20)' }
|
|
215
261
|
]
|
|
@@ -232,6 +278,7 @@ export const writeCommandSchema = [
|
|
|
232
278
|
command: 'programs proposals',
|
|
233
279
|
id: 'programs-proposals',
|
|
234
280
|
description: 'List program proposals',
|
|
281
|
+
requiredAccess: 'read',
|
|
235
282
|
agentNotes: 'Pass --status to filter. Use this before programs proposal dismiss to look up proposal IDs.',
|
|
236
283
|
options: [
|
|
237
284
|
{ name: 'status', type: 'string', format: 'enum', enum: ['pending', 'accepted', 'dismissed'], required: false, description: 'Filter by status (pending, accepted, dismissed)' }
|
|
@@ -264,6 +311,7 @@ export const writeCommandSchema = [
|
|
|
264
311
|
id: 'program-share-list',
|
|
265
312
|
description: 'List share artifacts for one program',
|
|
266
313
|
usage: 'programs share list --program-id <program-id>',
|
|
314
|
+
requiredAccess: 'read',
|
|
267
315
|
agentNotes: 'Returns existing share-ids for the program. Use this to look up share-id before programs share revoke — the public token is NOT the share-id.',
|
|
268
316
|
options: [
|
|
269
317
|
{ name: 'program-id', type: 'string', format: 'id', required: true, description: 'Program ID' }
|
|
@@ -315,11 +363,65 @@ export const writeCommandSchema = [
|
|
|
315
363
|
options: [
|
|
316
364
|
{ name: 'id', type: 'string', format: 'id', required: true, description: 'Observation ID' }
|
|
317
365
|
]
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
command: 'coach observations outcome',
|
|
369
|
+
id: 'coach-observations-outcome',
|
|
370
|
+
description: 'Record whether a coach observation recommendation worked',
|
|
371
|
+
usage: 'coach observations outcome --id <observation-id> --status <improved|unchanged|regressed|inconclusive>',
|
|
372
|
+
dryRun: true,
|
|
373
|
+
mcp: false,
|
|
374
|
+
agentNotes: 'Mutation. Records recommendation outcome on the original coach_observations row so later Ask Coach turns can compare new sessions against prior advice. Does not change seen/dismissed lifecycle state.',
|
|
375
|
+
options: [
|
|
376
|
+
{ name: 'id', type: 'string', format: 'id', required: true, description: 'Observation ID' },
|
|
377
|
+
{ name: 'status', type: 'string', format: 'enum', enum: ['improved', 'unchanged', 'regressed', 'inconclusive'], required: true, description: 'Observed recommendation outcome' },
|
|
378
|
+
{ name: 'observedAt', type: 'string', required: false, description: 'Optional ISO timestamp for when the outcome was observed' },
|
|
379
|
+
{ name: 'notes', type: 'string', required: false, description: 'Optional outcome notes' },
|
|
380
|
+
{ name: 'linkedFollowupObservationId', type: 'string', format: 'id', required: false, description: 'Optional follow-up observation ID connected to the outcome' }
|
|
381
|
+
]
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
command: 'coach observations feedback',
|
|
385
|
+
id: 'coach-observations-feedback',
|
|
386
|
+
description: 'Record whether the user accepted or rejected a coach observation',
|
|
387
|
+
usage: 'coach observations feedback --id <observation-id> --status <accepted|rejected>',
|
|
388
|
+
dryRun: true,
|
|
389
|
+
mcp: false,
|
|
390
|
+
agentNotes: 'Mutation. Records explicit user agreement/disagreement separately from seen/dismissed visibility state.',
|
|
391
|
+
options: [
|
|
392
|
+
{ name: 'id', type: 'string', format: 'id', required: true, description: 'Observation ID' },
|
|
393
|
+
{ name: 'status', type: 'string', format: 'enum', enum: ['accepted', 'rejected'], required: true, description: 'User feedback status' },
|
|
394
|
+
{ name: 'feedbackAt', type: 'string', required: false, description: 'Optional ISO timestamp for when feedback was recorded' }
|
|
395
|
+
]
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
command: 'ask',
|
|
399
|
+
id: 'ask',
|
|
400
|
+
description: 'Ask the AI coach a question and return the answer',
|
|
401
|
+
usage: 'ask --question "How is my bench progressing?" [--conversation-id <id>] [--tone default|hype|numbers-only] [--exclude <items>] [--history <json>]',
|
|
402
|
+
dryRun: false,
|
|
403
|
+
mcp: true,
|
|
404
|
+
agentNotes: 'Mutation. Calls the remote Ask Coach pipeline; persists the conversation server-side. If --conversation-id is omitted, a new UUID is generated and returned in the response. Pass --history as JSON to continue an existing conversation. Tone defaults to "default".',
|
|
405
|
+
options: [
|
|
406
|
+
{ name: 'question', type: 'string', required: true, maxLength: 500, description: 'Question text (1-500 chars)' },
|
|
407
|
+
{ name: 'conversation-id', type: 'string', format: 'id', required: false, description: 'Conversation ID (auto-generated if omitted)' },
|
|
408
|
+
{ name: 'history', type: 'string', required: false, description: 'Prior conversation turns as JSON array of {role, content}' },
|
|
409
|
+
{ name: 'exclude', type: 'string', required: false, description: 'Comma-separated AI privacy exclusions (e.g. coach_observations)' },
|
|
410
|
+
{ name: 'tone', type: 'string', format: 'enum', enum: ['default', 'hype', 'numbers-only'], required: false, description: 'Response tone (default: default)' }
|
|
411
|
+
]
|
|
318
412
|
}
|
|
319
413
|
];
|
|
320
414
|
|
|
321
415
|
export const writeCommands = new Set(writeCommandSchema.map((c) => c.id));
|
|
322
416
|
|
|
417
|
+
for (const command of commandSchema) {
|
|
418
|
+
command.requiredAccess ??= 'read';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
for (const command of writeCommandSchema) {
|
|
422
|
+
command.requiredAccess ??= 'write';
|
|
423
|
+
}
|
|
424
|
+
|
|
323
425
|
export const proposalSchema = {
|
|
324
426
|
description: 'JSON structure for programs propose --file. Weights are omitted — iOS calculates from history.',
|
|
325
427
|
required: ['name', 'equipmentTier', 'days'],
|
|
@@ -354,6 +456,56 @@ export const proposalSchema = {
|
|
|
354
456
|
}
|
|
355
457
|
};
|
|
356
458
|
|
|
459
|
+
// Describes how token scope gates commands, so an agent can decide before
|
|
460
|
+
// calling rather than discovering limits reactively via a 403.
|
|
461
|
+
export const accessModel = {
|
|
462
|
+
description: 'Agent tokens (incr_agent_*) carry an access scope. A command runs only if the active token satisfies its requiredAccess. Read tokens cannot run write commands.',
|
|
463
|
+
scopes: ['read', 'write'],
|
|
464
|
+
check: 'The active token access (status.auth.credential.access) must satisfy each command\'s requiredAccess (see schema/writeSchema entries).',
|
|
465
|
+
onInsufficientScope: 'A 403 with code INSUFFICIENT_SCOPE includes requiredAccess and requiresHuman:true. Minting a write token needs a human login — escalate rather than retry.'
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// Structured definitions for the human-only `agents` token-management commands.
|
|
469
|
+
// These are NOT registered as MCP tools and CANNOT be run by an agent token
|
|
470
|
+
// (management requires a human session); they are described here so the command
|
|
471
|
+
// surface is fully discoverable from `incremnt contract`.
|
|
472
|
+
export const agentCommandSchema = [
|
|
473
|
+
{
|
|
474
|
+
id: 'agents-create',
|
|
475
|
+
command: 'agents create',
|
|
476
|
+
usage: 'agents create --name <name> [--access <read|write>] [--expires-days <n>] [--store]',
|
|
477
|
+
description: 'Mint a new headless agent token.',
|
|
478
|
+
requiresHuman: true,
|
|
479
|
+
agentNotes: 'An agent CANNOT run this — minting requires a human (non-agent-token) login. On INSUFFICIENT_SCOPE, escalate to an operator. The plaintext token is returned ONCE in agentToken.token; capture it immediately (tokenIssued:true even if local persistence fails). --store replaces the active CLI session with the new token.',
|
|
480
|
+
options: [
|
|
481
|
+
{ name: 'name', type: 'string', required: true, description: 'Human-readable label for the token.' },
|
|
482
|
+
{ name: 'access', type: 'string', format: 'enum', enum: ['read', 'write'], required: false, description: 'Token scope (default read). read = read-only; write = can mutate.' },
|
|
483
|
+
{ name: 'expires-days', type: 'number', required: false, description: 'Days until expiry (1-3650, default 90).' },
|
|
484
|
+
{ name: 'store', type: 'boolean', required: false, description: 'Persist the token as the active CLI session, replacing the current login.' }
|
|
485
|
+
]
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
id: 'agents-list',
|
|
489
|
+
command: 'agents list',
|
|
490
|
+
usage: 'agents list',
|
|
491
|
+
description: 'List agent tokens for the account (never returns the secret).',
|
|
492
|
+
requiresHuman: true,
|
|
493
|
+
agentNotes: 'Returns token ids, hints, scopes and expiry — not the secret. Use the id with agents revoke.',
|
|
494
|
+
options: []
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
id: 'agents-revoke',
|
|
498
|
+
command: 'agents revoke',
|
|
499
|
+
usage: 'agents revoke --id <agent-token-id>',
|
|
500
|
+
description: 'Revoke an agent token by id.',
|
|
501
|
+
requiresHuman: true,
|
|
502
|
+
agentNotes: 'Use the id from agents list (the agt_... id, not the public token).',
|
|
503
|
+
options: [
|
|
504
|
+
{ name: 'id', type: 'string', format: 'id', required: true, description: 'Agent token id (agt_...) to revoke.' }
|
|
505
|
+
]
|
|
506
|
+
}
|
|
507
|
+
];
|
|
508
|
+
|
|
357
509
|
export const officialCommands = [
|
|
358
510
|
...commandSchema.map((c) => c.usage ?? c.command),
|
|
359
511
|
...writeCommandSchema.map((c) => c.usage ?? c.command),
|
|
@@ -366,7 +518,12 @@ export const officialCommands = [
|
|
|
366
518
|
'login --snapshot <snapshot-file>',
|
|
367
519
|
'login --base-url <base-url> --token <token>',
|
|
368
520
|
'login --base-url <base-url> --email <email>',
|
|
521
|
+
'login --agent-token <token>',
|
|
369
522
|
'login --session-file <session-file>',
|
|
523
|
+
'agents create --name <name>',
|
|
524
|
+
'agents create --name <name> --access <read|write>',
|
|
525
|
+
'agents list',
|
|
526
|
+
'agents revoke --id <agent-token-id>',
|
|
370
527
|
'logout'
|
|
371
528
|
];
|
|
372
529
|
|
package/src/format.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { commandSchema, writeCommandSchema } from './contract.js';
|
|
2
|
+
import { agentCommandSchema, commandSchema, writeCommandSchema } from './contract.js';
|
|
3
3
|
|
|
4
4
|
const shortMonths = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
5
5
|
const shortDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
@@ -803,6 +803,15 @@ function formatCoachObservationsCurrent(payload) {
|
|
|
803
803
|
lines.push(` ${chalk.bold(observation.title ?? observation.kind ?? 'Observation')}${status}${confidence}`);
|
|
804
804
|
if (observation.summary) lines.push(` ${observation.summary}`);
|
|
805
805
|
if (observation.actionText) lines.push(` ${chalk.dim('Action')} ${observation.actionText}`);
|
|
806
|
+
if (observation.outcomeStatus) {
|
|
807
|
+
const outcomeMeta = observation.outcomeObservedAt ? ` (${observation.outcomeObservedAt})` : '';
|
|
808
|
+
const outcomeNotes = observation.outcomeNotes ? ` ${observation.outcomeNotes}` : '';
|
|
809
|
+
lines.push(` ${chalk.dim('Outcome')} ${observation.outcomeStatus}${outcomeMeta}${outcomeNotes}`);
|
|
810
|
+
}
|
|
811
|
+
if (observation.userFeedbackStatus) {
|
|
812
|
+
const feedbackMeta = observation.userFeedbackAt ? ` (${observation.userFeedbackAt})` : '';
|
|
813
|
+
lines.push(` ${chalk.dim('Feedback')} ${observation.userFeedbackStatus}${feedbackMeta}`);
|
|
814
|
+
}
|
|
806
815
|
if (observation.id) lines.push(` ${chalk.dim(observation.id)}`);
|
|
807
816
|
lines.push('');
|
|
808
817
|
}
|
|
@@ -817,6 +826,18 @@ function formatCoachObservationMutation(payload) {
|
|
|
817
826
|
return ` Coach observation ${chalk.bold(observation.id)} is ${chalk.bold(observation.status)}.`;
|
|
818
827
|
}
|
|
819
828
|
|
|
829
|
+
function formatCoachObservationOutcomeMutation(payload) {
|
|
830
|
+
const observation = payload?.observation;
|
|
831
|
+
if (!observation) return 'Coach observation not found.';
|
|
832
|
+
return ` Coach observation ${chalk.bold(observation.id)} outcome is ${chalk.bold(observation.outcomeStatus ?? 'unknown')}.`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function formatCoachObservationFeedbackMutation(payload) {
|
|
836
|
+
const observation = payload?.observation;
|
|
837
|
+
if (!observation) return 'Coach observation not found.';
|
|
838
|
+
return ` Coach observation ${chalk.bold(observation.id)} feedback is ${chalk.bold(observation.userFeedbackStatus ?? 'unknown')}.`;
|
|
839
|
+
}
|
|
840
|
+
|
|
820
841
|
// --- Main export ---
|
|
821
842
|
|
|
822
843
|
export function formatHelp(opts = {}) {
|
|
@@ -848,6 +869,9 @@ export function formatHelp(opts = {}) {
|
|
|
848
869
|
cmd('login --session-file <file>', 'Import session file'),
|
|
849
870
|
cmd('logout', 'Clear session'),
|
|
850
871
|
'',
|
|
872
|
+
header('AGENT TOKENS'),
|
|
873
|
+
...agentCommandSchema.map((c) => cmd(c.usage ?? c.command, c.description)),
|
|
874
|
+
'',
|
|
851
875
|
header('OTHER'),
|
|
852
876
|
cmd('browse', 'Interactive Ink browser'),
|
|
853
877
|
cmd('tui', 'Alias for browse'),
|
|
@@ -891,7 +915,9 @@ export function formatPretty(command, payload) {
|
|
|
891
915
|
'increment-score-upload': formatIncrementScoreUpload,
|
|
892
916
|
'coach-observations-current': formatCoachObservationsCurrent,
|
|
893
917
|
'coach-observations-seen': formatCoachObservationMutation,
|
|
894
|
-
'coach-observations-dismiss': formatCoachObservationMutation
|
|
918
|
+
'coach-observations-dismiss': formatCoachObservationMutation,
|
|
919
|
+
'coach-observations-outcome': formatCoachObservationOutcomeMutation,
|
|
920
|
+
'coach-observations-feedback': formatCoachObservationFeedbackMutation
|
|
895
921
|
}[command];
|
|
896
922
|
|
|
897
923
|
return formatter ? formatter(payload) : null;
|
package/src/lib.js
CHANGED
|
@@ -2,14 +2,18 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import { spawn, execFileSync } from 'node:child_process';
|
|
5
|
-
import { capabilities, commandSchema, contractVersion, officialCommands, proposalSchema, readCommands, writeCommands, writeCommandSchema } from './contract.js';
|
|
5
|
+
import { accessModel, agentCommandSchema, capabilities, commandSchema, contractVersion, officialCommands, proposalSchema, readCommands, writeCommands, writeCommandSchema } from './contract.js';
|
|
6
6
|
import {
|
|
7
7
|
bootstrapSessionFromRemoteBaseUrl,
|
|
8
|
+
bootstrapSessionFromRemoteBaseUrlWithAgentToken,
|
|
8
9
|
bootstrapSessionFromRemoteBaseUrlWithDeviceFlow,
|
|
9
10
|
bootstrapSessionFromRemoteBaseUrlWithEmail,
|
|
10
11
|
bootstrapSessionFromSnapshot,
|
|
12
|
+
createRemoteAgentToken,
|
|
11
13
|
fetchRemoteAuthConfig,
|
|
12
|
-
importSessionFile
|
|
14
|
+
importSessionFile,
|
|
15
|
+
listRemoteAgentTokens,
|
|
16
|
+
revokeRemoteAgentToken
|
|
13
17
|
} from './auth.js';
|
|
14
18
|
import { clearSessionState, isSessionExpired, readSessionState, resolveConfigDir } from './state.js';
|
|
15
19
|
import { createTransport } from './transport.js';
|
|
@@ -19,6 +23,7 @@ import { runBrowseCli } from './browse.js';
|
|
|
19
23
|
import { ValidationError, validateOptions } from './validate.js';
|
|
20
24
|
import { projectFields } from './fields.js';
|
|
21
25
|
import { buildWriteRequest } from './remote.js';
|
|
26
|
+
import { resolveConfiguredBaseUrl } from './service-url.js';
|
|
22
27
|
|
|
23
28
|
const schemaById = new Map([
|
|
24
29
|
...commandSchema.map((c) => [c.id, c]),
|
|
@@ -96,7 +101,44 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
96
101
|
return 0;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
let transport;
|
|
105
|
+
try {
|
|
106
|
+
transport = await createTransport(options, sessionState);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// `status` is a diagnostic command: if the transport can't be built (e.g. the
|
|
109
|
+
// refresh endpoint is unreachable), still report local session state plus the
|
|
110
|
+
// transport error rather than hard-failing. Other commands fail with exit 1.
|
|
111
|
+
if (normalizedCommand === 'status') {
|
|
112
|
+
stdout.write(`${JSON.stringify({
|
|
113
|
+
contractVersion,
|
|
114
|
+
capabilities,
|
|
115
|
+
mode: null,
|
|
116
|
+
config: {
|
|
117
|
+
dir: resolveConfigDir(),
|
|
118
|
+
sessionPath: sessionState.path
|
|
119
|
+
},
|
|
120
|
+
auth: {
|
|
121
|
+
loggedIn: Boolean(sessionState.session),
|
|
122
|
+
sessionExists: sessionState.exists,
|
|
123
|
+
sessionSchemaVersion: sessionState.session?.version ?? null,
|
|
124
|
+
expired: isSessionExpired(sessionState.session),
|
|
125
|
+
error: sessionState.error,
|
|
126
|
+
account: sessionState.session?.account ?? null,
|
|
127
|
+
credential: null,
|
|
128
|
+
authExpired: isSessionExpired(sessionState.session),
|
|
129
|
+
reauthCommand: 'incremnt login'
|
|
130
|
+
},
|
|
131
|
+
transportError: cliErrorPayload(error)
|
|
132
|
+
}, null, 2)}\n`);
|
|
133
|
+
return 0;
|
|
134
|
+
}
|
|
135
|
+
if (options.json) {
|
|
136
|
+
stdout.write(`${JSON.stringify(cliErrorPayload(error), null, 2)}\n`);
|
|
137
|
+
} else {
|
|
138
|
+
stderr.write(`${error.message}\n`);
|
|
139
|
+
}
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
100
142
|
|
|
101
143
|
if (normalizedCommand === 'status') {
|
|
102
144
|
stdout.write(`${JSON.stringify({
|
|
@@ -108,12 +150,26 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
108
150
|
sessionPath: sessionState.path
|
|
109
151
|
},
|
|
110
152
|
auth: {
|
|
111
|
-
loggedIn: Boolean(sessionState.session),
|
|
153
|
+
loggedIn: Boolean(sessionState.session || transport.credential?.source === 'env'),
|
|
112
154
|
sessionExists: sessionState.exists,
|
|
113
155
|
sessionSchemaVersion: sessionState.session?.version ?? null,
|
|
114
|
-
expired: isSessionExpired(sessionState.session),
|
|
156
|
+
expired: transport.expired ?? isSessionExpired(sessionState.session),
|
|
115
157
|
error: sessionState.error,
|
|
116
|
-
account: sessionState.session?.account ?? null
|
|
158
|
+
account: transport.account ?? sessionState.session?.account ?? null,
|
|
159
|
+
credential: transport.kind === 'remote'
|
|
160
|
+
? {
|
|
161
|
+
source: transport.credential?.source ?? null,
|
|
162
|
+
type: transport.credential?.type ?? null,
|
|
163
|
+
access: transport.credential?.access ?? null,
|
|
164
|
+
expiresAt: transport.credential?.expiresAt ?? null,
|
|
165
|
+
canRefresh: transport.credential?.canRefresh ?? false,
|
|
166
|
+
reauthCommand: transport.credential?.type === 'agent-token'
|
|
167
|
+
? 'incremnt login --agent-token <token>'
|
|
168
|
+
: 'incremnt login'
|
|
169
|
+
}
|
|
170
|
+
: null,
|
|
171
|
+
authExpired: transport.expired ?? isSessionExpired(sessionState.session),
|
|
172
|
+
reauthCommand: 'incremnt login'
|
|
117
173
|
},
|
|
118
174
|
remote: {
|
|
119
175
|
bootstrap: transport.kind === 'remote' ? transport.bootstrap ?? false : false,
|
|
@@ -121,7 +177,8 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
121
177
|
baseUrl: transport.kind === 'remote' ? transport.baseUrl ?? null : null,
|
|
122
178
|
contractVersion: transport.kind === 'remote' ? transport.contractVersion ?? null : null,
|
|
123
179
|
capabilities: transport.kind === 'remote' ? transport.capabilities ?? null : null,
|
|
124
|
-
fixturePath: transport.kind === 'remote' ? transport.fixturePath ?? null : null
|
|
180
|
+
fixturePath: transport.kind === 'remote' ? transport.fixturePath ?? null : null,
|
|
181
|
+
contractUnchecked: transport.kind === 'remote' ? transport.contractUnchecked ?? false : false
|
|
125
182
|
},
|
|
126
183
|
snapshot: {
|
|
127
184
|
source: transport.snapshotSource?.source ?? null,
|
|
@@ -139,6 +196,8 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
139
196
|
officialCommands,
|
|
140
197
|
schema: commandSchema,
|
|
141
198
|
writeSchema: writeCommandSchema,
|
|
199
|
+
agentSchema: agentCommandSchema,
|
|
200
|
+
accessModel,
|
|
142
201
|
proposalSchema
|
|
143
202
|
}, null, 2)}\n`);
|
|
144
203
|
return 0;
|
|
@@ -247,6 +306,27 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
247
306
|
const loginBaseUrl = resolveLoginBaseUrl(options, sessionState);
|
|
248
307
|
|
|
249
308
|
if (loginBaseUrl) {
|
|
309
|
+
if (options['agent-token']) {
|
|
310
|
+
try {
|
|
311
|
+
const result = await bootstrapSessionFromRemoteBaseUrlWithAgentToken(loginBaseUrl, options['agent-token']);
|
|
312
|
+
stdout.write(`${JSON.stringify({
|
|
313
|
+
ok: true,
|
|
314
|
+
sessionPath: result.path,
|
|
315
|
+
account: result.session.account,
|
|
316
|
+
credential: result.session.auth ? {
|
|
317
|
+
type: result.session.auth.type ?? null,
|
|
318
|
+
access: result.session.auth.access ?? null
|
|
319
|
+
} : null,
|
|
320
|
+
remoteContractVersion: result.session.transport?.contractVersion ?? null,
|
|
321
|
+
transport: result.session.transport
|
|
322
|
+
}, null, 2)}\n`);
|
|
323
|
+
return 0;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
stderr.write(`${error.message}\n`);
|
|
326
|
+
return 1;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
250
330
|
if (options.token && options.email) {
|
|
251
331
|
stderr.write('Pass either --token or --email with --base-url, not both\n');
|
|
252
332
|
return 1;
|
|
@@ -348,6 +428,104 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
348
428
|
return 1;
|
|
349
429
|
}
|
|
350
430
|
|
|
431
|
+
if (normalizedCommand === 'agents create' || normalizedCommand === 'agents list' || normalizedCommand === 'agents revoke') {
|
|
432
|
+
const currentSessionState = await readSessionState();
|
|
433
|
+
const currentSession = currentSessionState.session ?? sessionState.session;
|
|
434
|
+
const baseUrl = options['base-url'] ?? currentSession?.transport?.baseUrl ?? null;
|
|
435
|
+
const bearerToken = currentSession?.auth?.type === 'agent-token'
|
|
436
|
+
? null
|
|
437
|
+
: currentSession?.auth?.accessToken;
|
|
438
|
+
|
|
439
|
+
if (!baseUrl || !bearerToken) {
|
|
440
|
+
const message = 'Agent token management requires a human login. Run incremnt login first.';
|
|
441
|
+
if (options.json) {
|
|
442
|
+
stdout.write(`${JSON.stringify({ error: message, code: 'REMOTE_AUTH_REQUIRED', reauthCommand: 'incremnt login' }, null, 2)}\n`);
|
|
443
|
+
} else {
|
|
444
|
+
stderr.write(`${message}\n`);
|
|
445
|
+
}
|
|
446
|
+
return 1;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
if (normalizedCommand === 'agents create') {
|
|
451
|
+
const name = typeof options.name === 'string' ? options.name.trim() : '';
|
|
452
|
+
if (!name) {
|
|
453
|
+
throw Object.assign(new Error('--name is required for agents create.'), { code: 'MISSING_OPTION' });
|
|
454
|
+
}
|
|
455
|
+
const access = options.access ?? 'read';
|
|
456
|
+
const expiresDays = options['expires-days'] ?? 90;
|
|
457
|
+
const payload = await createRemoteAgentToken(baseUrl, bearerToken, {
|
|
458
|
+
name,
|
|
459
|
+
access,
|
|
460
|
+
expiresDays
|
|
461
|
+
});
|
|
462
|
+
let stored = null;
|
|
463
|
+
let warning = null;
|
|
464
|
+
if (options.store) {
|
|
465
|
+
try {
|
|
466
|
+
const storedSession = await bootstrapSessionFromRemoteBaseUrlWithAgentToken(baseUrl, payload.agentToken.token, {
|
|
467
|
+
access: payload.agentToken.access,
|
|
468
|
+
expiresAt: payload.agentToken.expiresAt ?? null
|
|
469
|
+
});
|
|
470
|
+
stored = {
|
|
471
|
+
ok: true,
|
|
472
|
+
sessionPath: storedSession.path,
|
|
473
|
+
credential: {
|
|
474
|
+
type: storedSession.session.auth?.type ?? null,
|
|
475
|
+
access: storedSession.session.auth?.access ?? null
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
// --store replaces the active CLI session with this agent token. The
|
|
479
|
+
// human login that authorised this command is now gone; flag it so the
|
|
480
|
+
// caller isn't silently demoted (token management needs a human login).
|
|
481
|
+
warning = 'The active CLI session is now this agent token. Run `incremnt login` to restore your human session (required for agent token management).';
|
|
482
|
+
} catch (error) {
|
|
483
|
+
stored = {
|
|
484
|
+
ok: false,
|
|
485
|
+
error: error?.message ?? String(error),
|
|
486
|
+
code: error?.code ?? null
|
|
487
|
+
};
|
|
488
|
+
// The token was minted server-side and is shown once below. Do NOT fail
|
|
489
|
+
// the command — a non-zero exit invites callers to discard stdout and
|
|
490
|
+
// retry, orphaning a live token. Surface the salvage instruction instead.
|
|
491
|
+
warning = 'Token was created but could not be stored locally. Capture agentToken.token now — it cannot be retrieved again — and set INCREMNT_AGENT_TOKEN manually.';
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
stdout.write(`${JSON.stringify({
|
|
495
|
+
...payload,
|
|
496
|
+
tokenIssued: true,
|
|
497
|
+
...(options.store ? { persisted: Boolean(stored?.ok) } : {}),
|
|
498
|
+
stored,
|
|
499
|
+
...(warning ? { warning } : {})
|
|
500
|
+
}, null, 2)}\n`);
|
|
501
|
+
// Exit 0: the primary operation (minting the token) succeeded regardless of
|
|
502
|
+
// local persistence. Persistence failures are reported via `persisted`/`warning`.
|
|
503
|
+
return 0;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (normalizedCommand === 'agents list') {
|
|
507
|
+
const payload = await listRemoteAgentTokens(baseUrl, bearerToken);
|
|
508
|
+
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const id = typeof options.id === 'string' ? options.id.trim() : '';
|
|
513
|
+
if (!id) {
|
|
514
|
+
throw Object.assign(new Error('--id is required for agents revoke.'), { code: 'MISSING_OPTION' });
|
|
515
|
+
}
|
|
516
|
+
const payload = await revokeRemoteAgentToken(baseUrl, bearerToken, id);
|
|
517
|
+
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
518
|
+
return 0;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
if (options.json) {
|
|
521
|
+
stdout.write(`${JSON.stringify(cliErrorPayload(error), null, 2)}\n`);
|
|
522
|
+
} else {
|
|
523
|
+
stderr.write(`${error.message}\n`);
|
|
524
|
+
}
|
|
525
|
+
return 1;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
351
529
|
if (normalizedCommand === 'browse') {
|
|
352
530
|
if (!(stdout.isTTY ?? false) || !(process.stdin.isTTY ?? false)) {
|
|
353
531
|
stderr.write('browse requires an interactive TTY.\n');
|
|
@@ -403,7 +581,7 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
403
581
|
return 0;
|
|
404
582
|
} catch (error) {
|
|
405
583
|
if (explicitJson) {
|
|
406
|
-
stdout.write(`${JSON.stringify(
|
|
584
|
+
stdout.write(`${JSON.stringify(cliErrorPayload(error), null, 2)}\n`);
|
|
407
585
|
} else {
|
|
408
586
|
stderr.write(`${error.message}\n`);
|
|
409
587
|
}
|
|
@@ -423,7 +601,7 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
423
601
|
return 0;
|
|
424
602
|
} catch (error) {
|
|
425
603
|
if (explicitJson) {
|
|
426
|
-
stdout.write(`${JSON.stringify(
|
|
604
|
+
stdout.write(`${JSON.stringify(cliErrorPayload(error), null, 2)}\n`);
|
|
427
605
|
} else {
|
|
428
606
|
stderr.write(`${error.message}\n`);
|
|
429
607
|
}
|
|
@@ -501,7 +679,7 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
501
679
|
return 0;
|
|
502
680
|
} catch (error) {
|
|
503
681
|
if (explicitJson) {
|
|
504
|
-
stdout.write(`${JSON.stringify(
|
|
682
|
+
stdout.write(`${JSON.stringify(cliErrorPayload(error), null, 2)}\n`);
|
|
505
683
|
} else {
|
|
506
684
|
stderr.write(`${error.message}\n`);
|
|
507
685
|
}
|
|
@@ -510,14 +688,24 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
510
688
|
}
|
|
511
689
|
}
|
|
512
690
|
|
|
513
|
-
const DEFAULT_BASE_URL = 'https://incremnt-sync.onrender.com';
|
|
514
|
-
|
|
515
691
|
function resolveLoginBaseUrl(options, sessionState) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
692
|
+
return resolveConfiguredBaseUrl(options, sessionState.session);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function cliErrorPayload(error) {
|
|
696
|
+
const code = error?.code ?? null;
|
|
697
|
+
return {
|
|
698
|
+
error: error?.message ?? String(error),
|
|
699
|
+
code,
|
|
700
|
+
...(code === 'SESSION_EXPIRED' ? { authExpired: true, reauthCommand: 'incremnt login' } : {}),
|
|
701
|
+
...(code === 'REMOTE_AUTH_ERROR' ? { reauthCommand: 'incremnt login' } : {}),
|
|
702
|
+
...(code === 'SNAPSHOT_NOT_FOUND' ? { reauthCommand: 'incremnt login' } : {}),
|
|
703
|
+
...(code === 'INSUFFICIENT_SCOPE' ? {
|
|
704
|
+
requiredAccess: error?.requiredAccess ?? 'write',
|
|
705
|
+
requiresHuman: error?.requiresHuman ?? true,
|
|
706
|
+
remedy: error?.remedy ?? 'A write-capable agent token is required. Minting one needs a human login: run `incremnt login`, then `incremnt agents create --access write`.'
|
|
707
|
+
} : {})
|
|
708
|
+
};
|
|
521
709
|
}
|
|
522
710
|
|
|
523
711
|
function isHeadlessLoginEnvironment(options) {
|