@jmoyers/harness 0.1.9 → 0.1.11
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 +36 -155
- package/package.json +3 -1
- package/packages/harness-ai/src/anthropic-client.ts +99 -0
- package/packages/harness-ai/src/anthropic-protocol.ts +581 -0
- package/packages/harness-ai/src/anthropic-provider.ts +82 -0
- package/packages/harness-ai/src/async-iterable-stream.ts +65 -0
- package/packages/harness-ai/src/index.ts +36 -0
- package/packages/harness-ai/src/json-parse.ts +66 -0
- package/packages/harness-ai/src/sse.ts +80 -0
- package/packages/harness-ai/src/stream-object.ts +96 -0
- package/packages/harness-ai/src/stream-text.ts +1340 -0
- package/packages/harness-ai/src/types.ts +330 -0
- package/packages/harness-ai/src/ui-stream.ts +217 -0
- package/scripts/codex-live-mux-runtime.ts +265 -14
- package/scripts/control-plane-daemon.ts +33 -5
- package/scripts/harness.ts +579 -134
- package/src/cli/default-gateway-pointer.ts +193 -0
- package/src/cli/gateway-record.ts +16 -1
- package/src/config/config-core.ts +13 -2
- package/src/config/harness-paths.ts +4 -7
- package/src/config/harness-runtime-migration.ts +142 -19
- package/src/config/secrets-core.ts +92 -4
- package/src/control-plane/prompt/thread-title-namer.ts +316 -0
- package/src/control-plane/stream-command-parser.ts +12 -0
- package/src/control-plane/stream-protocol.ts +6 -0
- package/src/control-plane/stream-server-background.ts +18 -2
- package/src/control-plane/stream-server-command.ts +14 -0
- package/src/control-plane/stream-server.ts +460 -28
- package/src/domain/conversations.ts +11 -7
- package/src/domain/workspace.ts +9 -0
- package/src/mux/input-shortcuts.ts +38 -1
- package/src/mux/live-mux/git-parsing.ts +40 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
- package/src/mux/live-mux/left-rail-conversation-click.ts +6 -3
- package/src/mux/live-mux/modal-input-reducers.ts +34 -1
- package/src/mux/live-mux/modal-overlays.ts +45 -0
- package/src/mux/live-mux/modal-prompt-handlers.ts +85 -0
- package/src/mux/render-frame.ts +1 -1
- package/src/mux/task-screen-keybindings.ts +29 -1
- package/src/services/control-plane.ts +22 -0
- package/src/services/runtime-control-actions.ts +69 -0
- package/src/services/runtime-conversation-activation.ts +25 -0
- package/src/services/runtime-conversation-starter.ts +31 -7
- package/src/services/runtime-input-router.ts +6 -0
- package/src/services/runtime-modal-input.ts +18 -0
- package/src/services/runtime-navigation-input.ts +4 -0
- package/src/services/runtime-rail-input.ts +5 -0
- package/src/services/runtime-repository-actions.ts +2 -0
- package/src/services/runtime-workspace-actions.ts +5 -0
- package/src/store/control-plane-store.ts +36 -0
- package/src/store/event-store.ts +36 -0
- package/src/ui/global-shortcut-input.ts +2 -0
- package/src/ui/input.ts +31 -0
- package/src/ui/modals/manager.ts +26 -0
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
} from '../src/config/config-core.ts';
|
|
26
26
|
import { resolveHarnessRuntimePath } from '../src/config/harness-paths.ts';
|
|
27
27
|
import { migrateLegacyHarnessLayout } from '../src/config/harness-runtime-migration.ts';
|
|
28
|
-
import { loadHarnessSecrets } from '../src/config/secrets-core.ts';
|
|
28
|
+
import { loadHarnessSecrets, upsertHarnessSecret } from '../src/config/secrets-core.ts';
|
|
29
29
|
import { detectMuxGlobalShortcut, resolveMuxShortcutBindings } from '../src/mux/input-shortcuts.ts';
|
|
30
30
|
import { createMuxInputModeManager } from '../src/mux/terminal-input-modes.ts';
|
|
31
31
|
import type { buildWorkspaceRailViewRows } from '../src/mux/workspace-rail-model.ts';
|
|
@@ -92,7 +92,9 @@ import {
|
|
|
92
92
|
} from '../src/mux/live-mux/layout.ts';
|
|
93
93
|
import {
|
|
94
94
|
normalizeGitHubRemoteUrl,
|
|
95
|
+
resolveGitHubDefaultBranchForActions,
|
|
95
96
|
repositoryNameFromGitHubRemoteUrl,
|
|
97
|
+
resolveGitHubTrackedBranchForActions,
|
|
96
98
|
shouldShowGitHubPrActions,
|
|
97
99
|
} from '../src/mux/live-mux/git-parsing.ts';
|
|
98
100
|
import { readProcessUsageSample, runGitCommand } from '../src/mux/live-mux/git-snapshot.ts';
|
|
@@ -243,6 +245,7 @@ interface RuntimeCommandMenuContext {
|
|
|
243
245
|
readonly leftNavSelectionKind: WorkspaceModel['leftNavSelection']['kind'];
|
|
244
246
|
readonly profileRunning: boolean;
|
|
245
247
|
readonly statusTimelineRunning: boolean;
|
|
248
|
+
readonly githubRepositoryId: string | null;
|
|
246
249
|
readonly githubRepositoryUrl: string | null;
|
|
247
250
|
readonly githubDefaultBranch: string | null;
|
|
248
251
|
readonly githubTrackedBranch: string | null;
|
|
@@ -257,12 +260,26 @@ interface CommandMenuGitHubProjectPrState {
|
|
|
257
260
|
readonly loading: boolean;
|
|
258
261
|
}
|
|
259
262
|
|
|
263
|
+
interface GitHubDebugAuthState {
|
|
264
|
+
enabled: boolean;
|
|
265
|
+
token: 'env' | 'gh' | 'none';
|
|
266
|
+
auth: 'ok' | 'no' | 'er' | 'na' | 'uk';
|
|
267
|
+
projectPr: 'ok' | 'er' | 'na';
|
|
268
|
+
}
|
|
269
|
+
|
|
260
270
|
interface ThemePickerSessionState {
|
|
261
271
|
readonly initialThemeConfig: HarnessMuxThemeConfig | null;
|
|
262
272
|
committed: boolean;
|
|
263
273
|
previewActionId: string | null;
|
|
264
274
|
}
|
|
265
275
|
|
|
276
|
+
interface AllowedCommandMenuApiKey {
|
|
277
|
+
readonly actionIdSuffix: string;
|
|
278
|
+
readonly envVar: 'ANTHROPIC_API_KEY' | 'OPENAI_API_KEY';
|
|
279
|
+
readonly displayName: string;
|
|
280
|
+
readonly aliases: readonly string[];
|
|
281
|
+
}
|
|
282
|
+
|
|
266
283
|
const DEFAULT_RESIZE_MIN_INTERVAL_MS = 33;
|
|
267
284
|
const DEFAULT_PTY_RESIZE_SETTLE_MS = 75;
|
|
268
285
|
const DEFAULT_STARTUP_SETTLE_QUIET_MS = 300;
|
|
@@ -282,6 +299,21 @@ const REPOSITORY_COLLAPSE_ALL_CHORD_PREFIX = Buffer.from([0x0b]);
|
|
|
282
299
|
const UNTRACKED_REPOSITORY_GROUP_ID = 'untracked';
|
|
283
300
|
const THEME_PICKER_SCOPE = 'theme-select';
|
|
284
301
|
const THEME_ACTION_ID_PREFIX = 'theme.set.';
|
|
302
|
+
const API_KEY_ACTION_ID_PREFIX = 'api-key.set.';
|
|
303
|
+
const COMMAND_MENU_ALLOWED_API_KEYS: readonly AllowedCommandMenuApiKey[] = [
|
|
304
|
+
{
|
|
305
|
+
actionIdSuffix: 'anthropic',
|
|
306
|
+
envVar: 'ANTHROPIC_API_KEY',
|
|
307
|
+
displayName: 'Anthropic API Key',
|
|
308
|
+
aliases: ['anthropic api key', 'claude api key'],
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
actionIdSuffix: 'openai',
|
|
312
|
+
envVar: 'OPENAI_API_KEY',
|
|
313
|
+
displayName: 'OpenAI API Key',
|
|
314
|
+
aliases: ['openai api key', 'codex api key'],
|
|
315
|
+
},
|
|
316
|
+
];
|
|
285
317
|
const GIT_SUMMARY_LOADING: GitSummary = {
|
|
286
318
|
branch: '(loading)',
|
|
287
319
|
changedFiles: 0,
|
|
@@ -331,6 +363,15 @@ function parseGitHubPrUrl(result: Record<string, unknown>): string | null {
|
|
|
331
363
|
return typeof url === 'string' ? url : null;
|
|
332
364
|
}
|
|
333
365
|
|
|
366
|
+
function parseGitHubUrl(result: Record<string, unknown>): string | null {
|
|
367
|
+
const url = result['url'];
|
|
368
|
+
if (typeof url !== 'string') {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
const trimmed = url.trim();
|
|
372
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
373
|
+
}
|
|
374
|
+
|
|
334
375
|
function commandMenuProjectPathTail(path: string): string {
|
|
335
376
|
const normalized = path.trim().replaceAll('\\', '/').replace(/\/+$/u, '');
|
|
336
377
|
if (normalized.length === 0) {
|
|
@@ -404,6 +445,28 @@ function commandExistsOnPath(command: string): boolean {
|
|
|
404
445
|
}
|
|
405
446
|
}
|
|
406
447
|
|
|
448
|
+
function readGhAuthTokenForDebug(): string | null {
|
|
449
|
+
try {
|
|
450
|
+
const stdout = execFileSync('gh', ['auth', 'token'], {
|
|
451
|
+
encoding: 'utf8',
|
|
452
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
453
|
+
timeout: 2000,
|
|
454
|
+
windowsHide: true,
|
|
455
|
+
});
|
|
456
|
+
const token = stdout.trim();
|
|
457
|
+
return token.length > 0 ? token : null;
|
|
458
|
+
} catch {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function formatGitHubDebugTokens(state: GitHubDebugAuthState): string {
|
|
464
|
+
if (!state.enabled) {
|
|
465
|
+
return '[gh:off tk:na au:na pr:na]';
|
|
466
|
+
}
|
|
467
|
+
return `[gh:on tk:${state.token} au:${state.auth} pr:${state.projectPr}]`;
|
|
468
|
+
}
|
|
469
|
+
|
|
407
470
|
async function main(): Promise<number> {
|
|
408
471
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
409
472
|
process.stderr.write('codex:live:mux requires a TTY stdin/stdout\n');
|
|
@@ -502,6 +565,26 @@ async function main(): Promise<number> {
|
|
|
502
565
|
const resolvedMuxTheme = resolveAndApplyRuntimeTheme(runtimeThemeConfig, true);
|
|
503
566
|
let currentModalTheme = resolvedMuxTheme.theme.modalTheme;
|
|
504
567
|
const configuredMuxGit = loadedConfig.config.mux.git;
|
|
568
|
+
const githubTokenEnvVar = loadedConfig.config.github.tokenEnvVar;
|
|
569
|
+
const envGitHubTokenRaw = process.env[githubTokenEnvVar];
|
|
570
|
+
const hasEnvGitHubToken =
|
|
571
|
+
typeof envGitHubTokenRaw === 'string' && envGitHubTokenRaw.trim().length > 0;
|
|
572
|
+
const githubDebugAuthState: GitHubDebugAuthState = {
|
|
573
|
+
enabled: loadedConfig.config.github.enabled,
|
|
574
|
+
token: hasEnvGitHubToken ? 'env' : 'none',
|
|
575
|
+
auth: loadedConfig.config.github.enabled ? (hasEnvGitHubToken ? 'ok' : 'uk') : 'na',
|
|
576
|
+
projectPr: 'na',
|
|
577
|
+
};
|
|
578
|
+
if (githubDebugAuthState.enabled && !hasEnvGitHubToken) {
|
|
579
|
+
const ghToken = commandExistsOnPath('gh') ? readGhAuthTokenForDebug() : null;
|
|
580
|
+
if (ghToken !== null) {
|
|
581
|
+
githubDebugAuthState.token = 'gh';
|
|
582
|
+
githubDebugAuthState.auth = 'ok';
|
|
583
|
+
} else {
|
|
584
|
+
githubDebugAuthState.token = 'none';
|
|
585
|
+
githubDebugAuthState.auth = 'no';
|
|
586
|
+
}
|
|
587
|
+
}
|
|
505
588
|
const configuredCodexLaunch = loadedConfig.config.codex.launch;
|
|
506
589
|
const configuredCritique = loadedConfig.config.critique;
|
|
507
590
|
const codexLaunchModeByDirectoryPath: Record<string, 'yolo' | 'standard'> = {};
|
|
@@ -635,7 +718,7 @@ async function main(): Promise<number> {
|
|
|
635
718
|
await startControlPlaneStreamServer({
|
|
636
719
|
stateStorePath: resolveHarnessRuntimePath(
|
|
637
720
|
options.invocationDirectory,
|
|
638
|
-
|
|
721
|
+
'.harness/control-plane.sqlite',
|
|
639
722
|
),
|
|
640
723
|
codexTelemetry: loadedConfig.config.codex.telemetry,
|
|
641
724
|
codexHistory: loadedConfig.config.codex.history,
|
|
@@ -743,7 +826,7 @@ async function main(): Promise<number> {
|
|
|
743
826
|
workspace.taskPaneRepositoryEditClickState = null;
|
|
744
827
|
workspace.homePaneDragState = null;
|
|
745
828
|
|
|
746
|
-
const sessionEnv = {
|
|
829
|
+
const sessionEnv: Record<string, string> = {
|
|
747
830
|
...sanitizeProcessEnv(),
|
|
748
831
|
TERM: process.env.TERM ?? 'xterm-256color',
|
|
749
832
|
};
|
|
@@ -1408,12 +1491,47 @@ async function main(): Promise<number> {
|
|
|
1408
1491
|
activeDirectoryId === null
|
|
1409
1492
|
? null
|
|
1410
1493
|
: (directoryRepositorySnapshotByDirectoryId.get(activeDirectoryId) ?? null);
|
|
1494
|
+
let githubRepositoryId =
|
|
1495
|
+
activeDirectoryId === null
|
|
1496
|
+
? null
|
|
1497
|
+
: (repositoryAssociationByDirectoryId.get(activeDirectoryId) ?? null);
|
|
1498
|
+
if (githubRepositoryId !== null) {
|
|
1499
|
+
const associatedRepository = repositories.get(githubRepositoryId);
|
|
1500
|
+
if (associatedRepository === undefined || associatedRepository.archivedAt !== null) {
|
|
1501
|
+
githubRepositoryId = null;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
const snapshotRemoteUrl = activeDirectoryRepositorySnapshot?.normalizedRemoteUrl ?? null;
|
|
1505
|
+
if (githubRepositoryId === null && snapshotRemoteUrl !== null) {
|
|
1506
|
+
const snapshotRemote = normalizeGitHubRemoteUrl(snapshotRemoteUrl);
|
|
1507
|
+
if (snapshotRemote !== null) {
|
|
1508
|
+
for (const repository of repositories.values()) {
|
|
1509
|
+
if (repository.archivedAt !== null) {
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
1512
|
+
if (normalizeGitHubRemoteUrl(repository.remoteUrl) === snapshotRemote) {
|
|
1513
|
+
githubRepositoryId = repository.repositoryId;
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
const githubRepositoryRecord =
|
|
1520
|
+
githubRepositoryId === null ? null : (repositories.get(githubRepositoryId) ?? null);
|
|
1411
1521
|
const githubProjectPrState =
|
|
1412
1522
|
activeDirectoryId !== null &&
|
|
1413
1523
|
commandMenuGitHubProjectPrState !== null &&
|
|
1414
1524
|
commandMenuGitHubProjectPrState.directoryId === activeDirectoryId
|
|
1415
1525
|
? commandMenuGitHubProjectPrState
|
|
1416
1526
|
: null;
|
|
1527
|
+
const currentBranchForActions =
|
|
1528
|
+
activeDirectoryId === null
|
|
1529
|
+
? null
|
|
1530
|
+
: (gitSummaryByDirectoryId.get(activeDirectoryId)?.branch ?? null);
|
|
1531
|
+
const trackedBranchForActions = resolveGitHubTrackedBranchForActions({
|
|
1532
|
+
projectTrackedBranch: githubProjectPrState?.branchName ?? null,
|
|
1533
|
+
currentBranch: currentBranchForActions,
|
|
1534
|
+
});
|
|
1417
1535
|
const selectedText =
|
|
1418
1536
|
workspace.selection === null
|
|
1419
1537
|
? ''
|
|
@@ -1433,9 +1551,13 @@ async function main(): Promise<number> {
|
|
|
1433
1551
|
statusTimelineRunning: existsSync(
|
|
1434
1552
|
resolveStatusTimelineStatePath(options.invocationDirectory, muxSessionName),
|
|
1435
1553
|
),
|
|
1554
|
+
githubRepositoryId,
|
|
1436
1555
|
githubRepositoryUrl: activeDirectoryRepositorySnapshot?.normalizedRemoteUrl ?? null,
|
|
1437
|
-
githubDefaultBranch:
|
|
1438
|
-
|
|
1556
|
+
githubDefaultBranch: resolveGitHubDefaultBranchForActions({
|
|
1557
|
+
repositoryDefaultBranch: githubRepositoryRecord?.defaultBranch ?? null,
|
|
1558
|
+
snapshotDefaultBranch: activeDirectoryRepositorySnapshot?.defaultBranch ?? null,
|
|
1559
|
+
}),
|
|
1560
|
+
githubTrackedBranch: trackedBranchForActions,
|
|
1439
1561
|
githubOpenPrUrl: githubProjectPrState?.openPrUrl ?? null,
|
|
1440
1562
|
githubProjectPrLoading: githubProjectPrState?.loading ?? false,
|
|
1441
1563
|
};
|
|
@@ -1502,6 +1624,7 @@ async function main(): Promise<number> {
|
|
|
1502
1624
|
resolveCommandMenuActions,
|
|
1503
1625
|
getNewThreadPrompt: () => workspace.newThreadPrompt,
|
|
1504
1626
|
getAddDirectoryPrompt: () => workspace.addDirectoryPrompt,
|
|
1627
|
+
getApiKeyPrompt: () => workspace.apiKeyPrompt,
|
|
1505
1628
|
getTaskEditorPrompt: () => workspace.taskEditorPrompt,
|
|
1506
1629
|
getRepositoryPrompt: () => workspace.repositoryPrompt,
|
|
1507
1630
|
getConversationTitleEdit: () => workspace.conversationTitleEdit,
|
|
@@ -1707,6 +1830,13 @@ async function main(): Promise<number> {
|
|
|
1707
1830
|
};
|
|
1708
1831
|
const githubAuthHintNotice =
|
|
1709
1832
|
'GitHub PR actions become available after auth (`gh auth login` or `GITHUB_TOKEN`).';
|
|
1833
|
+
const setGitHubDebugAuthState = (
|
|
1834
|
+
update: Partial<Pick<GitHubDebugAuthState, 'token' | 'auth' | 'projectPr'>>,
|
|
1835
|
+
): void => {
|
|
1836
|
+
githubDebugAuthState.token = update.token ?? githubDebugAuthState.token;
|
|
1837
|
+
githubDebugAuthState.auth = update.auth ?? githubDebugAuthState.auth;
|
|
1838
|
+
githubDebugAuthState.projectPr = update.projectPr ?? githubDebugAuthState.projectPr;
|
|
1839
|
+
};
|
|
1710
1840
|
const isGitHubAuthUnavailableError = (error: unknown): boolean => {
|
|
1711
1841
|
const message = formatErrorMessage(error).toLowerCase();
|
|
1712
1842
|
return (
|
|
@@ -1729,7 +1859,14 @@ async function main(): Promise<number> {
|
|
|
1729
1859
|
directoryId,
|
|
1730
1860
|
});
|
|
1731
1861
|
commandMenuGitHubProjectPrState = parseGitHubProjectPrState(directoryId, result);
|
|
1732
|
-
|
|
1862
|
+
setGitHubDebugAuthState({
|
|
1863
|
+
projectPr: 'ok',
|
|
1864
|
+
});
|
|
1865
|
+
} catch (error: unknown) {
|
|
1866
|
+
setGitHubDebugAuthState({
|
|
1867
|
+
projectPr: 'er',
|
|
1868
|
+
auth: isGitHubAuthUnavailableError(error) ? 'no' : githubDebugAuthState.auth,
|
|
1869
|
+
});
|
|
1733
1870
|
commandMenuGitHubProjectPrState = {
|
|
1734
1871
|
directoryId,
|
|
1735
1872
|
branchName: null,
|
|
@@ -2306,6 +2443,7 @@ async function main(): Promise<number> {
|
|
|
2306
2443
|
}
|
|
2307
2444
|
workspace.newThreadPrompt = null;
|
|
2308
2445
|
workspace.addDirectoryPrompt = null;
|
|
2446
|
+
workspace.apiKeyPrompt = null;
|
|
2309
2447
|
workspace.taskEditorPrompt = null;
|
|
2310
2448
|
workspace.repositoryPrompt = null;
|
|
2311
2449
|
if (workspace.conversationTitleEdit !== null) {
|
|
@@ -2517,6 +2655,12 @@ async function main(): Promise<number> {
|
|
|
2517
2655
|
},
|
|
2518
2656
|
});
|
|
2519
2657
|
},
|
|
2658
|
+
listConversationIdsForTitleRefresh: () => conversationManager.orderedIds(),
|
|
2659
|
+
conversationAgentTypeForTitleRefresh: (sessionId) =>
|
|
2660
|
+
conversationManager.get(sessionId)?.agentType ?? null,
|
|
2661
|
+
refreshConversationTitle: async (sessionId) => {
|
|
2662
|
+
return await controlPlaneService.refreshConversationTitle(sessionId);
|
|
2663
|
+
},
|
|
2520
2664
|
});
|
|
2521
2665
|
const runtimeWorkspaceActions = new RuntimeWorkspaceActions({
|
|
2522
2666
|
conversationActions: conversationLifecycle,
|
|
@@ -2539,6 +2683,7 @@ async function main(): Promise<number> {
|
|
|
2539
2683
|
}
|
|
2540
2684
|
workspace.newThreadPrompt = null;
|
|
2541
2685
|
workspace.addDirectoryPrompt = null;
|
|
2686
|
+
workspace.apiKeyPrompt = null;
|
|
2542
2687
|
workspace.taskEditorPrompt = null;
|
|
2543
2688
|
workspace.repositoryPrompt = null;
|
|
2544
2689
|
if (workspace.conversationTitleEdit !== null) {
|
|
@@ -2558,6 +2703,39 @@ async function main(): Promise<number> {
|
|
|
2558
2703
|
markDirty();
|
|
2559
2704
|
};
|
|
2560
2705
|
|
|
2706
|
+
const openApiKeyPrompt = (apiKey: AllowedCommandMenuApiKey): void => {
|
|
2707
|
+
workspace.newThreadPrompt = null;
|
|
2708
|
+
workspace.addDirectoryPrompt = null;
|
|
2709
|
+
workspace.taskEditorPrompt = null;
|
|
2710
|
+
workspace.repositoryPrompt = null;
|
|
2711
|
+
if (workspace.conversationTitleEdit !== null) {
|
|
2712
|
+
stopConversationTitleEdit(true);
|
|
2713
|
+
}
|
|
2714
|
+
workspace.conversationTitleEditClickState = null;
|
|
2715
|
+
const existingRaw = sessionEnv[apiKey.envVar] ?? process.env[apiKey.envVar];
|
|
2716
|
+
const hasExistingValue = typeof existingRaw === 'string' && existingRaw.trim().length > 0;
|
|
2717
|
+
workspace.apiKeyPrompt = {
|
|
2718
|
+
keyName: apiKey.envVar,
|
|
2719
|
+
displayName: apiKey.displayName,
|
|
2720
|
+
value: '',
|
|
2721
|
+
error: null,
|
|
2722
|
+
hasExistingValue,
|
|
2723
|
+
};
|
|
2724
|
+
markDirty();
|
|
2725
|
+
};
|
|
2726
|
+
|
|
2727
|
+
const persistApiKey = (keyName: string, value: string): void => {
|
|
2728
|
+
const result = upsertHarnessSecret({
|
|
2729
|
+
cwd: options.invocationDirectory,
|
|
2730
|
+
key: keyName,
|
|
2731
|
+
value,
|
|
2732
|
+
});
|
|
2733
|
+
sessionEnv[keyName] = value;
|
|
2734
|
+
process.env[keyName] = value;
|
|
2735
|
+
const action = result.replacedExisting ? 'updated' : 'saved';
|
|
2736
|
+
setCommandNotice(`${keyName} ${action}`);
|
|
2737
|
+
};
|
|
2738
|
+
|
|
2561
2739
|
const startThreadFromCommandMenu = (
|
|
2562
2740
|
directoryId: string,
|
|
2563
2741
|
agentType: ReturnType<typeof normalizeThreadAgentType>,
|
|
@@ -2661,7 +2839,7 @@ async function main(): Promise<number> {
|
|
|
2661
2839
|
return [
|
|
2662
2840
|
{
|
|
2663
2841
|
id: 'critique.review.unstaged',
|
|
2664
|
-
title: 'Critique AI Review: Unstaged Changes',
|
|
2842
|
+
title: 'Critique AI Review: Unstaged Changes (git)',
|
|
2665
2843
|
aliases: ['critique unstaged review', 'review unstaged diff', 'ai review unstaged'],
|
|
2666
2844
|
keywords: ['critique', 'review', 'unstaged', 'diff', 'ai'],
|
|
2667
2845
|
detail: 'runs critique review',
|
|
@@ -2671,7 +2849,7 @@ async function main(): Promise<number> {
|
|
|
2671
2849
|
},
|
|
2672
2850
|
{
|
|
2673
2851
|
id: 'critique.review.staged',
|
|
2674
|
-
title: 'Critique AI Review: Staged Changes',
|
|
2852
|
+
title: 'Critique AI Review: Staged Changes (git)',
|
|
2675
2853
|
aliases: ['critique staged review', 'review staged diff', 'ai review staged'],
|
|
2676
2854
|
keywords: ['critique', 'review', 'staged', 'diff', 'ai'],
|
|
2677
2855
|
detail: 'runs critique review --staged',
|
|
@@ -2681,7 +2859,7 @@ async function main(): Promise<number> {
|
|
|
2681
2859
|
},
|
|
2682
2860
|
{
|
|
2683
2861
|
id: 'critique.review.base-branch',
|
|
2684
|
-
title: 'Critique AI Review: Current Branch vs Base',
|
|
2862
|
+
title: 'Critique AI Review: Current Branch vs Base (git)',
|
|
2685
2863
|
aliases: ['critique base review', 'review against base branch', 'ai review base'],
|
|
2686
2864
|
keywords: ['critique', 'review', 'base', 'branch', 'diff', 'ai'],
|
|
2687
2865
|
detail: 'runs critique review <base> HEAD',
|
|
@@ -2824,6 +3002,21 @@ async function main(): Promise<number> {
|
|
|
2824
3002
|
},
|
|
2825
3003
|
});
|
|
2826
3004
|
|
|
3005
|
+
commandMenuRegistry.registerProvider('api-key.set', () => {
|
|
3006
|
+
return COMMAND_MENU_ALLOWED_API_KEYS.map(
|
|
3007
|
+
(apiKey): RegisteredCommandMenuAction<RuntimeCommandMenuContext> => ({
|
|
3008
|
+
id: `${API_KEY_ACTION_ID_PREFIX}${apiKey.actionIdSuffix}`,
|
|
3009
|
+
title: `Set ${apiKey.displayName}`,
|
|
3010
|
+
aliases: [...apiKey.aliases],
|
|
3011
|
+
keywords: ['api', 'key', 'set', apiKey.envVar.toLowerCase()],
|
|
3012
|
+
detail: apiKey.envVar,
|
|
3013
|
+
run: () => {
|
|
3014
|
+
openApiKeyPrompt(apiKey);
|
|
3015
|
+
},
|
|
3016
|
+
}),
|
|
3017
|
+
);
|
|
3018
|
+
});
|
|
3019
|
+
|
|
2827
3020
|
commandMenuRegistry.registerProvider('theme.set', () => {
|
|
2828
3021
|
const selectedThemeName = getActiveMuxTheme().name;
|
|
2829
3022
|
return muxThemePresetNames().map(
|
|
@@ -2873,10 +3066,10 @@ async function main(): Promise<number> {
|
|
|
2873
3066
|
if (repositoryUrl === null) {
|
|
2874
3067
|
return [];
|
|
2875
3068
|
}
|
|
2876
|
-
|
|
3069
|
+
const actions: RegisteredCommandMenuAction<RuntimeCommandMenuContext>[] = [
|
|
2877
3070
|
{
|
|
2878
3071
|
id: 'github.repo.open',
|
|
2879
|
-
title: 'Open GitHub for This Repo',
|
|
3072
|
+
title: 'Open GitHub for This Repo (git)',
|
|
2880
3073
|
aliases: ['open github for this repo', 'open github repo', 'open repository on github'],
|
|
2881
3074
|
keywords: ['github', 'repository', 'repo', 'open'],
|
|
2882
3075
|
detail: repositoryUrl,
|
|
@@ -2890,6 +3083,41 @@ async function main(): Promise<number> {
|
|
|
2890
3083
|
},
|
|
2891
3084
|
},
|
|
2892
3085
|
];
|
|
3086
|
+
if (context.githubRepositoryId !== null) {
|
|
3087
|
+
const repositoryId = context.githubRepositoryId;
|
|
3088
|
+
actions.push({
|
|
3089
|
+
id: 'github.repo.my-prs.open',
|
|
3090
|
+
title: 'Show My Open Pull Requests (git)',
|
|
3091
|
+
aliases: ['show my open pull requests', 'my open pull requests', 'show my prs', 'my prs'],
|
|
3092
|
+
keywords: ['github', 'pr', 'pull-request', 'open', 'my'],
|
|
3093
|
+
detail: repositoryUrl,
|
|
3094
|
+
run: async () => {
|
|
3095
|
+
queueControlPlaneOp(async () => {
|
|
3096
|
+
const result = await streamClient.sendCommand({
|
|
3097
|
+
type: 'github.repo-my-prs-url',
|
|
3098
|
+
repositoryId,
|
|
3099
|
+
});
|
|
3100
|
+
const parsedResult = asRecord(result);
|
|
3101
|
+
if (parsedResult === null) {
|
|
3102
|
+
setCommandNotice('github my open pull requests url unavailable');
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
const myPrsUrl = parseGitHubUrl(parsedResult);
|
|
3106
|
+
if (myPrsUrl === null) {
|
|
3107
|
+
setCommandNotice('github my open pull requests url unavailable');
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
const opened = openUrlInBrowser(myPrsUrl);
|
|
3111
|
+
setCommandNotice(
|
|
3112
|
+
opened
|
|
3113
|
+
? 'opened my open pull requests in browser'
|
|
3114
|
+
: `open pull requests: ${myPrsUrl}`,
|
|
3115
|
+
);
|
|
3116
|
+
}, 'command-menu-open-my-open-prs');
|
|
3117
|
+
},
|
|
3118
|
+
});
|
|
3119
|
+
}
|
|
3120
|
+
return actions;
|
|
2893
3121
|
});
|
|
2894
3122
|
|
|
2895
3123
|
commandMenuRegistry.registerProvider('github.project-pr', (context) => {
|
|
@@ -2908,7 +3136,7 @@ async function main(): Promise<number> {
|
|
|
2908
3136
|
return [
|
|
2909
3137
|
{
|
|
2910
3138
|
id: 'github.pr.open',
|
|
2911
|
-
title: 'Open PR',
|
|
3139
|
+
title: 'Open PR (git)',
|
|
2912
3140
|
aliases: ['open pull request', 'open pr'],
|
|
2913
3141
|
keywords: ['github', 'pr', 'open', 'pull-request'],
|
|
2914
3142
|
detail: context.githubTrackedBranch ?? 'current project',
|
|
@@ -2922,9 +3150,17 @@ async function main(): Promise<number> {
|
|
|
2922
3150
|
});
|
|
2923
3151
|
} catch (error: unknown) {
|
|
2924
3152
|
if (isGitHubAuthUnavailableError(error)) {
|
|
3153
|
+
setGitHubDebugAuthState({
|
|
3154
|
+
auth: 'no',
|
|
3155
|
+
projectPr: 'er',
|
|
3156
|
+
});
|
|
2925
3157
|
setCommandNotice(githubAuthHintNotice);
|
|
2926
3158
|
return;
|
|
2927
3159
|
}
|
|
3160
|
+
setGitHubDebugAuthState({
|
|
3161
|
+
auth: 'er',
|
|
3162
|
+
projectPr: 'er',
|
|
3163
|
+
});
|
|
2928
3164
|
throw error;
|
|
2929
3165
|
}
|
|
2930
3166
|
const parsedResult = asRecord(result);
|
|
@@ -2933,6 +3169,9 @@ async function main(): Promise<number> {
|
|
|
2933
3169
|
return;
|
|
2934
3170
|
}
|
|
2935
3171
|
const state = parseGitHubProjectPrState(directoryId, parsedResult);
|
|
3172
|
+
setGitHubDebugAuthState({
|
|
3173
|
+
projectPr: 'ok',
|
|
3174
|
+
});
|
|
2936
3175
|
commandMenuGitHubProjectPrState = state;
|
|
2937
3176
|
if (state.openPrUrl === null) {
|
|
2938
3177
|
setCommandNotice('no open pull request for tracked branch');
|
|
@@ -2951,7 +3190,7 @@ async function main(): Promise<number> {
|
|
|
2951
3190
|
return [
|
|
2952
3191
|
{
|
|
2953
3192
|
id: 'github.pr.create',
|
|
2954
|
-
title: 'Create PR',
|
|
3193
|
+
title: 'Create PR (git)',
|
|
2955
3194
|
aliases: ['create pull request', 'new pr'],
|
|
2956
3195
|
keywords: ['github', 'pr', 'create', 'pull-request'],
|
|
2957
3196
|
detail: context.githubTrackedBranch,
|
|
@@ -2965,9 +3204,15 @@ async function main(): Promise<number> {
|
|
|
2965
3204
|
});
|
|
2966
3205
|
} catch (error: unknown) {
|
|
2967
3206
|
if (isGitHubAuthUnavailableError(error)) {
|
|
3207
|
+
setGitHubDebugAuthState({
|
|
3208
|
+
auth: 'no',
|
|
3209
|
+
});
|
|
2968
3210
|
setCommandNotice(githubAuthHintNotice);
|
|
2969
3211
|
return;
|
|
2970
3212
|
}
|
|
3213
|
+
setGitHubDebugAuthState({
|
|
3214
|
+
auth: 'er',
|
|
3215
|
+
});
|
|
2971
3216
|
throw error;
|
|
2972
3217
|
}
|
|
2973
3218
|
const parsedResult = asRecord(result);
|
|
@@ -2979,6 +3224,10 @@ async function main(): Promise<number> {
|
|
|
2979
3224
|
if (prUrl === null) {
|
|
2980
3225
|
throw new Error('github.pr-create returned malformed pr url');
|
|
2981
3226
|
}
|
|
3227
|
+
setGitHubDebugAuthState({
|
|
3228
|
+
auth: 'ok',
|
|
3229
|
+
projectPr: 'ok',
|
|
3230
|
+
});
|
|
2982
3231
|
refreshCommandMenuGitHubProjectPrState(directoryId);
|
|
2983
3232
|
const opened = openUrlInBrowser(prUrl);
|
|
2984
3233
|
setCommandNotice(
|
|
@@ -3087,7 +3336,8 @@ async function main(): Promise<number> {
|
|
|
3087
3336
|
>({
|
|
3088
3337
|
renderFlush: {
|
|
3089
3338
|
perfNowNs,
|
|
3090
|
-
statusFooterForConversation: (conversation) =>
|
|
3339
|
+
statusFooterForConversation: (conversation) =>
|
|
3340
|
+
`${formatGitHubDebugTokens(githubDebugAuthState)} ${debugFooterForConversation(conversation)}`,
|
|
3091
3341
|
currentStatusNotice: () => debugFooterNotice.current(),
|
|
3092
3342
|
currentStatusRow: () => outputLoadSampler.currentStatusRow(),
|
|
3093
3343
|
onStatusLineComposed: (input) => {
|
|
@@ -3394,6 +3644,7 @@ async function main(): Promise<number> {
|
|
|
3394
3644
|
scheduleConversationTitlePersist,
|
|
3395
3645
|
resolveCommandMenuActions,
|
|
3396
3646
|
executeCommandMenuAction,
|
|
3647
|
+
persistApiKey,
|
|
3397
3648
|
requestStop,
|
|
3398
3649
|
resolveDirectoryForAction,
|
|
3399
3650
|
openNewThreadPrompt,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { startCodexLiveSession } from '../src/codex/live-session.ts';
|
|
3
3
|
import { startControlPlaneStreamServer } from '../src/control-plane/stream-server.ts';
|
|
4
|
-
import { loadHarnessConfig } from '../src/config/config-core.ts';
|
|
5
|
-
import {
|
|
4
|
+
import { loadHarnessConfig, resolveHarnessConfigPath } from '../src/config/config-core.ts';
|
|
5
|
+
import {
|
|
6
|
+
resolveHarnessRuntimePath,
|
|
7
|
+
resolveHarnessWorkspaceDirectory,
|
|
8
|
+
} from '../src/config/harness-paths.ts';
|
|
6
9
|
import { migrateLegacyHarnessLayout } from '../src/config/harness-runtime-migration.ts';
|
|
7
10
|
import { loadHarnessSecrets } from '../src/config/secrets-core.ts';
|
|
8
11
|
import {
|
|
@@ -69,7 +72,7 @@ function parseArgs(argv: string[], invocationDirectory: string): DaemonOptions {
|
|
|
69
72
|
const defaultAuthToken = process.env.HARNESS_CONTROL_PLANE_AUTH_TOKEN ?? null;
|
|
70
73
|
const defaultStateDbPath = resolveHarnessRuntimePath(
|
|
71
74
|
invocationDirectory,
|
|
72
|
-
|
|
75
|
+
'.harness/control-plane.sqlite',
|
|
73
76
|
);
|
|
74
77
|
|
|
75
78
|
let host = defaultHost;
|
|
@@ -114,7 +117,7 @@ function parseArgs(argv: string[], invocationDirectory: string): DaemonOptions {
|
|
|
114
117
|
if (value === undefined) {
|
|
115
118
|
throw new Error('missing value for --state-db-path');
|
|
116
119
|
}
|
|
117
|
-
stateDbPath = value;
|
|
120
|
+
stateDbPath = resolveHarnessRuntimePath(invocationDirectory, value);
|
|
118
121
|
idx += 1;
|
|
119
122
|
continue;
|
|
120
123
|
}
|
|
@@ -129,6 +132,17 @@ function parseArgs(argv: string[], invocationDirectory: string): DaemonOptions {
|
|
|
129
132
|
if (!loopbackHosts.has(host) && authToken === null) {
|
|
130
133
|
throw new Error('non-loopback hosts require --auth-token or HARNESS_CONTROL_PLANE_AUTH_TOKEN');
|
|
131
134
|
}
|
|
135
|
+
const workspaceRuntimeRoot = resolveHarnessWorkspaceDirectory(invocationDirectory, process.env);
|
|
136
|
+
const normalizedRuntimeRoot = resolve(workspaceRuntimeRoot);
|
|
137
|
+
const normalizedStateDbPath = resolve(stateDbPath);
|
|
138
|
+
if (
|
|
139
|
+
normalizedStateDbPath !== normalizedRuntimeRoot &&
|
|
140
|
+
!normalizedStateDbPath.startsWith(`${normalizedRuntimeRoot}/`)
|
|
141
|
+
) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`invalid --state-db-path: ${stateDbPath}. state db path must be under workspace runtime root ${workspaceRuntimeRoot}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
132
146
|
|
|
133
147
|
return {
|
|
134
148
|
host,
|
|
@@ -140,6 +154,11 @@ function parseArgs(argv: string[], invocationDirectory: string): DaemonOptions {
|
|
|
140
154
|
|
|
141
155
|
async function main(): Promise<number> {
|
|
142
156
|
const invocationDirectory = resolveInvocationDirectory();
|
|
157
|
+
const gatewayRunIdRaw = process.env.HARNESS_GATEWAY_RUN_ID;
|
|
158
|
+
const gatewayRunId =
|
|
159
|
+
typeof gatewayRunIdRaw === 'string' && gatewayRunIdRaw.trim().length > 0
|
|
160
|
+
? gatewayRunIdRaw.trim()
|
|
161
|
+
: `gateway-run-${process.pid}`;
|
|
143
162
|
migrateLegacyHarnessLayout(invocationDirectory, process.env);
|
|
144
163
|
loadHarnessSecrets({ cwd: invocationDirectory });
|
|
145
164
|
configureProcessPerf(invocationDirectory);
|
|
@@ -159,14 +178,22 @@ async function main(): Promise<number> {
|
|
|
159
178
|
}
|
|
160
179
|
const startupSpan = startPerfSpan('daemon.startup.total', {
|
|
161
180
|
process: 'daemon',
|
|
181
|
+
gatewayRunId,
|
|
162
182
|
});
|
|
163
183
|
recordPerfEvent('daemon.startup.begin', {
|
|
164
184
|
process: 'daemon',
|
|
185
|
+
gatewayRunId,
|
|
165
186
|
});
|
|
166
187
|
const options = parseArgs(process.argv.slice(2), invocationDirectory);
|
|
188
|
+
const workspaceRuntimeRoot = resolveHarnessWorkspaceDirectory(invocationDirectory, process.env);
|
|
189
|
+
const configPath = resolveHarnessConfigPath(invocationDirectory, process.env);
|
|
190
|
+
process.stdout.write(
|
|
191
|
+
`[control-plane] boot runId=${gatewayRunId} pid=${String(process.pid)} ppid=${String(process.ppid)} workspaceRoot=${invocationDirectory} runtimeRoot=${workspaceRuntimeRoot} config=${configPath} db=${options.stateDbPath}\n`,
|
|
192
|
+
);
|
|
167
193
|
|
|
168
194
|
const listenSpan = startPerfSpan('daemon.startup.listen', {
|
|
169
195
|
process: 'daemon',
|
|
196
|
+
gatewayRunId,
|
|
170
197
|
});
|
|
171
198
|
const serverOptions: Parameters<typeof startControlPlaneStreamServer>[0] = {
|
|
172
199
|
host: options.host,
|
|
@@ -251,13 +278,14 @@ async function main(): Promise<number> {
|
|
|
251
278
|
const address = server.address();
|
|
252
279
|
recordPerfEvent('daemon.startup.listening', {
|
|
253
280
|
process: 'daemon',
|
|
281
|
+
gatewayRunId,
|
|
254
282
|
host: address.address,
|
|
255
283
|
port: address.port,
|
|
256
284
|
auth: options.authToken === null ? 'off' : 'on',
|
|
257
285
|
});
|
|
258
286
|
startupSpan.end({ listening: true });
|
|
259
287
|
process.stdout.write(
|
|
260
|
-
`[control-plane] listening host=${address.address} port=${String(address.port)} auth=${options.authToken === null ? 'off' : 'on'} db=${options.stateDbPath}\n`,
|
|
288
|
+
`[control-plane] listening host=${address.address} port=${String(address.port)} auth=${options.authToken === null ? 'off' : 'on'} db=${options.stateDbPath} runId=${gatewayRunId}\n`,
|
|
261
289
|
);
|
|
262
290
|
|
|
263
291
|
let stopRequested = false;
|