@gajae-code/coding-agent 0.4.1 → 0.4.3
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/CHANGELOG.md +22 -0
- package/dist/types/async/job-manager.d.ts +25 -0
- package/dist/types/commands/ultragoal.d.ts +1 -0
- package/dist/types/commit/model-selection.d.ts +1 -1
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/model-resolver.d.ts +1 -19
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +26 -4
- package/dist/types/gjc-runtime/goal-mode-request.d.ts +8 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/harness-control-plane/finalize.d.ts +8 -0
- package/dist/types/harness-control-plane/receipts.d.ts +16 -1
- package/dist/types/harness-control-plane/types.d.ts +16 -3
- package/dist/types/modes/acp/acp-event-mapper.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/command-contract.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/event-contract.d.ts +84 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +14 -7
- package/dist/types/modes/shared/agent-wire/event-observation.d.ts +37 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +13 -34
- package/dist/types/reminders/star-reminder.d.ts +115 -0
- package/dist/types/session/agent-session.d.ts +30 -1
- package/dist/types/session/session-manager.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/browser/actions.d.ts +54 -0
- package/dist/types/tools/browser.d.ts +80 -0
- package/dist/types/tools/image-gen.d.ts +1 -0
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/job.d.ts +1 -1
- package/examples/extensions/README.md +20 -41
- package/package.json +7 -7
- package/src/async/job-manager.ts +120 -1
- package/src/cli/grep-cli.ts +1 -1
- package/src/commands/harness.ts +42 -3
- package/src/commands/ultragoal.ts +8 -1
- package/src/commit/agentic/index.ts +2 -2
- package/src/commit/model-selection.ts +7 -22
- package/src/commit/pipeline.ts +2 -2
- package/src/config/model-registry.ts +17 -9
- package/src/config/model-resolver.ts +14 -84
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +27 -4
- package/src/defaults/gjc/skills/team/SKILL.md +10 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +3 -2
- package/src/gjc-runtime/goal-mode-request.ts +21 -1
- package/src/gjc-runtime/launch-tmux.ts +25 -2
- package/src/gjc-runtime/team-runtime.ts +78 -3
- package/src/gjc-runtime/ultragoal-guard.ts +18 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +240 -30
- package/src/harness-control-plane/finalize.ts +84 -0
- package/src/harness-control-plane/owner.ts +16 -3
- package/src/harness-control-plane/receipts.ts +39 -1
- package/src/harness-control-plane/rpc-adapter.ts +7 -1
- package/src/harness-control-plane/types.ts +33 -12
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-agent.ts +17 -9
- package/src/modes/acp/acp-event-mapper.ts +33 -1
- package/src/modes/components/custom-editor.ts +19 -3
- package/src/modes/controllers/input-controller.ts +27 -7
- package/src/modes/controllers/selector-controller.ts +7 -1
- package/src/modes/interactive-mode.ts +29 -1
- package/src/modes/rpc/rpc-client.ts +16 -3
- package/src/modes/rpc/rpc-mode.ts +5 -2
- package/src/modes/shared/agent-wire/command-contract.ts +18 -0
- package/src/modes/shared/agent-wire/event-contract.ts +147 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +35 -16
- package/src/modes/shared/agent-wire/event-observation.ts +397 -0
- package/src/modes/shared/agent-wire/protocol.ts +24 -81
- package/src/modes/utils/context-usage.ts +2 -2
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/plan.md +1 -1
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/tools/browser.md +3 -2
- package/src/reminders/star-reminder.ts +422 -0
- package/src/runtime-mcp/manager.ts +15 -2
- package/src/sdk.ts +3 -1
- package/src/session/agent-session.ts +139 -17
- package/src/session/session-manager.ts +1 -1
- package/src/task/agents.ts +1 -1
- package/src/tools/bash.ts +6 -1
- package/src/tools/browser/actions.ts +189 -0
- package/src/tools/browser.ts +91 -1
- package/src/tools/image-gen.ts +42 -15
- package/src/tools/index.ts +7 -1
- package/src/tools/inspect-image.ts +10 -8
- package/src/tools/job.ts +12 -2
- package/src/tools/monitor.ts +98 -17
- package/src/utils/commit-message-generator.ts +6 -13
- package/src/utils/title-generator.ts +1 -1
- package/dist/types/harness-control-plane/frame-mapper.d.ts +0 -29
- package/src/harness-control-plane/frame-mapper.ts +0 -286
- package/src/priority.json +0 -37
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub star reminder.
|
|
3
|
+
*
|
|
4
|
+
* On interactive launch, if `gh` is authenticated and the GJC repo is not
|
|
5
|
+
* starred, the user is nudged to star it. Declining switches to a per-session
|
|
6
|
+
* persuasion message injected at the before-agent-start point until the repo is
|
|
7
|
+
* starred. Detection is `gh`-only: if `gh` is missing, unauthenticated, offline,
|
|
8
|
+
* or fails for any non-404 reason, the feature stays completely silent.
|
|
9
|
+
*
|
|
10
|
+
* All state lives in a user-global file under the GJC config root and is updated
|
|
11
|
+
* under a file lock with atomic temp+rename writes and a monotonic merge that
|
|
12
|
+
* never lets a stale "declined"/"unstarred" write clobber a confirmed star.
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import * as fs from "node:fs/promises";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import type { ImageContent, MessageAttribution } from "@gajae-code/ai";
|
|
18
|
+
import { getConfigRootDir, isEnoent } from "@gajae-code/utils";
|
|
19
|
+
import { withFileLock } from "../config/file-lock";
|
|
20
|
+
import type { CustomMessage } from "../session/messages";
|
|
21
|
+
|
|
22
|
+
export const STAR_REMINDER_REPO = "Yeachan-Heo/gajae-code";
|
|
23
|
+
export const STAR_REMINDER_CUSTOM_TYPE = "star-reminder";
|
|
24
|
+
export const STARRED_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
|
|
26
|
+
const GH_TIMEOUT_MS = 5_000;
|
|
27
|
+
|
|
28
|
+
export interface StarReminderState {
|
|
29
|
+
declined: boolean;
|
|
30
|
+
starred: boolean;
|
|
31
|
+
/** ISO-8601 timestamp of the last authoritative star check, or "" if never. */
|
|
32
|
+
starredCheckedAt: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type StarCheckStatus = "starred" | "unstarred" | "unavailable";
|
|
36
|
+
|
|
37
|
+
export interface GhResult {
|
|
38
|
+
exitCode: number;
|
|
39
|
+
stdout: string;
|
|
40
|
+
stderr: string;
|
|
41
|
+
timedOut?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type RunGh = (args: string[], options?: { timeoutMs?: number }) => Promise<GhResult>;
|
|
45
|
+
|
|
46
|
+
export interface StarReminderDeps {
|
|
47
|
+
statePath?: string;
|
|
48
|
+
now?: () => Date;
|
|
49
|
+
runGh?: RunGh;
|
|
50
|
+
sleep?: (ms: number) => Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --------------------------------------------------------------------------
|
|
54
|
+
// State path + defaults
|
|
55
|
+
// --------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export function getStarReminderStatePath(): string {
|
|
58
|
+
return path.join(getConfigRootDir(), "star-reminder.json");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function defaultStarReminderState(): StarReminderState {
|
|
62
|
+
return { declined: false, starred: false, starredCheckedAt: "" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveStatePath(deps?: StarReminderDeps): string {
|
|
66
|
+
return deps?.statePath ?? getStarReminderStatePath();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveNow(deps?: StarReminderDeps): Date {
|
|
70
|
+
return deps?.now ? deps.now() : new Date();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isValidState(value: unknown): value is StarReminderState {
|
|
74
|
+
if (!value || typeof value !== "object") return false;
|
|
75
|
+
const v = value as Record<string, unknown>;
|
|
76
|
+
return typeof v.declined === "boolean" && typeof v.starred === "boolean" && typeof v.starredCheckedAt === "string";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// --------------------------------------------------------------------------
|
|
80
|
+
// State IO
|
|
81
|
+
// --------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
/** Read state without locking. Missing or malformed files return the default. */
|
|
84
|
+
export async function readStarReminderStateUnlocked(statePath?: string): Promise<StarReminderState> {
|
|
85
|
+
const target = statePath ?? getStarReminderStatePath();
|
|
86
|
+
try {
|
|
87
|
+
const raw = await fs.readFile(target, "utf8");
|
|
88
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
89
|
+
if (!isValidState(parsed)) return defaultStarReminderState();
|
|
90
|
+
return { declined: parsed.declined, starred: parsed.starred, starredCheckedAt: parsed.starredCheckedAt };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (isEnoent(err)) return defaultStarReminderState();
|
|
93
|
+
// Malformed JSON or any read error -> treat as default; never throw to UI.
|
|
94
|
+
return defaultStarReminderState();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Whether a stored `starred:true` is still within the 24h cache window. */
|
|
99
|
+
export function isStarredCacheFresh(state: StarReminderState, now: Date = new Date()): boolean {
|
|
100
|
+
if (!state.starred) return false;
|
|
101
|
+
const checked = new Date(state.starredCheckedAt).getTime();
|
|
102
|
+
if (Number.isNaN(checked)) return false;
|
|
103
|
+
const age = now.getTime() - checked;
|
|
104
|
+
return age >= 0 && age < STARRED_CACHE_TTL_MS;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function writeStateAtomic(statePath: string, state: StarReminderState): Promise<void> {
|
|
108
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true, mode: 0o700 });
|
|
109
|
+
const temp = `${statePath}.tmp.${process.pid}.${Date.now()}.${randomUUID()}`;
|
|
110
|
+
try {
|
|
111
|
+
await fs.writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
112
|
+
await fs.rename(temp, statePath);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
await fs.rm(temp, { force: true }).catch(() => {});
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Lock-protected read-modify-write. The mutator receives the freshly re-read
|
|
121
|
+
* state under the lock; callers MUST base monotonic decisions on that value,
|
|
122
|
+
* not on any snapshot captured before the lock was held.
|
|
123
|
+
*/
|
|
124
|
+
export async function updateStarReminderStateLocked(
|
|
125
|
+
mutator: (current: StarReminderState) => StarReminderState | Promise<StarReminderState>,
|
|
126
|
+
deps?: StarReminderDeps,
|
|
127
|
+
): Promise<StarReminderState> {
|
|
128
|
+
const statePath = resolveStatePath(deps);
|
|
129
|
+
// The lock file lives next to the state file, so its parent dir must exist
|
|
130
|
+
// before withFileLock tries to create the (non-recursive) lock directory.
|
|
131
|
+
await fs.mkdir(path.dirname(statePath), { recursive: true, mode: 0o700 });
|
|
132
|
+
return withFileLock(statePath, async () => {
|
|
133
|
+
const current = await readStarReminderStateUnlocked(statePath);
|
|
134
|
+
const next = await mutator(current);
|
|
135
|
+
await writeStateAtomic(statePath, next);
|
|
136
|
+
return next;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --------------------------------------------------------------------------
|
|
141
|
+
// Monotonic merge helpers
|
|
142
|
+
// --------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/** A successful PUT is authoritative: always record starred and clear declined. */
|
|
145
|
+
export async function recordStarredFromPut(deps?: StarReminderDeps): Promise<StarReminderState> {
|
|
146
|
+
const checkedAt = resolveNow(deps).toISOString();
|
|
147
|
+
return updateStarReminderStateLocked(() => ({ declined: false, starred: true, starredCheckedAt: checkedAt }), deps);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Record the result of a fresh `gh` star check performed by this operation.
|
|
152
|
+
* - "starred": authoritative, clears declined so all reminders stop.
|
|
153
|
+
* - "unstarred": may downgrade, but only when the current state is not a
|
|
154
|
+
* still-fresh confirmed star (which a concurrent process may have just
|
|
155
|
+
* written); in that case the fresher confirmation wins.
|
|
156
|
+
*/
|
|
157
|
+
export async function recordFreshStarCheck(
|
|
158
|
+
status: "starred" | "unstarred",
|
|
159
|
+
deps?: StarReminderDeps,
|
|
160
|
+
): Promise<StarReminderState> {
|
|
161
|
+
const now = resolveNow(deps);
|
|
162
|
+
const checkedAt = now.toISOString();
|
|
163
|
+
return updateStarReminderStateLocked(current => {
|
|
164
|
+
if (status === "starred") {
|
|
165
|
+
return { declined: false, starred: true, starredCheckedAt: checkedAt };
|
|
166
|
+
}
|
|
167
|
+
// Preserve a confirmed star that is either still within the cache window
|
|
168
|
+
// or was written concurrently AFTER this operation's check time. The latter
|
|
169
|
+
// guards against a stale unstarred observation clobbering a newer star that
|
|
170
|
+
// another process recorded while we were waiting on the lock.
|
|
171
|
+
if (current.starred) {
|
|
172
|
+
const currentChecked = new Date(current.starredCheckedAt).getTime();
|
|
173
|
+
const newerThanThisCheck = !Number.isNaN(currentChecked) && currentChecked > now.getTime();
|
|
174
|
+
if (newerThanThisCheck || isStarredCacheFresh(current, now)) {
|
|
175
|
+
return current;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { declined: current.declined, starred: false, starredCheckedAt: checkedAt };
|
|
179
|
+
}, deps);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Record a launch-nudge decline. Never downgrades a confirmed star. */
|
|
183
|
+
export async function recordDeclinedAfterNo(deps?: StarReminderDeps): Promise<StarReminderState> {
|
|
184
|
+
return updateStarReminderStateLocked(current => {
|
|
185
|
+
if (current.starred) return current;
|
|
186
|
+
return { ...current, declined: true };
|
|
187
|
+
}, deps);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// --------------------------------------------------------------------------
|
|
191
|
+
// gh helpers
|
|
192
|
+
// --------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
/** Default `gh` runner. Returns an unavailable-style result instead of throwing. */
|
|
195
|
+
export async function runGhDefault(args: string[], options?: { timeoutMs?: number }): Promise<GhResult> {
|
|
196
|
+
const ghPath = Bun.which("gh");
|
|
197
|
+
if (!ghPath) {
|
|
198
|
+
return { exitCode: -1, stdout: "", stderr: "gh not found", timedOut: false };
|
|
199
|
+
}
|
|
200
|
+
const timeoutMs = options?.timeoutMs ?? GH_TIMEOUT_MS;
|
|
201
|
+
let timedOut = false;
|
|
202
|
+
try {
|
|
203
|
+
const proc = Bun.spawn([ghPath, ...args], { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
|
|
204
|
+
const timer = setTimeout(() => {
|
|
205
|
+
timedOut = true;
|
|
206
|
+
proc.kill();
|
|
207
|
+
}, timeoutMs);
|
|
208
|
+
try {
|
|
209
|
+
const [stdout, stderr] = await Promise.all([
|
|
210
|
+
new Response(proc.stdout).text(),
|
|
211
|
+
new Response(proc.stderr).text(),
|
|
212
|
+
]);
|
|
213
|
+
const exitCode = await proc.exited;
|
|
214
|
+
return { exitCode, stdout, stderr, timedOut };
|
|
215
|
+
} finally {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
}
|
|
218
|
+
} catch (err) {
|
|
219
|
+
return { exitCode: -1, stdout: "", stderr: err instanceof Error ? err.message : String(err), timedOut };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function resolveRunGh(deps?: StarReminderDeps): RunGh {
|
|
224
|
+
return deps?.runGh ?? runGhDefault;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Classify the star state of the repo via `gh api`. */
|
|
228
|
+
export async function checkGhStarred(deps?: StarReminderDeps): Promise<StarCheckStatus> {
|
|
229
|
+
const runGh = resolveRunGh(deps);
|
|
230
|
+
const res = await runGh(["api", `user/starred/${STAR_REMINDER_REPO}`], { timeoutMs: GH_TIMEOUT_MS });
|
|
231
|
+
if (res.timedOut) return "unavailable";
|
|
232
|
+
if (res.exitCode === 0) return "starred";
|
|
233
|
+
// A genuine 404 from this endpoint is the unambiguous "not starred" signal.
|
|
234
|
+
// gh emits "gh: Not Found (HTTP 404)" for that case. Other failures (missing
|
|
235
|
+
// gh, auth, network, malformed output) must stay silent, so we key strictly
|
|
236
|
+
// on the explicit HTTP 404 status rather than any "404"/"not found" substring,
|
|
237
|
+
// which could appear in unrelated error text.
|
|
238
|
+
if (/http[\s/]?404\b/i.test(res.stderr)) return "unstarred";
|
|
239
|
+
return "unavailable";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Star the repo via `gh api -X PUT`. Returns whether the PUT succeeded. */
|
|
243
|
+
export async function autoStarRepo(deps?: StarReminderDeps): Promise<boolean> {
|
|
244
|
+
const runGh = resolveRunGh(deps);
|
|
245
|
+
const res = await runGh(["api", "-X", "PUT", `user/starred/${STAR_REMINDER_REPO}`], { timeoutMs: GH_TIMEOUT_MS });
|
|
246
|
+
return res.exitCode === 0 && !res.timedOut;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Determine the star state for this session, hitting `gh` only when needed.
|
|
251
|
+
* A fresh cached star skips `gh`; unstarred and declined states are rechecked.
|
|
252
|
+
* Authoritative results are persisted via the monotonic helpers.
|
|
253
|
+
*/
|
|
254
|
+
export async function refreshStarStateForSession(deps?: StarReminderDeps): Promise<StarCheckStatus> {
|
|
255
|
+
const statePath = resolveStatePath(deps);
|
|
256
|
+
const state = await readStarReminderStateUnlocked(statePath);
|
|
257
|
+
if (state.starred && isStarredCacheFresh(state, resolveNow(deps))) {
|
|
258
|
+
return "starred";
|
|
259
|
+
}
|
|
260
|
+
const status = await checkGhStarred(deps);
|
|
261
|
+
if (status === "starred") {
|
|
262
|
+
await recordFreshStarCheck("starred", deps);
|
|
263
|
+
} else if (status === "unstarred") {
|
|
264
|
+
await recordFreshStarCheck("unstarred", deps);
|
|
265
|
+
}
|
|
266
|
+
return status;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// --------------------------------------------------------------------------
|
|
270
|
+
// Launch nudge
|
|
271
|
+
// --------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
export interface StarReminderPromptUI {
|
|
274
|
+
/** Show a yes/no confirmation. Resolves true when the user accepts. */
|
|
275
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
276
|
+
/** Optional guard; when it returns false the nudge is skipped silently. */
|
|
277
|
+
isIdle?: () => boolean;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const LAUNCH_PROMPT_TITLE = "Enjoying GJC?";
|
|
281
|
+
const LAUNCH_PROMPT_MESSAGE = `Star ${STAR_REMINDER_REPO} on GitHub to support the project?`;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Run the launch nudge once. Caller is responsible for the `startup.quiet`,
|
|
285
|
+
* `starReminder.enabled`, and true-interactive gates. All errors are swallowed
|
|
286
|
+
* so the launch path can never be broken by the reminder.
|
|
287
|
+
*/
|
|
288
|
+
export async function maybeShowLaunchStarReminder(ui: StarReminderPromptUI, deps?: StarReminderDeps): Promise<void> {
|
|
289
|
+
try {
|
|
290
|
+
const statePath = resolveStatePath(deps);
|
|
291
|
+
const state = await readStarReminderStateUnlocked(statePath);
|
|
292
|
+
// Declined users no longer see the launch prompt; the injection path
|
|
293
|
+
// handles re-checks for them.
|
|
294
|
+
if (state.declined) return;
|
|
295
|
+
if (state.starred && isStarredCacheFresh(state, resolveNow(deps))) return;
|
|
296
|
+
|
|
297
|
+
const status = await checkGhStarred(deps);
|
|
298
|
+
if (status === "starred") {
|
|
299
|
+
await recordFreshStarCheck("starred", deps);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (status === "unavailable") return;
|
|
303
|
+
|
|
304
|
+
// status === "unstarred". Persist this fresh unstarred evidence first so a
|
|
305
|
+
// stale cached starred:true is downgraded and a subsequent No is recorded as
|
|
306
|
+
// declined (recordDeclinedAfterNo only preserves a still-starred state).
|
|
307
|
+
await recordFreshStarCheck("unstarred", deps);
|
|
308
|
+
if (ui.isIdle && !ui.isIdle()) return;
|
|
309
|
+
const accepted = await ui.confirm(LAUNCH_PROMPT_TITLE, LAUNCH_PROMPT_MESSAGE);
|
|
310
|
+
if (accepted) {
|
|
311
|
+
const ok = await autoStarRepo(deps);
|
|
312
|
+
if (ok) await recordStarredFromPut(deps);
|
|
313
|
+
// On PUT failure: stay silent and do not mark starred; a later launch
|
|
314
|
+
// may prompt again while still not declined.
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
await recordDeclinedAfterNo(deps);
|
|
318
|
+
} catch {
|
|
319
|
+
// Never surface star-reminder failures to the user.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Schedule the launch nudge to run after the first render so the networked
|
|
325
|
+
* `gh` check never blocks startup. Returns immediately.
|
|
326
|
+
*/
|
|
327
|
+
export function scheduleLaunchStarReminderAfterFirstRender(ui: StarReminderPromptUI, deps?: StarReminderDeps): void {
|
|
328
|
+
setTimeout(() => {
|
|
329
|
+
void maybeShowLaunchStarReminder(ui, deps);
|
|
330
|
+
}, 0);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface StarReminderLaunchGateInput {
|
|
334
|
+
/** The `starReminder.enabled` setting. */
|
|
335
|
+
enabled: boolean;
|
|
336
|
+
/** The `startup.quiet` setting. */
|
|
337
|
+
quiet: boolean;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface StarReminderLaunchGate {
|
|
341
|
+
/** Whether to register the decline-driven injection contributor. */
|
|
342
|
+
register: boolean;
|
|
343
|
+
/** Whether to schedule the launch nudge after first render. */
|
|
344
|
+
schedule: boolean;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Pure decision for interactive wiring. The injection contributor is registered
|
|
349
|
+
* whenever the feature is enabled; the launch nudge is additionally suppressed
|
|
350
|
+
* by quiet startup. Centralizing this keeps the interactive gate testable.
|
|
351
|
+
*/
|
|
352
|
+
export function starReminderLaunchGate(input: StarReminderLaunchGateInput): StarReminderLaunchGate {
|
|
353
|
+
return { register: input.enabled, schedule: input.enabled && !input.quiet };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// --------------------------------------------------------------------------
|
|
357
|
+
// Decline-driven injection
|
|
358
|
+
// --------------------------------------------------------------------------
|
|
359
|
+
|
|
360
|
+
export interface StarReminderSessionRef {
|
|
361
|
+
getSessionId(): string | undefined;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export type StarReminderCustomMessage = Pick<
|
|
365
|
+
CustomMessage,
|
|
366
|
+
"customType" | "content" | "display" | "details" | "attribution"
|
|
367
|
+
>;
|
|
368
|
+
|
|
369
|
+
export type InternalBeforeAgentStartContributor = (event: {
|
|
370
|
+
prompt: string;
|
|
371
|
+
images?: ImageContent[];
|
|
372
|
+
sessionId: string | undefined;
|
|
373
|
+
}) => Promise<StarReminderCustomMessage | undefined>;
|
|
374
|
+
|
|
375
|
+
const PERSUASION_CONTENT =
|
|
376
|
+
`The user previously declined the launch GitHub-star prompt. At an appropriate moment in this response, ` +
|
|
377
|
+
`briefly and politely encourage starring https://github.com/${STAR_REMINDER_REPO} to support the project. ` +
|
|
378
|
+
`Ask at most once and do not be pushy.`;
|
|
379
|
+
|
|
380
|
+
export function createStarReminderMessage(): StarReminderCustomMessage {
|
|
381
|
+
const attribution: MessageAttribution = "agent";
|
|
382
|
+
return {
|
|
383
|
+
customType: STAR_REMINDER_CUSTOM_TYPE,
|
|
384
|
+
content: PERSUASION_CONTENT,
|
|
385
|
+
display: false,
|
|
386
|
+
attribution,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Build a before-agent-start contributor that injects the persuasion message
|
|
392
|
+
* once per logical session id, for declined-and-still-unstarred users only.
|
|
393
|
+
*/
|
|
394
|
+
export function createStarReminderBeforeAgentStartContributor(
|
|
395
|
+
session: StarReminderSessionRef,
|
|
396
|
+
deps?: StarReminderDeps,
|
|
397
|
+
): InternalBeforeAgentStartContributor {
|
|
398
|
+
const injectedSessionIds = new Set<string>();
|
|
399
|
+
return async event => {
|
|
400
|
+
try {
|
|
401
|
+
const state = await readStarReminderStateUnlocked(resolveStatePath(deps));
|
|
402
|
+
if (!state.declined) return undefined;
|
|
403
|
+
|
|
404
|
+
// Without a stable logical session id we cannot enforce once-per-session,
|
|
405
|
+
// so prefer not injecting at all.
|
|
406
|
+
const sessionId = session.getSessionId() ?? event.sessionId;
|
|
407
|
+
if (!sessionId) return undefined;
|
|
408
|
+
if (injectedSessionIds.has(sessionId)) return undefined;
|
|
409
|
+
|
|
410
|
+
// Process each logical session at most once, regardless of outcome:
|
|
411
|
+
// even when gh is unavailable we must not re-check (and possibly delay)
|
|
412
|
+
// on every subsequent prompt in the same session.
|
|
413
|
+
injectedSessionIds.add(sessionId);
|
|
414
|
+
|
|
415
|
+
const status = await refreshStarStateForSession(deps);
|
|
416
|
+
if (status !== "unstarred") return undefined;
|
|
417
|
+
return createStarReminderMessage();
|
|
418
|
+
} catch {
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
@@ -304,6 +304,7 @@ export class MCPManager {
|
|
|
304
304
|
config: MCPServerConfig;
|
|
305
305
|
tracked: TrackedPromise<ToolLoadResult>;
|
|
306
306
|
toolsPromise: Promise<ToolLoadResult>;
|
|
307
|
+
connectionAbort: AbortController;
|
|
307
308
|
};
|
|
308
309
|
|
|
309
310
|
const errors = new Map<string, string>();
|
|
@@ -424,7 +425,7 @@ export class MCPManager {
|
|
|
424
425
|
this.#pendingToolLoads.set(name, toolsPromise);
|
|
425
426
|
|
|
426
427
|
const tracked = trackPromise(toolsPromise);
|
|
427
|
-
connectionTasks.push({ name, config, tracked, toolsPromise });
|
|
428
|
+
connectionTasks.push({ name, config, tracked, toolsPromise, connectionAbort });
|
|
428
429
|
|
|
429
430
|
void toolsPromise
|
|
430
431
|
.then(async ({ connection, serverTools }) => {
|
|
@@ -475,7 +476,19 @@ export class MCPManager {
|
|
|
475
476
|
|
|
476
477
|
const pendingWithoutCache = pendingTasks.filter(task => !cachedTools.has(task.name));
|
|
477
478
|
if (pendingWithoutCache.length > 0) {
|
|
478
|
-
|
|
479
|
+
for (const task of pendingWithoutCache) {
|
|
480
|
+
const message = `MCP server connection timed out during startup: ${task.name}`;
|
|
481
|
+
errors.set(task.name, message);
|
|
482
|
+
reportedErrors.add(task.name);
|
|
483
|
+
task.connectionAbort.abort(new Error(message));
|
|
484
|
+
if (this.#pendingConnections.has(task.name)) this.#pendingConnections.delete(task.name);
|
|
485
|
+
if (this.#pendingToolLoads.get(task.name) === task.toolsPromise)
|
|
486
|
+
this.#pendingToolLoads.delete(task.name);
|
|
487
|
+
this.#pendingConnectionControllers.delete(task.name);
|
|
488
|
+
}
|
|
489
|
+
// Do not await these promises here: a misbehaving stdio/MCP transport can ignore
|
|
490
|
+
// AbortSignal and keep startup blocked indefinitely. The background toolsPromise
|
|
491
|
+
// handler will clean up if it eventually settles.
|
|
479
492
|
}
|
|
480
493
|
}
|
|
481
494
|
|
package/src/sdk.ts
CHANGED
|
@@ -868,7 +868,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
868
868
|
imageProvider === "auto" ||
|
|
869
869
|
imageProvider === "openai" ||
|
|
870
870
|
imageProvider === "gemini" ||
|
|
871
|
-
imageProvider === "openrouter"
|
|
871
|
+
imageProvider === "openrouter" ||
|
|
872
|
+
imageProvider === "antigravity"
|
|
872
873
|
) {
|
|
873
874
|
setPreferredImageProvider(imageProvider);
|
|
874
875
|
}
|
|
@@ -1222,6 +1223,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1222
1223
|
timestamp: Date.now(),
|
|
1223
1224
|
}),
|
|
1224
1225
|
sendCustomMessage: (msg, opts) => session.sendCustomMessage(msg, opts),
|
|
1226
|
+
purgeQueuedCustomMessages: predicate => session.purgeQueuedCustomMessages(predicate),
|
|
1225
1227
|
peekQueueInvoker: () => session.peekQueueInvoker(),
|
|
1226
1228
|
peekStandingResolveHandler: () => session.peekStandingResolveHandler(),
|
|
1227
1229
|
setStandingResolveHandler: handler => session.setStandingResolveHandler(handler),
|