sentinelayer-cli 0.43.0 → 0.45.0
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 +29 -16
- package/package.json +1 -1
- package/src/agents/jules/tools/aidenid-email.js +7 -9
- package/src/agents/jules/tools/frontend-analyze.js +15 -3
- package/src/agents/jules/tools/runtime-audit.js +51 -21
- package/src/ai/aidenid.js +5 -4
- package/src/ai/proxy.js +15 -14
- package/src/audit/orchestrator.js +8 -5
- package/src/auth/admission-scope.js +5 -3
- package/src/auth/credential-destinations.js +221 -0
- package/src/auth/gate.js +4 -4
- package/src/auth/http.js +22 -5
- package/src/auth/service.js +52 -56
- package/src/cli.js +14 -6
- package/src/commands/ai/provision-governance.js +7 -6
- package/src/commands/init.js +25 -16
- package/src/commands/scan.js +6 -4
- package/src/commands/session.js +166 -51
- package/src/commands/spec.js +2 -2
- package/src/commands/watch.js +4 -3
- package/src/config/service.js +37 -0
- package/src/daemon/pulse.js +2 -1
- package/src/daemon/watchdog.js +117 -68
- package/src/legacy-cli.js +119 -39
- package/src/mcp/bridge-inputs.js +223 -0
- package/src/mcp/cli-command-tools.js +160 -7
- package/src/mcp/registry.js +6 -5
- package/src/mcp/session-stdio-server.js +157 -8
- package/src/mcp/smoke.js +27 -11
- package/src/mcp/token-service.js +7 -9
- package/src/memory/retrieval.js +14 -5
- package/src/pocket/ring-owner.js +13 -8
- package/src/review/ai-review.js +2 -2
- package/src/review/dd-report-email-client.js +4 -5
- package/src/session/admission-access.js +41 -10
- package/src/session/admission-auth.js +95 -16
- package/src/session/admission.js +91 -21
- package/src/session/checkpoints.js +11 -10
- package/src/session/control-events.js +2 -0
- package/src/session/file-locks.js +19 -18
- package/src/session/invitations.js +26 -19
- package/src/session/live-source.js +13 -13
- package/src/session/reactions.js +258 -0
- package/src/session/route-classes.js +107 -0
- package/src/session/sync.js +110 -66
- package/src/session/tickets.js +21 -19
- package/src/session/title-sync.js +4 -3
- package/src/telemetry/sync.js +8 -9
package/src/cli.js
CHANGED
|
@@ -242,6 +242,11 @@ export async function buildCliProgram({
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
245
|
+
// Where the user's credentials may go is decided once, here, from the real environment and the
|
|
246
|
+
// global config, and frozen: nothing that changes process.env later can move it.
|
|
247
|
+
const { freezeProcessTrustContext } = await import("./auth/credential-destinations.js");
|
|
248
|
+
await freezeProcessTrustContext();
|
|
249
|
+
|
|
245
250
|
// Normalize slash commands (/omargate → omargate, /audit → audit, etc.)
|
|
246
251
|
const normalizedArgs = normalizeSlashArgs(rawArgs);
|
|
247
252
|
|
|
@@ -269,15 +274,18 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
|
|
|
269
274
|
// the subcommand (say, post-agent, observe, locks, checkpoints, ...), so no
|
|
270
275
|
// subcommand, present or future, can reach the human token. A tombstoned
|
|
271
276
|
// admission refuses here, before any request.
|
|
272
|
-
const { assertDispatchMatchesScope, resolveAgentAdmissionTarget, withAgentAdmission } =
|
|
273
|
-
"./session/admission-auth.js"
|
|
274
|
-
);
|
|
277
|
+
const { assertDispatchMatchesScope, resolveAgentAdmissionTarget, withAgentAdmission } =
|
|
278
|
+
await import("./session/admission-auth.js");
|
|
275
279
|
const target = await resolveAgentAdmissionTarget(normalizedArgs);
|
|
276
|
-
// Execution must match authorization: before ANY action runs,
|
|
277
|
-
//
|
|
280
|
+
// Execution must match authorization: before ANY action runs, decide on Commander's parsed
|
|
281
|
+
// arguments and options -- the values the action will use. Through the MCP CLI bridge in an
|
|
282
|
+
// agent context the action binds to its session's admission, runs a legacy data-plane route
|
|
283
|
+
// as before, or is refused (src/session/route-classes.js); and the identity it resolves must
|
|
284
|
+
// be the one whose scope is in force.
|
|
278
285
|
program.hook("preAction", (_hooked, actionCommand) => assertDispatchMatchesScope(actionCommand));
|
|
279
286
|
if (target) {
|
|
280
|
-
|
|
287
|
+
// An admission seen above that is gone by the time it is read refuses: never a fallback.
|
|
288
|
+
await withAgentAdmission(target.sessionId, target.agentId, dispatch, { requireStored: true });
|
|
281
289
|
return;
|
|
282
290
|
}
|
|
283
291
|
await dispatch();
|
|
@@ -25,8 +25,7 @@ import {
|
|
|
25
25
|
import { listSites, recordTemporarySite } from "../../ai/site-store.js";
|
|
26
26
|
import { getIdentityById, recordProvisionedIdentity } from "../../ai/identity-store.js";
|
|
27
27
|
import { resolveOutputRoot } from "../../config/service.js";
|
|
28
|
-
import {
|
|
29
|
-
import { fetchAidenIdCredentials } from "../../auth/service.js";
|
|
28
|
+
import { fetchAidenIdCredentials, resolveActiveAuthSession } from "../../auth/service.js";
|
|
30
29
|
import {
|
|
31
30
|
buildCurlPreview,
|
|
32
31
|
normalizeIdempotencyKey,
|
|
@@ -37,6 +36,7 @@ import {
|
|
|
37
36
|
stableTimestampForFile,
|
|
38
37
|
writeArtifact,
|
|
39
38
|
} from "./shared.js";
|
|
39
|
+
import { isAuthenticated } from "../../auth/credential-destinations.js";
|
|
40
40
|
|
|
41
41
|
function addProvisionEmailOptions(cmd) {
|
|
42
42
|
return cmd
|
|
@@ -92,12 +92,13 @@ async function provisionEmailAction(options, command) {
|
|
|
92
92
|
payload,
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
const session = await resolveActiveAuthSession({ cwd: process.cwd(), env: process.env, autoRotate: false }).catch(
|
|
96
|
+
() => null,
|
|
97
|
+
);
|
|
97
98
|
|
|
98
99
|
const makeFetcher = () => {
|
|
99
|
-
if (!session
|
|
100
|
-
return () => fetchAidenIdCredentials({ apiUrl: session.apiUrl,
|
|
100
|
+
if (!isAuthenticated(session)) return null;
|
|
101
|
+
return () => fetchAidenIdCredentials({ apiUrl: session.apiUrl, auth: session });
|
|
101
102
|
};
|
|
102
103
|
|
|
103
104
|
const resolvedCredentials = await resolveAidenIdCredentials({
|
package/src/commands/init.js
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
|
|
3
|
-
import { loadConfig } from "../config/service.js";
|
|
3
|
+
import { loadConfig, userConfigValues } from "../config/service.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
// Config values the scaffold flow reads from the environment. The API it talks to and the token
|
|
6
|
+
// it sends are never set here: they come from the trust context and the auth service
|
|
7
|
+
// (src/auth/credential-destinations.js), so a workspace .sentinelayer.yml cannot choose them,
|
|
8
|
+
// and child processes inherit neither.
|
|
9
|
+
// Provider keys come from the user's own config (global, or the environment), never the workspace.
|
|
10
|
+
function applyConfigEnvDefaults(config) {
|
|
11
|
+
const resolvedConfig = config.resolved;
|
|
12
|
+
const userValues = userConfigValues(config);
|
|
9
13
|
if (!process.env.SENTINELAYER_WEB_URL && resolvedConfig.webUrl) {
|
|
10
|
-
process.env.SENTINELAYER_WEB_URL = resolvedConfig.webUrl;
|
|
11
|
-
}
|
|
12
|
-
if (!process.env.SENTINELAYER_TOKEN && resolvedConfig.sentinelayerToken) {
|
|
13
|
-
process.env.SENTINELAYER_TOKEN = resolvedConfig.sentinelayerToken;
|
|
14
|
+
process.env.SENTINELAYER_WEB_URL = resolvedConfig.webUrl; // printed as a link only
|
|
14
15
|
}
|
|
15
|
-
if (!process.env.OPENAI_API_KEY &&
|
|
16
|
-
process.env.OPENAI_API_KEY =
|
|
16
|
+
if (!process.env.OPENAI_API_KEY && userValues.openaiApiKey) {
|
|
17
|
+
process.env.OPENAI_API_KEY = userValues.openaiApiKey;
|
|
17
18
|
}
|
|
18
|
-
if (!process.env.ANTHROPIC_API_KEY &&
|
|
19
|
-
process.env.ANTHROPIC_API_KEY =
|
|
19
|
+
if (!process.env.ANTHROPIC_API_KEY && userValues.anthropicApiKey) {
|
|
20
|
+
process.env.ANTHROPIC_API_KEY = userValues.anthropicApiKey;
|
|
20
21
|
}
|
|
21
|
-
if (!process.env.GOOGLE_API_KEY &&
|
|
22
|
-
process.env.GOOGLE_API_KEY =
|
|
22
|
+
if (!process.env.GOOGLE_API_KEY && userValues.googleApiKey) {
|
|
23
|
+
process.env.GOOGLE_API_KEY = userValues.googleApiKey;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -30,9 +31,11 @@ export function registerInitCommand(program, invokeLegacy) {
|
|
|
30
31
|
.option("--non-interactive", "Disable prompts and require interview payload")
|
|
31
32
|
.option("--interview-file <path>", "Load interview JSON from file")
|
|
32
33
|
.option("--skip-browser-open", "Do not auto-open browser during auth")
|
|
34
|
+
.option("--inject-secret", "Set the project token as a GitHub Actions secret on this directory's git remote")
|
|
35
|
+
.option("--inject-openai-key", "Also set OPENAI_API_KEY from your environment as a GitHub Actions secret there")
|
|
33
36
|
.action(async (projectName, options) => {
|
|
34
37
|
const config = await loadConfig();
|
|
35
|
-
applyConfigEnvDefaults(config
|
|
38
|
+
applyConfigEnvDefaults(config);
|
|
36
39
|
|
|
37
40
|
const legacyArgs = [];
|
|
38
41
|
const normalizedProjectName = String(projectName || "").trim();
|
|
@@ -49,6 +52,12 @@ export function registerInitCommand(program, invokeLegacy) {
|
|
|
49
52
|
if (options.skipBrowserOpen) {
|
|
50
53
|
legacyArgs.push("--skip-browser-open");
|
|
51
54
|
}
|
|
55
|
+
if (options.injectSecret) {
|
|
56
|
+
legacyArgs.push("--inject-secret");
|
|
57
|
+
}
|
|
58
|
+
if (options.injectOpenaiKey) {
|
|
59
|
+
legacyArgs.push("--inject-openai-key");
|
|
60
|
+
}
|
|
52
61
|
|
|
53
62
|
await invokeLegacy(legacyArgs);
|
|
54
63
|
});
|
package/src/commands/scan.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
resolveProvider,
|
|
13
13
|
} from "../ai/client.js";
|
|
14
14
|
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
15
|
-
import { loadConfig, resolveOutputRoot } from "../config/service.js";
|
|
15
|
+
import { loadConfig, resolveOutputRoot, userConfigValues } from "../config/service.js";
|
|
16
16
|
import { evaluateBudget } from "../cost/budget.js";
|
|
17
17
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
18
18
|
import { estimateModelCost } from "../cost/tracker.js";
|
|
@@ -34,6 +34,7 @@ import { detectRepoSlug, setupSecrets } from "../scan/gh-secrets.js";
|
|
|
34
34
|
import { appendRunEvent, deriveStopClassFromBudget } from "../telemetry/ledger.js";
|
|
35
35
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
36
36
|
import { authLoginHint } from "../ui/command-hints.js";
|
|
37
|
+
import { exportCredentialToken, isAuthenticated } from "../auth/credential-destinations.js";
|
|
37
38
|
|
|
38
39
|
const LEGACY_SCAN_WORKFLOW_PATH = ".github/workflows/security-review.yml";
|
|
39
40
|
|
|
@@ -578,7 +579,7 @@ export function registerScanCommand(program) {
|
|
|
578
579
|
configModel: config.resolved.defaultModelId,
|
|
579
580
|
});
|
|
580
581
|
const explicitApiKey = String(options.apiKey || "").trim();
|
|
581
|
-
const configuredApiKey = resolveConfiguredApiKey(resolvedProvider, config
|
|
582
|
+
const configuredApiKey = resolveConfiguredApiKey(resolvedProvider, userConfigValues(config));
|
|
582
583
|
|
|
583
584
|
const prompt = buildAiPreScanPrompt({
|
|
584
585
|
targetPath,
|
|
@@ -856,8 +857,9 @@ export function registerScanCommand(program) {
|
|
|
856
857
|
env: process.env,
|
|
857
858
|
autoRotate: false,
|
|
858
859
|
});
|
|
859
|
-
if (session
|
|
860
|
-
|
|
860
|
+
if (isAuthenticated(session)) {
|
|
861
|
+
// the one reviewed export of the token: to a GitHub Actions secret, through gh
|
|
862
|
+
tokenValue = exportCredentialToken(session.credential, { purpose: "github-actions-secret" });
|
|
861
863
|
}
|
|
862
864
|
} catch {
|
|
863
865
|
/* no active auth session */
|
package/src/commands/session.js
CHANGED
|
@@ -7,6 +7,7 @@ import { spawn as defaultSpawn } from "node:child_process";
|
|
|
7
7
|
|
|
8
8
|
import pc from "picocolors";
|
|
9
9
|
|
|
10
|
+
import { credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
|
|
10
11
|
import { SentinelayerApiError, requestJsonMutation } from "../auth/http.js";
|
|
11
12
|
import {
|
|
12
13
|
buildProvisionEmailPayload,
|
|
@@ -15,7 +16,6 @@ import {
|
|
|
15
16
|
resolveAidenIdCredentials,
|
|
16
17
|
} from "../ai/aidenid.js";
|
|
17
18
|
import { recordProvisionedIdentity } from "../ai/identity-store.js";
|
|
18
|
-
import { readStoredSession } from "../auth/session-store.js";
|
|
19
19
|
import { fetchAidenIdCredentials } from "../auth/service.js";
|
|
20
20
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
21
21
|
import { resolveOutputRoot } from "../config/service.js";
|
|
@@ -111,6 +111,11 @@ import {
|
|
|
111
111
|
currentAdmittedAgent,
|
|
112
112
|
withAgentAdmission,
|
|
113
113
|
} from "../session/admission-auth.js";
|
|
114
|
+
import {
|
|
115
|
+
SESSION_REACTION_TYPES,
|
|
116
|
+
isSessionReactionType,
|
|
117
|
+
submitSessionReaction,
|
|
118
|
+
} from "../session/reactions.js";
|
|
114
119
|
import {
|
|
115
120
|
claimTicket,
|
|
116
121
|
listTickets,
|
|
@@ -564,10 +569,16 @@ const SESSION_MESSAGE_ACTION_TYPES = new Set([
|
|
|
564
569
|
"reply",
|
|
565
570
|
"like",
|
|
566
571
|
"dislike",
|
|
572
|
+
"unlike",
|
|
573
|
+
"undislike",
|
|
567
574
|
"disregard",
|
|
568
575
|
"view",
|
|
569
576
|
]);
|
|
570
577
|
|
|
578
|
+
// like/dislike/unlike/undislike all go through src/session/reactions.js, the path the
|
|
579
|
+
// local MCP server shares; `ack` keeps the plain message-action path.
|
|
580
|
+
const SESSION_REACT_COMMAND_TYPES = Object.freeze(["ack", ...SESSION_REACTION_TYPES]);
|
|
581
|
+
|
|
571
582
|
const SESSION_MESSAGE_ACTION_ALIASES = new Map([
|
|
572
583
|
["comment", "reply"],
|
|
573
584
|
]);
|
|
@@ -599,6 +610,16 @@ const SESSION_MESSAGE_ACTION_DESCRIPTIONS = Object.freeze([
|
|
|
599
610
|
command: "sl session react <id> dislike --target-sequence <n>",
|
|
600
611
|
description: "Negative lightweight feedback. Use --target-action-id <uuid> to react to a threaded reply.",
|
|
601
612
|
},
|
|
613
|
+
{
|
|
614
|
+
type: "unlike",
|
|
615
|
+
command: "sl session react <id> unlike --target-sequence <n>",
|
|
616
|
+
description: "Retract your own active like. Refused when you have none; needs a server with reaction undo.",
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
type: "undislike",
|
|
620
|
+
command: "sl session react <id> undislike --target-sequence <n>",
|
|
621
|
+
description: "Retract your own active dislike. Refused when you have none; needs a server with reaction undo.",
|
|
622
|
+
},
|
|
602
623
|
{
|
|
603
624
|
type: "disregard",
|
|
604
625
|
command: "sl session action <id> disregard --target-sequence <n>",
|
|
@@ -630,7 +651,7 @@ function shortSha256(value) {
|
|
|
630
651
|
|
|
631
652
|
function actionEventType(actionType) {
|
|
632
653
|
if (actionType === "reply") return "session_reply";
|
|
633
|
-
if (actionType
|
|
654
|
+
if (isSessionReactionType(actionType)) return "session_reaction";
|
|
634
655
|
return "session_action";
|
|
635
656
|
}
|
|
636
657
|
|
|
@@ -686,6 +707,7 @@ function buildSessionActionEvent(sessionId, action = {}) {
|
|
|
686
707
|
}),
|
|
687
708
|
);
|
|
688
709
|
const actorId = actionActorId(action);
|
|
710
|
+
const collapsedIntoActionId = normalizeString(action.collapsedIntoActionId ?? action.collapsed_into_action_id);
|
|
689
711
|
const event = createAgentEvent({
|
|
690
712
|
event: actionEventType(actionType),
|
|
691
713
|
agent: {
|
|
@@ -710,6 +732,8 @@ function buildSessionActionEvent(sessionId, action = {}) {
|
|
|
710
732
|
metadata: action.metadata && typeof action.metadata === "object" ? action.metadata : undefined,
|
|
711
733
|
message: actionDisplayMessage(action),
|
|
712
734
|
source: "session_action",
|
|
735
|
+
// A retained no-op reaction key (sentinelayer-api#914): evidence, not a reaction.
|
|
736
|
+
...(collapsedIntoActionId ? { collapsedIntoActionId } : {}),
|
|
713
737
|
},
|
|
714
738
|
});
|
|
715
739
|
event.eventId = `session-action-${id}`;
|
|
@@ -1298,19 +1322,11 @@ async function resolveSessionRemoteSyncState({ dashboardUrl } = {}) {
|
|
|
1298
1322
|
};
|
|
1299
1323
|
}
|
|
1300
1324
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
storedSession = await readStoredSession();
|
|
1304
|
-
} catch {
|
|
1305
|
-
storedSession = null;
|
|
1306
|
-
}
|
|
1307
|
-
const apiUrl =
|
|
1308
|
-
normalizeString(storedSession?.apiUrl) ||
|
|
1309
|
-
normalizeString(process.env.SENTINELAYER_API_URL) ||
|
|
1310
|
-
"https://api.sentinelayer.com";
|
|
1311
|
-
const hasToken = Boolean(
|
|
1312
|
-
normalizeString(storedSession?.token) || normalizeString(process.env.SENTINELAYER_TOKEN),
|
|
1325
|
+
const auth = await resolveActiveAuthSession({ cwd: process.cwd(), env: process.env, autoRotate: false }).catch(
|
|
1326
|
+
() => null,
|
|
1313
1327
|
);
|
|
1328
|
+
const apiUrl = normalizeString(auth?.apiUrl) || "https://api.sentinelayer.com";
|
|
1329
|
+
const hasToken = isAuthenticated(auth);
|
|
1314
1330
|
if (!hasToken) {
|
|
1315
1331
|
return {
|
|
1316
1332
|
status: "auth_required",
|
|
@@ -1434,12 +1450,12 @@ async function findReusableSessionCandidate({
|
|
|
1434
1450
|
// - When `SENTINELAYER_SKIP_REMOTE_SYNC=1` (test bootstrap), short-circuits
|
|
1435
1451
|
// to `{ ok: true, source: "skipped", session: null }` so unit tests
|
|
1436
1452
|
// can exercise the local materialization path without a real API.
|
|
1437
|
-
async function fetchRemoteSessionDetail(endpoint,
|
|
1453
|
+
async function fetchRemoteSessionDetail(endpoint, credential) {
|
|
1438
1454
|
let response;
|
|
1439
1455
|
let payload;
|
|
1440
1456
|
try {
|
|
1441
1457
|
({ response, payload } = await fetchJsonWithFullTimeout(
|
|
1442
|
-
endpoint, { method: "GET",
|
|
1458
|
+
endpoint, { method: "GET", credential, redirect: "error" }, 2_000,
|
|
1443
1459
|
));
|
|
1444
1460
|
} catch (err) {
|
|
1445
1461
|
return {
|
|
@@ -1492,7 +1508,7 @@ async function verifyRemoteSession(sessionId, { targetPath } = {}) {
|
|
|
1492
1508
|
} catch {
|
|
1493
1509
|
return { ok: false, reason: "no_session" };
|
|
1494
1510
|
}
|
|
1495
|
-
if (!auth
|
|
1511
|
+
if (!isAuthenticated(auth)) {
|
|
1496
1512
|
return { ok: false, reason: "not_authenticated", status: 401 };
|
|
1497
1513
|
}
|
|
1498
1514
|
const apiUrl = String(auth.apiUrl || "").replace(/\/+$/, "");
|
|
@@ -1500,10 +1516,10 @@ async function verifyRemoteSession(sessionId, { targetPath } = {}) {
|
|
|
1500
1516
|
return { ok: false, reason: "no_api_url" };
|
|
1501
1517
|
}
|
|
1502
1518
|
const endpoint = `${apiUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}`;
|
|
1503
|
-
const
|
|
1504
|
-
const firstAttempt = await fetchRemoteSessionDetail(endpoint,
|
|
1519
|
+
const credential = await credentialFor(auth);
|
|
1520
|
+
const firstAttempt = await fetchRemoteSessionDetail(endpoint, credential);
|
|
1505
1521
|
const detail = firstAttempt.retryable
|
|
1506
|
-
? await fetchRemoteSessionDetail(endpoint,
|
|
1522
|
+
? await fetchRemoteSessionDetail(endpoint, credential)
|
|
1507
1523
|
: firstAttempt;
|
|
1508
1524
|
if (detail.ok) {
|
|
1509
1525
|
return {
|
|
@@ -2758,7 +2774,7 @@ async function resolveAdminApiSession({ targetPath, explicitApiUrl }) {
|
|
|
2758
2774
|
explicitApiUrl,
|
|
2759
2775
|
autoRotate: true,
|
|
2760
2776
|
});
|
|
2761
|
-
if (!session
|
|
2777
|
+
if (!isAuthenticated(session)) {
|
|
2762
2778
|
throw new Error(`No active auth token found. Run \`${authLoginHint()}\` first.`);
|
|
2763
2779
|
}
|
|
2764
2780
|
return session;
|
|
@@ -2778,10 +2794,8 @@ async function postAdminSessionMutation({
|
|
|
2778
2794
|
return requestJsonMutation(`${apiUrl}${pathSuffix}`, {
|
|
2779
2795
|
method: "POST",
|
|
2780
2796
|
operationName,
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
...headers,
|
|
2784
|
-
},
|
|
2797
|
+
credential: await credentialFor(session),
|
|
2798
|
+
headers,
|
|
2785
2799
|
body,
|
|
2786
2800
|
});
|
|
2787
2801
|
}
|
|
@@ -3282,7 +3296,7 @@ export function registerSessionCommand(program) {
|
|
|
3282
3296
|
env: process.env,
|
|
3283
3297
|
autoRotate: false,
|
|
3284
3298
|
});
|
|
3285
|
-
if (!session
|
|
3299
|
+
if (!isAuthenticated(session) || !session?.apiUrl) {
|
|
3286
3300
|
throw new Error(`Not authenticated. Run \`${authLoginHint()}\` first.`);
|
|
3287
3301
|
}
|
|
3288
3302
|
const apiUrl = String(session.apiUrl).replace(/\/+$/, "");
|
|
@@ -3291,7 +3305,7 @@ export function registerSessionCommand(program) {
|
|
|
3291
3305
|
{
|
|
3292
3306
|
method: "POST",
|
|
3293
3307
|
operationName: "session.set_title",
|
|
3294
|
-
|
|
3308
|
+
credential: session.credential,
|
|
3295
3309
|
body: { title: normalizedTitle },
|
|
3296
3310
|
},
|
|
3297
3311
|
);
|
|
@@ -3332,7 +3346,7 @@ export function registerSessionCommand(program) {
|
|
|
3332
3346
|
env: process.env,
|
|
3333
3347
|
autoRotate: false,
|
|
3334
3348
|
});
|
|
3335
|
-
if (!session
|
|
3349
|
+
if (!isAuthenticated(session) || !session?.apiUrl) {
|
|
3336
3350
|
throw new Error(`Not authenticated. Run \`${authLoginHint()}\` first.`);
|
|
3337
3351
|
}
|
|
3338
3352
|
const apiUrl = String(session.apiUrl).replace(/\/+$/, "");
|
|
@@ -3341,7 +3355,7 @@ export function registerSessionCommand(program) {
|
|
|
3341
3355
|
{
|
|
3342
3356
|
method: "POST",
|
|
3343
3357
|
operationName: "session.sweep_empty",
|
|
3344
|
-
|
|
3358
|
+
credential: session.credential,
|
|
3345
3359
|
body: {
|
|
3346
3360
|
cutoffMinutes,
|
|
3347
3361
|
maxEvents,
|
|
@@ -4282,6 +4296,21 @@ export function registerSessionCommand(program) {
|
|
|
4282
4296
|
);
|
|
4283
4297
|
}
|
|
4284
4298
|
const agentId = identity.agentId;
|
|
4299
|
+
if (isSessionReactionType(normalizedActionType)) {
|
|
4300
|
+
return runReactionCommand({
|
|
4301
|
+
sessionId: normalizedSessionId,
|
|
4302
|
+
reaction: normalizedActionType,
|
|
4303
|
+
agentId,
|
|
4304
|
+
targetPath,
|
|
4305
|
+
targetSequenceId,
|
|
4306
|
+
targetCursor,
|
|
4307
|
+
targetActionId,
|
|
4308
|
+
note,
|
|
4309
|
+
options,
|
|
4310
|
+
command,
|
|
4311
|
+
commandName,
|
|
4312
|
+
});
|
|
4313
|
+
}
|
|
4285
4314
|
const idempotencyKey =
|
|
4286
4315
|
normalizeString(options.idempotencyKey) ||
|
|
4287
4316
|
defaultActionIdempotencyKey({
|
|
@@ -4364,6 +4393,89 @@ export function registerSessionCommand(program) {
|
|
|
4364
4393
|
return payload;
|
|
4365
4394
|
}
|
|
4366
4395
|
|
|
4396
|
+
// like/dislike/unlike/undislike: the shared reaction path (src/session/reactions.js)
|
|
4397
|
+
// decides admission, the operation key and the outcome; this only mirrors and prints.
|
|
4398
|
+
// `--idempotency-key` is a RETRY of the intent that minted that key.
|
|
4399
|
+
async function runReactionCommand({
|
|
4400
|
+
sessionId,
|
|
4401
|
+
reaction,
|
|
4402
|
+
agentId,
|
|
4403
|
+
targetPath,
|
|
4404
|
+
targetSequenceId,
|
|
4405
|
+
targetCursor,
|
|
4406
|
+
targetActionId,
|
|
4407
|
+
note,
|
|
4408
|
+
options,
|
|
4409
|
+
command,
|
|
4410
|
+
commandName,
|
|
4411
|
+
}) {
|
|
4412
|
+
const outcome = await submitSessionReaction({
|
|
4413
|
+
surface: "cli",
|
|
4414
|
+
sessionId,
|
|
4415
|
+
agentId,
|
|
4416
|
+
reaction,
|
|
4417
|
+
targetSequenceId,
|
|
4418
|
+
targetCursor,
|
|
4419
|
+
targetActionId,
|
|
4420
|
+
note,
|
|
4421
|
+
idempotencyKey: options.idempotencyKey,
|
|
4422
|
+
targetPath,
|
|
4423
|
+
});
|
|
4424
|
+
let event = null;
|
|
4425
|
+
let localAppend = { appended: false, reason: "not_recorded" };
|
|
4426
|
+
if (outcome.action) {
|
|
4427
|
+
const appended = await appendActionEventIfMissing(
|
|
4428
|
+
sessionId,
|
|
4429
|
+
buildSessionActionEvent(sessionId, outcome.action),
|
|
4430
|
+
{ targetPath },
|
|
4431
|
+
);
|
|
4432
|
+
event = appended.event;
|
|
4433
|
+
localAppend = { appended: Boolean(appended.appended), reason: appended.reason || "" };
|
|
4434
|
+
}
|
|
4435
|
+
let evidenceEvent = null;
|
|
4436
|
+
if (outcome.collapsedActionId && outcome.action) {
|
|
4437
|
+
// Mirror the server's retained no-op row: this request's key, collapsed into
|
|
4438
|
+
// the active reaction it repeated.
|
|
4439
|
+
const appended = await appendActionEventIfMissing(
|
|
4440
|
+
sessionId,
|
|
4441
|
+
buildSessionActionEvent(sessionId, {
|
|
4442
|
+
id: outcome.collapsedActionId,
|
|
4443
|
+
actionType: reaction,
|
|
4444
|
+
targetSequenceId: outcome.action.targetSequenceId,
|
|
4445
|
+
targetCursor: outcome.action.targetCursor,
|
|
4446
|
+
targetActionId: outcome.action.targetActionId,
|
|
4447
|
+
actorKind: outcome.action.actorKind,
|
|
4448
|
+
actorId: outcome.action.actorId,
|
|
4449
|
+
actorRole: outcome.action.actorRole,
|
|
4450
|
+
idempotencyKey: outcome.operationKey,
|
|
4451
|
+
collapsedIntoActionId: outcome.action.id,
|
|
4452
|
+
}),
|
|
4453
|
+
{ targetPath },
|
|
4454
|
+
);
|
|
4455
|
+
evidenceEvent = appended.event;
|
|
4456
|
+
}
|
|
4457
|
+
const payload = {
|
|
4458
|
+
command: commandName,
|
|
4459
|
+
targetPath,
|
|
4460
|
+
...outcome,
|
|
4461
|
+
event,
|
|
4462
|
+
evidenceEvent,
|
|
4463
|
+
localAppend,
|
|
4464
|
+
};
|
|
4465
|
+
if (!outcome.ok) process.exitCode = 1;
|
|
4466
|
+
if (shouldEmitJson(options, command)) {
|
|
4467
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4468
|
+
return payload;
|
|
4469
|
+
}
|
|
4470
|
+
if (!outcome.ok) {
|
|
4471
|
+
console.error(pc.yellow(outcome.message));
|
|
4472
|
+
return payload;
|
|
4473
|
+
}
|
|
4474
|
+
if (event) console.log(formatEventLine(event));
|
|
4475
|
+
if (outcome.outcome !== "applied" || !event) console.log(pc.gray(outcome.message));
|
|
4476
|
+
return payload;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4367
4479
|
session
|
|
4368
4480
|
.command("actions")
|
|
4369
4481
|
.description("List supported low-noise message actions with examples")
|
|
@@ -4390,7 +4502,7 @@ export function registerSessionCommand(program) {
|
|
|
4390
4502
|
session
|
|
4391
4503
|
.command("action <sessionId> <actionType>")
|
|
4392
4504
|
.description(
|
|
4393
|
-
"Create a message action for a target session event (ack, working_on, reply/comment, like, dislike, disregard, view)",
|
|
4505
|
+
"Create a message action for a target session event (ack, working_on, reply/comment, like, dislike, unlike, undislike, disregard, view)",
|
|
4394
4506
|
)
|
|
4395
4507
|
.option("--target-sequence <n>", "Target event sequence id")
|
|
4396
4508
|
.option("--target-cursor <cursor>", "Target event cursor")
|
|
@@ -4406,18 +4518,23 @@ export function registerSessionCommand(program) {
|
|
|
4406
4518
|
|
|
4407
4519
|
session
|
|
4408
4520
|
.command("react <sessionId> <reaction>")
|
|
4409
|
-
.description(
|
|
4521
|
+
.description(
|
|
4522
|
+
"React to or acknowledge a target session event with ack, like, or dislike; retract your own like/dislike with unlike/undislike",
|
|
4523
|
+
)
|
|
4410
4524
|
.option("--target-sequence <n>", "Target event sequence id")
|
|
4411
4525
|
.option("--target-cursor <cursor>", "Target event cursor")
|
|
4412
4526
|
.option("--target-action-id <uuid>", "Target a threaded reply/action by action UUID")
|
|
4413
4527
|
.option("--agent <id>", "Agent id authoring the action (defaults to the joined session agent)")
|
|
4414
|
-
.option(
|
|
4528
|
+
.option(
|
|
4529
|
+
"--idempotency-key <key>",
|
|
4530
|
+
"Retry an earlier reaction as the SAME intent by resending its operationKey (without it, each call is a new intent)",
|
|
4531
|
+
)
|
|
4415
4532
|
.option("--path <path>", "Workspace path for the session", ".")
|
|
4416
4533
|
.option("--json", "Emit machine-readable output")
|
|
4417
4534
|
.action(async (sessionId, reaction, options, command) => {
|
|
4418
4535
|
const normalizedReaction = normalizeSessionMessageActionType(reaction);
|
|
4419
|
-
if (!
|
|
4420
|
-
throw new Error(
|
|
4536
|
+
if (!SESSION_REACT_COMMAND_TYPES.includes(normalizedReaction)) {
|
|
4537
|
+
throw new Error(`reaction must be one of: ${SESSION_REACT_COMMAND_TYPES.join(", ")}.`);
|
|
4421
4538
|
}
|
|
4422
4539
|
await runMessageActionCommand({
|
|
4423
4540
|
sessionId,
|
|
@@ -6273,7 +6390,7 @@ export function registerSessionCommand(program) {
|
|
|
6273
6390
|
env: process.env,
|
|
6274
6391
|
autoRotate: false,
|
|
6275
6392
|
});
|
|
6276
|
-
if (!authSession
|
|
6393
|
+
if (!isAuthenticated(authSession)) {
|
|
6277
6394
|
throw new Error(`Remote session read requires authentication. Run \`${authLoginHint()}\` first.`);
|
|
6278
6395
|
}
|
|
6279
6396
|
hydration = await hydrateSessionFromRemote({
|
|
@@ -6547,13 +6664,13 @@ export function registerSessionCommand(program) {
|
|
|
6547
6664
|
autoRotate: false,
|
|
6548
6665
|
}).catch(() => null);
|
|
6549
6666
|
const apiBaseUrl = session?.apiUrl || "";
|
|
6550
|
-
const
|
|
6667
|
+
const credential = session?.credential || null;
|
|
6551
6668
|
try {
|
|
6552
6669
|
for await (const item of mergeLiveSources({
|
|
6553
6670
|
sessionId: normalizedSessionId,
|
|
6554
6671
|
targetPath,
|
|
6555
6672
|
apiBaseUrl: apiBaseUrl || undefined,
|
|
6556
|
-
|
|
6673
|
+
credential: credential || undefined,
|
|
6557
6674
|
signal: ac.signal,
|
|
6558
6675
|
})) {
|
|
6559
6676
|
if (item.event) {
|
|
@@ -8476,21 +8593,19 @@ export function registerSessionCommand(program) {
|
|
|
8476
8593
|
return;
|
|
8477
8594
|
}
|
|
8478
8595
|
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
}
|
|
8596
|
+
const storedSession = await resolveActiveAuthSession({
|
|
8597
|
+
cwd: process.cwd(),
|
|
8598
|
+
env: process.env,
|
|
8599
|
+
autoRotate: false,
|
|
8600
|
+
}).catch(() => null);
|
|
8485
8601
|
|
|
8486
|
-
const fetchCredentials =
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
: null;
|
|
8602
|
+
const fetchCredentials = isAuthenticated(storedSession)
|
|
8603
|
+
? () =>
|
|
8604
|
+
fetchAidenIdCredentials({
|
|
8605
|
+
apiUrl: storedSession.apiUrl,
|
|
8606
|
+
auth: storedSession,
|
|
8607
|
+
})
|
|
8608
|
+
: null;
|
|
8494
8609
|
const credentials = await resolveAidenIdCredentials({
|
|
8495
8610
|
apiKey: options.apiKey,
|
|
8496
8611
|
orgId: options.orgId,
|
package/src/commands/spec.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
resolveProvider,
|
|
11
11
|
} from "../ai/client.js";
|
|
12
12
|
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
13
|
-
import { loadConfig } from "../config/service.js";
|
|
13
|
+
import { loadConfig, userConfigValues } from "../config/service.js";
|
|
14
14
|
import { evaluateBudget } from "../cost/budget.js";
|
|
15
15
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
16
16
|
import { estimateModelCost } from "../cost/tracker.js";
|
|
@@ -265,7 +265,7 @@ async function maybeEnhanceSpecWithAi({
|
|
|
265
265
|
configModel: config.resolved.defaultModelId,
|
|
266
266
|
});
|
|
267
267
|
const explicitApiKey = String(options.apiKey || "").trim();
|
|
268
|
-
const configuredApiKey = resolveConfiguredApiKey(resolvedProvider, config
|
|
268
|
+
const configuredApiKey = resolveConfiguredApiKey(resolvedProvider, userConfigValues(config));
|
|
269
269
|
|
|
270
270
|
const prompt = buildAiSpecPrompt({
|
|
271
271
|
baseSpecMarkdown,
|
package/src/commands/watch.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../auth/service.js";
|
|
14
14
|
import { resolveOutputRoot } from "../config/service.js";
|
|
15
15
|
import { authLoginHint } from "../ui/command-hints.js";
|
|
16
|
+
import { isAuthenticated } from "../auth/credential-destinations.js";
|
|
16
17
|
|
|
17
18
|
const TERMINAL_RUN_STATUSES = new Set(["completed", "failed", "cancelled"]);
|
|
18
19
|
|
|
@@ -369,7 +370,7 @@ export function registerWatchCommand(program) {
|
|
|
369
370
|
throw new Error(formatApiError(error));
|
|
370
371
|
}
|
|
371
372
|
|
|
372
|
-
if (!session
|
|
373
|
+
if (!isAuthenticated(session)) {
|
|
373
374
|
throw new Error(`No active auth token found. Run \`${authLoginHint()}\` first.`);
|
|
374
375
|
}
|
|
375
376
|
|
|
@@ -395,7 +396,7 @@ export function registerWatchCommand(program) {
|
|
|
395
396
|
while (true) {
|
|
396
397
|
const response = await listRuntimeRunEvents({
|
|
397
398
|
apiUrl: session.apiUrl,
|
|
398
|
-
|
|
399
|
+
credential: session.credential,
|
|
399
400
|
runId,
|
|
400
401
|
afterEventId,
|
|
401
402
|
});
|
|
@@ -428,7 +429,7 @@ export function registerWatchCommand(program) {
|
|
|
428
429
|
|
|
429
430
|
const statusResponse = await getRuntimeRunStatus({
|
|
430
431
|
apiUrl: session.apiUrl,
|
|
431
|
-
|
|
432
|
+
credential: session.credential,
|
|
432
433
|
runId,
|
|
433
434
|
});
|
|
434
435
|
latestStatus = String(statusResponse?.status || "unknown").trim().toLowerCase();
|