@quantiya/codevibe-antigravity-plugin 2.0.85 → 2.0.87
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 +2 -2
- package/antigravity-plugin.json +1 -1
- package/dist/server.js +496 -100
- package/libexec/companion-launcher +48 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ install the unified `codevibe --agent` interface yet.
|
|
|
45
45
|
- **macOS, Linux, or WSL Ubuntu** — Windows without WSL is not supported
|
|
46
46
|
- **Node.js** 22.0.0+
|
|
47
47
|
- **tmux** — `brew install tmux` on macOS, `apt install tmux` on Linux/WSL
|
|
48
|
-
- **Antigravity CLI (`agy`)
|
|
48
|
+
- **Antigravity CLI (`agy`) 1.4.3+** with `--log-file` advertised by `agy --help` — install from [Google's documentation](https://antigravity.google/docs). Companion checks this before creating a session and tells unsupported clients to update; direct `agy` is unchanged.
|
|
49
49
|
|
|
50
50
|
> Antigravity CLI is distributed by Google as a binary; it is not on npm. After installing `agy`, run `codevibe --agent agy` and the plugin handles the rest.
|
|
51
51
|
|
|
@@ -66,7 +66,7 @@ All agy sessions on a computer write to the same folders. CodeVibe tells them ap
|
|
|
66
66
|
|
|
67
67
|
On each launch, `codevibe --agent agy` registers one named entry, `codevibe-antigravity`, in `~/.gemini/config/hooks.json`, and keeps the hook scripts in `~/.codevibe/codevibe-agy/hooks/`:
|
|
68
68
|
- Other entries in that file are left as they are.
|
|
69
|
-
- A file that is not valid JSON, or is a symlink, is never changed. CodeVibe
|
|
69
|
+
- A file that is not valid JSON, or is a symlink, is never changed. Without hooks, CodeVibe uses only this launch's pinned native log and a `USER_INPUT`-first main transcript; unknown ownership waits, and `SYSTEM_MESSAGE`-first subagents are excluded.
|
|
70
70
|
- For other agy sessions on the machine, `PreToolUse` immediately permits execution with `{"decision":"allow"}`, and other hook events are skipped.
|
|
71
71
|
- To switch it off, add `"enabled": false` to the `codevibe-antigravity` entry. CodeVibe leaves a switched-off entry alone. Deleting the entry only lasts until the next `codevibe --agent agy` launch, which writes it again. After uninstalling CodeVibe you can delete the entry; see INSTALLATION.md.
|
|
72
72
|
|
package/antigravity-plugin.json
CHANGED
|
@@ -6,6 +6,6 @@
|
|
|
6
6
|
"implementation": {
|
|
7
7
|
"architecture": "transcript+tmux+approval-heuristic",
|
|
8
8
|
"schema_version": "v1.0",
|
|
9
|
-
"note": "Monitors ~/.gemini/antigravity-cli/brain/<UUID>/.system_generated/logs/transcript.jsonl and the tmux pane.
|
|
9
|
+
"note": "Requires native agy 1.4.3+ advertising --log-file; checked before Companion session creation. Monitors ~/.gemini/antigravity-cli/brain/<UUID>/.system_generated/logs/transcript.jsonl and the tmux pane. Own agy JSON hooks identify main conversations. Without hooks, ownership requires this launch's pinned native log and a USER_INPUT-first main transcript; SYSTEM_MESSAGE-first subagents and shared/newest logs are excluded. Unknown ownership defers sync."
|
|
10
10
|
}
|
|
11
11
|
}
|
package/dist/server.js
CHANGED
|
@@ -32,6 +32,7 @@ var server_exports = {};
|
|
|
32
32
|
__export(server_exports, {
|
|
33
33
|
ApprovalDetector: () => ApprovalDetector,
|
|
34
34
|
McpServer: () => McpServer,
|
|
35
|
+
PromptResponder: () => PromptResponder,
|
|
35
36
|
SessionNotFoundError: () => SessionNotFoundError,
|
|
36
37
|
StoredAuthFailureError: () => StoredAuthFailureError,
|
|
37
38
|
TERMINAL_SHUTDOWN_SIGNALS: () => TERMINAL_SHUTDOWN_SIGNALS,
|
|
@@ -789,6 +790,20 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
789
790
|
getActiveConversations() {
|
|
790
791
|
return Array.from(this.activeConversations);
|
|
791
792
|
}
|
|
793
|
+
/** Retry a deferred native read after THIS launcher's hook proves ownership.
|
|
794
|
+
* No offset reset or legacy fallback. Join an in-flight read first so a late
|
|
795
|
+
* hook cannot lose its wakeup in that read's rejection/finally cleanup. */
|
|
796
|
+
retryOwnedConversation(conversationId) {
|
|
797
|
+
if (!this.admission || !this.isWatching || !CONVERSATION_DIR_REGEX.test(conversationId)) return;
|
|
798
|
+
const generation = this.readGeneration, filePath = this.transcriptPathFor(conversationId);
|
|
799
|
+
const pending = this.activeReads.get(filePath) ?? Promise.resolve();
|
|
800
|
+
void pending.then(() => {
|
|
801
|
+
if (!this.isWatching || generation !== this.readGeneration) return;
|
|
802
|
+
if (!fs2.existsSync(filePath)) return;
|
|
803
|
+
this.ensureOffset(filePath);
|
|
804
|
+
void this.scheduleReadNewLines(filePath, conversationId);
|
|
805
|
+
}).catch((error) => logger.warn("Owned transcript wakeup failed", { conversationId, error: String(error) }));
|
|
806
|
+
}
|
|
792
807
|
// ─── follower event handlers ─────────────────────────────────────────────
|
|
793
808
|
onFileAdded(filePath) {
|
|
794
809
|
if (this.isShuttingDown) return;
|
|
@@ -1233,6 +1248,10 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
1233
1248
|
}
|
|
1234
1249
|
const generation = this.readGeneration;
|
|
1235
1250
|
const read = this.drainReadQueue(filePath, conversationId, generation).catch((error) => {
|
|
1251
|
+
if (error?.code === "SYNC_SOURCE_OWNERSHIP_PENDING") {
|
|
1252
|
+
logger.debug("Transcript waiting for its own hook or process-bound log", { conversationId });
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1236
1255
|
logger.error("Transcript read chain failed (non-fatal) \u2014 next change event retries", {
|
|
1237
1256
|
filePath,
|
|
1238
1257
|
conversationId,
|
|
@@ -2924,6 +2943,7 @@ function isComposerBoxAt(lines, idx) {
|
|
|
2924
2943
|
}
|
|
2925
2944
|
return false;
|
|
2926
2945
|
}
|
|
2946
|
+
var ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
2927
2947
|
function isPaneInputSafetyBlocked(input) {
|
|
2928
2948
|
const rawText = Array.isArray(input) ? input.join("\n") : input;
|
|
2929
2949
|
if (!rawText) return { blocked: false, kind: null };
|
|
@@ -3093,9 +3113,10 @@ function isPaneInputSafetyBlocked(input) {
|
|
|
3093
3113
|
return { blocked: false, kind: null };
|
|
3094
3114
|
}
|
|
3095
3115
|
function looksLikePromptDelta(chunk) {
|
|
3116
|
+
const text = chunk.includes("\x1B") ? chunk.replace(ANSI_ESCAPE_REGEX, "") : chunk;
|
|
3096
3117
|
return (
|
|
3097
3118
|
// Approval UI signatures
|
|
3098
|
-
/Requesting permission for:/i.test(chunk) || /Do you want to proceed\?/i.test(chunk) || /\besc to cancel\b/i.test(
|
|
3119
|
+
/Requesting permission for:/i.test(chunk) || /Do you want to proceed\?/i.test(chunk) || /\besc to cancel\b/i.test(text) || /^\s*[1-9]\. (?:Yes|No)\b/im.test(text) || /\[(?:y\/n|Y\/n|y\/N)\]/.test(chunk) || /^\s*(?:[❯>]\s*)?\?\s+(?!for\s+(?:shortcuts|help|commands))\S/im.test(text) || /Question\b(?:\s+\d+(?:[\/:of\s]+\d+)?)?:/i.test(text) || /\benter\s+(?:to\s+)?(?:select|submit(?:\s+all)?|next)\b/i.test(text) || /\b(?:navigate|use\s+arrow\s+keys)\b/i.test(text) || /\b\d+[\/:of\s]+\d+:\s+\S/i.test(text) || /\bSubmit(?:\s+All)?\b/i.test(text) || /(?:^|[\s·|│])(?:←|<-)\s*Back\b/i.test(text) || /^File access\s*$/im.test(text) || /^\s*Allow access to this file\?\s*$/im.test(text) || /(?:Yes, allow access|No, deny access)/i.test(text) || /(?:↑\/↓\s+Navigate|ctrl\+r\s+Review)/i.test(text) || /Our servers are experiencing/i.test(text) || /Error ID:\s*[0-9a-fA-F-]*/i.test(text) || /(?:⚠️|⚠\uFE0F?)\s+.*(?:high[- ]traffic|try again in a minute|try again later|stream was interrupted|stream interrupted|overloaded|unexpected error|quota\b|upgrade your subscription|increase your limits|resets in\s+\d|credits balance is too low|out of credits|rate[- ]limit|capacity exhausted|exhausted your quota|internal error|currently unreachable|network issue connecting|authentication expired|authentication issue|terminated due to error|invalid output|invalid tool call)/i.test(text)
|
|
3099
3120
|
);
|
|
3100
3121
|
}
|
|
3101
3122
|
function isNonSystemErrorWarningLine(trimmed) {
|
|
@@ -4216,7 +4237,7 @@ function isQuestionFooterLine(line) {
|
|
|
4216
4237
|
}
|
|
4217
4238
|
if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) return false;
|
|
4218
4239
|
if (isQuestionHeaderLine(trimmed)) return false;
|
|
4219
|
-
return /^(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip)(?:\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?select)?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:enter|<enter>)\s+(?:to\s+)?select(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:enter|<enter>)?\s*(?:Select|to select))(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel))?$/i.test(trimmed) || /^press\s+<?(?:enter|return|space)>?\s+to\s+select$/i.test(trimmed) || /^space\s+to\s+select$/i.test(trimmed) || /^(?:\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?)$/i.test(trimmed) || /^(?:[\u2800-\u28FF]+(?:\s*waiting(?:\.\.\.)?)?|waiting(?:\.\.\.)?)$/i.test(trimmed);
|
|
4240
|
+
return /^(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip)(?:\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?)(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip|back))?(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:space|<space>)\s+(?:to\s+)?(?:select|toggle)(?:\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?))?(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip|back))?$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:(?:→|->)\s*Next|(?:←|<-)\s*Back))*(?:\s*[·│,]\s*(?:enter|<enter>)?\s*(?:Select|Submit(?:\s+All)?|to select))(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel|Back))?(?:\s*[·│,]\s*(?:(?:→|->)\s*Next|(?:←|<-)\s*Back))*(?:\s*[·│,]\s*ctrl\+[a-z0-9]+\s+[a-zA-Z]+)*$/i.test(trimmed) || /^(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate(?:\s*[·│,]\s*(?:esc|<esc>)?\s*(?:Skip|Cancel|Back))?$/i.test(trimmed) || /^(?:←|<-)\s*Back\s*[·│,]\s*(?:enter|<enter>)\s+(?:to\s+)?(?:select|submit(?:\s+all)?)(?:\s*[·│,]\s*(?:esc|<esc>)\s+(?:to\s+)?(?:cancel|skip))?$/i.test(trimmed) || /^press\s+<?(?:enter|return|space)>?\s+to\s+select$/i.test(trimmed) || /^space\s+to\s+select$/i.test(trimmed) || /^(?:\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?)$/i.test(trimmed) || /^(?:[\u2800-\u28FF]+(?:\s*waiting(?:\.\.\.)?)?|waiting(?:\.\.\.)?)$/i.test(trimmed);
|
|
4220
4241
|
}
|
|
4221
4242
|
function extractQuestionHeader(snapshot) {
|
|
4222
4243
|
const stripped = snapshot.replace(ANSI_ESCAPE_REGEX, "");
|
|
@@ -4242,7 +4263,6 @@ function hashPromptSnapshot(snapshot) {
|
|
|
4242
4263
|
}
|
|
4243
4264
|
return (0, import_crypto.createHash)("sha256").update(normalized).digest("hex");
|
|
4244
4265
|
}
|
|
4245
|
-
var ANSI_ESCAPE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
4246
4266
|
function escapeShellArg(value) {
|
|
4247
4267
|
return value.replace(/'/g, `'\\''`);
|
|
4248
4268
|
}
|
|
@@ -4387,33 +4407,8 @@ function parseQuestionSnapshot(stripped, originalSnapshot, header) {
|
|
|
4387
4407
|
const options = parseOptions(belowHeader);
|
|
4388
4408
|
if (options.length < 2) return null;
|
|
4389
4409
|
const normalizedHeader = header.fullLine.replace(/^[❯>\s*•-]+\s*/, "").trim();
|
|
4390
|
-
const
|
|
4391
|
-
|
|
4392
|
-
let footerHasCtrlRReview = false;
|
|
4393
|
-
const startLine = headerIdx >= 0 ? headerIdx + 1 : 0;
|
|
4394
|
-
let hasSeenOptions = false;
|
|
4395
|
-
for (let i = startLine; i < lines.length; i++) {
|
|
4396
|
-
if (lineInFence[i]) continue;
|
|
4397
|
-
const trimmed = lines[i].trim();
|
|
4398
|
-
if (!trimmed) continue;
|
|
4399
|
-
if (hasSeenOptions && isResumedTurnSignal(trimmed)) break;
|
|
4400
|
-
if (/^[>\s❯]*[1-9][.)]\s+\S/.test(trimmed)) {
|
|
4401
|
-
hasSeenOptions = true;
|
|
4402
|
-
}
|
|
4403
|
-
const isFooter = isQuestionFooterLine(trimmed) || /^(?:ctrl\+[a-z0-9]+\s+[a-zA-Z]+|\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?)$/i.test(trimmed);
|
|
4404
|
-
if (isFooter) {
|
|
4405
|
-
if (/(?:↑\/↓|↑|↓|\^|v)\s*(?:\/|\s+)?Navigate\b/i.test(trimmed) || /\(?use\s+arrow\s+keys(?:\s+to\s+navigate)?\)?/i.test(trimmed)) {
|
|
4406
|
-
footerHasArrowHint = true;
|
|
4407
|
-
}
|
|
4408
|
-
if (/ctrl\+r\s+Review\b/i.test(trimmed)) {
|
|
4409
|
-
footerHasCtrlRReview = true;
|
|
4410
|
-
}
|
|
4411
|
-
}
|
|
4412
|
-
}
|
|
4413
|
-
const hasArrowKeyHint = headerHasArrowHint || footerHasArrowHint;
|
|
4414
|
-
const hasWriteInOption = options.some((opt) => /^\s*write-?in\b/i.test(opt.text));
|
|
4415
|
-
const hasCtrlRReviewFooter = footerHasCtrlRReview;
|
|
4416
|
-
const isBaseNumberedWizard = /^Question\s+\d+\/\d+:/i.test(normalizedHeader) && !hasArrowKeyHint && !hasWriteInOption && !hasCtrlRReviewFooter;
|
|
4410
|
+
const isMultiSelect = /\bselect\s+(?:all|multiple)\b/i.test(normalizedHeader) || /\bselect\s+(?:all|multiple)\b/i.test(belowHeader) || /(?:<space>|\bspace)\s+(?:to\s+)?(?:select|toggle)\b/i.test(belowHeader) || /\bctrl\+a\s+select\s+all\b/i.test(belowHeader);
|
|
4411
|
+
const isBaseNumberedWizard = /^Question\s+\d+\/\d+:/i.test(normalizedHeader) && !isMultiSelect;
|
|
4417
4412
|
const submitMap = isBaseNumberedWizard ? buildQuestionSubmitMap(options) : {};
|
|
4418
4413
|
const body = extractBodyBetweenHeaderAndFirstOption(belowHeader);
|
|
4419
4414
|
return {
|
|
@@ -4476,12 +4471,47 @@ function buildApprovalSubmitMap(options) {
|
|
|
4476
4471
|
function buildQuestionSubmitMap(options) {
|
|
4477
4472
|
const map = {};
|
|
4478
4473
|
for (const opt of options) {
|
|
4479
|
-
|
|
4480
|
-
map[opt.number] = [opt.number, "Enter"];
|
|
4481
|
-
}
|
|
4474
|
+
map[opt.number] = [opt.number];
|
|
4482
4475
|
}
|
|
4483
4476
|
return map;
|
|
4484
4477
|
}
|
|
4478
|
+
function isWriteInInputActive(paneText, expectedHeader, expectedStep) {
|
|
4479
|
+
if (!paneText) return false;
|
|
4480
|
+
if (detectActivePromptKind(paneText) !== "question") return false;
|
|
4481
|
+
const stripped = paneText.replace(ANSI_ESCAPE_REGEX2, "");
|
|
4482
|
+
const lines = stripped.trimEnd().split("\n");
|
|
4483
|
+
const windowStart = Math.max(0, lines.length - 30);
|
|
4484
|
+
let headerIdx = -1;
|
|
4485
|
+
for (let i = lines.length - 1; i >= windowStart; i--) {
|
|
4486
|
+
if (isQuestionHeaderLine(lines[i])) {
|
|
4487
|
+
headerIdx = i;
|
|
4488
|
+
break;
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
if (headerIdx < 0) return false;
|
|
4492
|
+
const rawHeader = lines[headerIdx];
|
|
4493
|
+
const extracted = extractQuestionHeader(rawHeader);
|
|
4494
|
+
if (!extracted) return false;
|
|
4495
|
+
if (expectedHeader) {
|
|
4496
|
+
const expectedExtracted = extractQuestionHeader(expectedHeader);
|
|
4497
|
+
const expectedBody = (expectedExtracted ? expectedExtracted.body : expectedHeader).replace(/^[❯>\s*•-]+\s*/, "").trim().toLowerCase();
|
|
4498
|
+
const paneBody = extracted.body.replace(/^[❯>\s*•-]+\s*/, "").trim().toLowerCase();
|
|
4499
|
+
if (paneBody !== expectedBody) {
|
|
4500
|
+
return false;
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
const expectedStepVal = expectedStep || (expectedHeader ? expectedHeader.match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i)?.[1] : void 0);
|
|
4504
|
+
if (expectedStepVal) {
|
|
4505
|
+
const stepMatch = extracted.fullLine.match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i);
|
|
4506
|
+
const paneStep = stepMatch ? stepMatch[1] : null;
|
|
4507
|
+
if (!paneStep || paneStep !== expectedStepVal) {
|
|
4508
|
+
return false;
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
const searchLines = lines.slice(headerIdx);
|
|
4512
|
+
const text = searchLines.join("\n");
|
|
4513
|
+
return /Your answer:/i.test(text) && /enter\s+Submit(?:\s+All)?\s*[·│,]\s*esc\s+Back/i.test(text);
|
|
4514
|
+
}
|
|
4485
4515
|
function extractIdentity(headerText) {
|
|
4486
4516
|
if (!headerText) return {};
|
|
4487
4517
|
const trimmed = headerText.trim();
|
|
@@ -5207,13 +5237,15 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
5207
5237
|
const qOptTexts = (targetQ.options || []).map((o) => (typeof o === "string" ? o : o.text).toLowerCase().trim());
|
|
5208
5238
|
const cOptTexts = (candidate.options || []).map((o) => (typeof o === "string" ? o : o.text).toLowerCase().trim());
|
|
5209
5239
|
if (cOptTexts.length > 0 && qOptTexts.length > 0) {
|
|
5210
|
-
const
|
|
5211
|
-
const
|
|
5240
|
+
const hasTrailingWriteIn = cOptTexts.length === qOptTexts.length + 1 && /^\s*write-?in\b/i.test(cOptTexts[cOptTexts.length - 1]);
|
|
5241
|
+
const cEffectiveTexts = hasTrailingWriteIn ? cOptTexts.slice(0, -1) : cOptTexts;
|
|
5242
|
+
const matchedQCount = qOptTexts.filter((q) => cEffectiveTexts.includes(q)).length;
|
|
5243
|
+
const matchedCCount = cEffectiveTexts.filter((c) => qOptTexts.includes(c)).length;
|
|
5212
5244
|
const qRatio = matchedQCount / qOptTexts.length;
|
|
5213
|
-
if (
|
|
5245
|
+
if (cEffectiveTexts.length !== qOptTexts.length || matchedQCount !== qOptTexts.length || matchedCCount !== cEffectiveTexts.length) {
|
|
5214
5246
|
return { score: -1, textMatch: true };
|
|
5215
5247
|
}
|
|
5216
|
-
const posMatches = qOptTexts.filter((q, idx) => idx <
|
|
5248
|
+
const posMatches = qOptTexts.filter((q, idx) => idx < cEffectiveTexts.length && cEffectiveTexts[idx] === q).length;
|
|
5217
5249
|
const posRatio = posMatches / qOptTexts.length;
|
|
5218
5250
|
const score = qRatio * 0.5 + posRatio * 0.5;
|
|
5219
5251
|
return { score, textMatch: true };
|
|
@@ -5246,13 +5278,19 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
5246
5278
|
});
|
|
5247
5279
|
return null;
|
|
5248
5280
|
}
|
|
5281
|
+
const targetStepQ = sqs[bestStepIdx];
|
|
5282
|
+
if (targetStepQ?.isMultiSelect) {
|
|
5283
|
+
candidate.submitMap = {};
|
|
5284
|
+
}
|
|
5249
5285
|
const isAdvancement = bestStepIdx !== currentActiveIdx;
|
|
5250
5286
|
if (!isAdvancement) {
|
|
5251
5287
|
const existing = [...this.pendingPrompts.values()].find(
|
|
5252
5288
|
(p) => p.conversationId === conversationId && p.kind === "question"
|
|
5253
5289
|
);
|
|
5254
5290
|
if (existing) {
|
|
5255
|
-
const
|
|
5291
|
+
const targetQ = existing.pendingCall?.structuredQuestions?.[existing.wizardStepIndex ?? 0];
|
|
5292
|
+
const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
|
|
5293
|
+
const isInitialUpgrade = existing.isHookPrompt && !isAuthoritativeMultiSelect && Object.keys(existing.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
|
|
5256
5294
|
if (isInitialUpgrade) {
|
|
5257
5295
|
existing.submitMap = candidate.submitMap;
|
|
5258
5296
|
if (candidate.options && candidate.options.length > 0) {
|
|
@@ -5271,12 +5309,30 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
5271
5309
|
this.emit("prompt-upgraded", existing);
|
|
5272
5310
|
return existing;
|
|
5273
5311
|
}
|
|
5274
|
-
|
|
5275
|
-
|
|
5312
|
+
const areControlsMutated = (existingOpts, candidateOpts) => {
|
|
5313
|
+
if (!existingOpts || !candidateOpts) return false;
|
|
5314
|
+
if (existingOpts.length !== candidateOpts.length) return true;
|
|
5315
|
+
return !existingOpts.every((o, idx) => o.number === candidateOpts[idx].number && (o.text || "").trim() === (candidateOpts[idx].text || "").trim());
|
|
5316
|
+
};
|
|
5317
|
+
const isControlDelta = Boolean(
|
|
5318
|
+
existing.paneOptions && existing.paneOptions.length > 0 && candidate.options && candidate.options.length > 0 && areControlsMutated(existing.paneOptions, candidate.options)
|
|
5319
|
+
);
|
|
5320
|
+
if (isControlDelta) {
|
|
5321
|
+
logger.info("Control delta observed on active question step; resolving old prompt and re-emitting", {
|
|
5322
|
+
oldPromptId: existing.promptId,
|
|
5323
|
+
oldSemanticKey: existing.paneSemanticKey,
|
|
5324
|
+
newSemanticKey: candidateSemanticKey
|
|
5325
|
+
});
|
|
5326
|
+
this.resolvePrompt(existing.promptId, { advanceWizard: false });
|
|
5327
|
+
this.emit("prompt-resolved", existing.promptId);
|
|
5328
|
+
} else {
|
|
5329
|
+
if (!existing.paneSemanticKey) {
|
|
5330
|
+
existing.paneSemanticKey = candidateSemanticKey;
|
|
5331
|
+
}
|
|
5332
|
+
if (candidate.body) existing.body = candidate.body;
|
|
5333
|
+
if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
|
|
5334
|
+
return null;
|
|
5276
5335
|
}
|
|
5277
|
-
if (candidate.body) existing.body = candidate.body;
|
|
5278
|
-
if (candidate.paneHash) this.paneOnlyEmittedHashes.add(candidate.paneHash);
|
|
5279
|
-
return null;
|
|
5280
5336
|
}
|
|
5281
5337
|
} else {
|
|
5282
5338
|
liveWizard2.activeStepIndex = bestStepIdx;
|
|
@@ -5298,12 +5354,14 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
5298
5354
|
const hookQuestionText = p.pendingCall?.questionText || p.matchedPaneHeader || p.paneDisplayHeader;
|
|
5299
5355
|
const isSameQuestion = questionsMatch(hookQuestionText, paneQuestionText);
|
|
5300
5356
|
if (isSameQuestion) {
|
|
5301
|
-
const
|
|
5357
|
+
const targetQ = p.pendingCall?.structuredQuestions?.[p.wizardStepIndex ?? 0];
|
|
5358
|
+
const isAuthoritativeMultiSelect = Boolean(targetQ?.isMultiSelect);
|
|
5359
|
+
const isInitialUpgrade = p.isHookPrompt && !isAuthoritativeMultiSelect && Object.keys(p.submitMap || {}).length === 0 && Object.keys(candidate.submitMap || {}).length > 0;
|
|
5302
5360
|
const isControlDelta = !isInitialUpgrade && Boolean(
|
|
5303
5361
|
p.paneSemanticKey && candidateSemanticKey !== p.paneSemanticKey
|
|
5304
5362
|
);
|
|
5305
5363
|
if (!isControlDelta) {
|
|
5306
|
-
if (
|
|
5364
|
+
if (isInitialUpgrade) {
|
|
5307
5365
|
p.submitMap = candidate.submitMap;
|
|
5308
5366
|
if (candidate.options && candidate.options.length > 0) {
|
|
5309
5367
|
p.paneOptions = candidate.options;
|
|
@@ -5424,6 +5482,16 @@ var ApprovalDetector = class extends import_events3.EventEmitter {
|
|
|
5424
5482
|
} else {
|
|
5425
5483
|
p.pendingCall = call;
|
|
5426
5484
|
}
|
|
5485
|
+
const isHookMultiSelect = Boolean(
|
|
5486
|
+
call.isMultiSelect || call.structuredQuestions && call.structuredQuestions[0]?.isMultiSelect
|
|
5487
|
+
);
|
|
5488
|
+
if (isHookMultiSelect && Object.keys(p.submitMap || {}).length > 0) {
|
|
5489
|
+
p.submitMap = {};
|
|
5490
|
+
logger.info("emitHookQuestionPrompt: revoked actionable submitMap because incoming hook is multi-select", {
|
|
5491
|
+
promptId: p.promptId
|
|
5492
|
+
});
|
|
5493
|
+
this.emit("prompt-upgraded", p);
|
|
5494
|
+
}
|
|
5427
5495
|
p.isHookPrompt = true;
|
|
5428
5496
|
call.matchedPromptId = p.promptId;
|
|
5429
5497
|
if (call.structuredQuestions && call.structuredQuestions.length > 0) {
|
|
@@ -5961,23 +6029,22 @@ var PromptResponder = class {
|
|
|
5961
6029
|
}
|
|
5962
6030
|
/**
|
|
5963
6031
|
* Resolve a pending approval prompt with the mobile-user's chosen
|
|
5964
|
-
* option number
|
|
5965
|
-
*
|
|
5966
|
-
*
|
|
6032
|
+
* option number and optional follow-up text for write-in questions.
|
|
6033
|
+
* Uses the submitMap from the PendingPromptState to translate
|
|
6034
|
+
* option-number → terminal keystrokes.
|
|
5967
6035
|
*
|
|
5968
6036
|
* Active-prompt-identity guard (DESIGN.md §3.4):
|
|
5969
6037
|
* 1. Detector check: is `promptId` still in pendingPrompts? If not
|
|
5970
6038
|
* (expired or already-resolved), drop with `prompt-expired`.
|
|
5971
|
-
* 2.
|
|
5972
|
-
*
|
|
5973
|
-
*
|
|
5974
|
-
*
|
|
5975
|
-
*
|
|
5976
|
-
*
|
|
5977
|
-
*
|
|
5978
|
-
* 4. Send the keystrokes via tmux send-keys.
|
|
6039
|
+
* 2. submitMap lookup: is `optionNumber` a valid key? If not, drop with `invalid-option`.
|
|
6040
|
+
* If `followUpText` is provided, verify option is Write-in; otherwise drop with `follow-up-not-allowed`.
|
|
6041
|
+
* If bare Write-in option has no text, drop with `write-in-text-required`.
|
|
6042
|
+
* 3. Pane-observer check: captureSnapshot() — does the pane STILL show
|
|
6043
|
+
* an approval UI matching our pending prompt's semantic key?
|
|
6044
|
+
* 4. Send option keystrokes via tmux send-keys.
|
|
6045
|
+
* 5. If write-in followUpText is present: wait 250ms, verify pane state, type literal text, send Enter.
|
|
5979
6046
|
*/
|
|
5980
|
-
async sendApprovalReply(promptId, optionNumber) {
|
|
6047
|
+
async sendApprovalReply(promptId, optionNumber, followUpText, beforeTypeText) {
|
|
5981
6048
|
const target = this.resolveTmuxTarget();
|
|
5982
6049
|
if (!target) {
|
|
5983
6050
|
return { ok: false, reason: "no-tmux-target" };
|
|
@@ -5992,15 +6059,6 @@ var PromptResponder = class {
|
|
|
5992
6059
|
details: `promptId ${promptId} is not in pendingPrompts`
|
|
5993
6060
|
};
|
|
5994
6061
|
}
|
|
5995
|
-
if (this.paneObserver) {
|
|
5996
|
-
if (!state.paneSemanticKey) {
|
|
5997
|
-
return {
|
|
5998
|
-
ok: false,
|
|
5999
|
-
reason: "prompt-probe-failed",
|
|
6000
|
-
details: "pending prompt has no exact semantic identity"
|
|
6001
|
-
};
|
|
6002
|
-
}
|
|
6003
|
-
}
|
|
6004
6062
|
const keys = state.submitMap[optionNumber];
|
|
6005
6063
|
if (!keys || keys.length === 0) {
|
|
6006
6064
|
return {
|
|
@@ -6009,21 +6067,86 @@ var PromptResponder = class {
|
|
|
6009
6067
|
details: `option '${optionNumber}' not in submitMap (valid: ${Object.keys(state.submitMap).join(",")})`
|
|
6010
6068
|
};
|
|
6011
6069
|
}
|
|
6070
|
+
const isQuestionPrompt = state.kind === "question";
|
|
6071
|
+
const expectedStep = (state.paneDisplayHeader || "").match(/\bQuestion\s+(\d+(?:[\/:of\s]+\d+)?):/i)?.[1];
|
|
6072
|
+
const matchingOpt = state.paneOptions?.find((o) => String(o.number) === String(optionNumber));
|
|
6073
|
+
const isWriteInOption = matchingOpt ? /^\s*write-?in\b/i.test(matchingOpt.text) : false;
|
|
6074
|
+
if (isQuestionPrompt) {
|
|
6075
|
+
if (state.paneOptions && state.paneOptions.length > 0 && !matchingOpt) {
|
|
6076
|
+
return {
|
|
6077
|
+
ok: false,
|
|
6078
|
+
reason: "invalid-option",
|
|
6079
|
+
details: `option '${optionNumber}' not found in advertised options`,
|
|
6080
|
+
deliveryPhase: "none"
|
|
6081
|
+
};
|
|
6082
|
+
}
|
|
6083
|
+
if (isWriteInOption && (!followUpText || followUpText.trim().length === 0)) {
|
|
6084
|
+
return {
|
|
6085
|
+
ok: false,
|
|
6086
|
+
reason: "write-in-text-required",
|
|
6087
|
+
details: 'Please provide your write-in text along with the option number (e.g. "4, your response")',
|
|
6088
|
+
deliveryPhase: "none"
|
|
6089
|
+
};
|
|
6090
|
+
}
|
|
6091
|
+
if (followUpText && followUpText.trim().length > 0 && !isWriteInOption) {
|
|
6092
|
+
return {
|
|
6093
|
+
ok: false,
|
|
6094
|
+
reason: "follow-up-not-allowed",
|
|
6095
|
+
details: `Option '${optionNumber}' does not accept follow-up text`,
|
|
6096
|
+
deliveryPhase: "none"
|
|
6097
|
+
};
|
|
6098
|
+
}
|
|
6099
|
+
}
|
|
6100
|
+
if (this.paneObserver) {
|
|
6101
|
+
if (!state.paneSemanticKey) {
|
|
6102
|
+
return {
|
|
6103
|
+
ok: false,
|
|
6104
|
+
reason: "prompt-probe-failed",
|
|
6105
|
+
details: "pending prompt has no exact semantic identity",
|
|
6106
|
+
deliveryPhase: "none"
|
|
6107
|
+
};
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
let deliveryPhase = "none";
|
|
6012
6111
|
try {
|
|
6013
6112
|
for (const key of keys) {
|
|
6014
6113
|
if (!this.detector || !this.paneObserver) {
|
|
6015
6114
|
return {
|
|
6016
6115
|
ok: false,
|
|
6017
6116
|
reason: "prompt-probe-failed",
|
|
6018
|
-
details: "approval responder is missing its detector or pane observer"
|
|
6117
|
+
details: "approval responder is missing its detector or pane observer",
|
|
6118
|
+
deliveryPhase
|
|
6019
6119
|
};
|
|
6020
6120
|
}
|
|
6021
|
-
|
|
6022
|
-
|
|
6121
|
+
const currentPrompt = this.detector.getPendingPrompt(promptId);
|
|
6122
|
+
if (currentPrompt !== state || !currentPrompt.submitMap || !currentPrompt.submitMap[optionNumber]) {
|
|
6123
|
+
return {
|
|
6124
|
+
ok: false,
|
|
6125
|
+
reason: "prompt-expired",
|
|
6126
|
+
details: "option is no longer actionable on active prompt (controls revoked or updated)",
|
|
6127
|
+
deliveryPhase
|
|
6128
|
+
};
|
|
6023
6129
|
}
|
|
6024
6130
|
let liveSemanticKey = null;
|
|
6025
6131
|
try {
|
|
6026
6132
|
const snapshot = await this.paneObserver.captureSnapshot();
|
|
6133
|
+
const postCapturePrompt = this.detector.getPendingPrompt(promptId);
|
|
6134
|
+
if (postCapturePrompt !== state || !postCapturePrompt.submitMap || !postCapturePrompt.submitMap[optionNumber]) {
|
|
6135
|
+
return {
|
|
6136
|
+
ok: false,
|
|
6137
|
+
reason: "prompt-expired",
|
|
6138
|
+
details: "option is no longer actionable on active prompt (controls revoked or updated)",
|
|
6139
|
+
deliveryPhase
|
|
6140
|
+
};
|
|
6141
|
+
}
|
|
6142
|
+
if (isQuestionPrompt && isWriteInInputActive(snapshot, state.matchedPaneHeader, expectedStep)) {
|
|
6143
|
+
return {
|
|
6144
|
+
ok: false,
|
|
6145
|
+
reason: "write-in-already-active",
|
|
6146
|
+
details: "write-in input field is already open on desktop; finish or cancel on desktop before selecting an option",
|
|
6147
|
+
deliveryPhase: "none"
|
|
6148
|
+
};
|
|
6149
|
+
}
|
|
6027
6150
|
const live = parseApprovalSnapshot(snapshot);
|
|
6028
6151
|
if (!live) {
|
|
6029
6152
|
if (detectActivePromptKind(snapshot) === null) {
|
|
@@ -6031,13 +6154,15 @@ var PromptResponder = class {
|
|
|
6031
6154
|
return {
|
|
6032
6155
|
ok: false,
|
|
6033
6156
|
reason: "prompt-superseded",
|
|
6034
|
-
details: "the approval chooser is no longer active"
|
|
6157
|
+
details: "the approval chooser is no longer active",
|
|
6158
|
+
deliveryPhase
|
|
6035
6159
|
};
|
|
6036
6160
|
}
|
|
6037
6161
|
return {
|
|
6038
6162
|
ok: false,
|
|
6039
6163
|
reason: "prompt-probe-failed",
|
|
6040
|
-
details: "the active chooser could not be parsed exactly"
|
|
6164
|
+
details: "the active chooser could not be parsed exactly",
|
|
6165
|
+
deliveryPhase
|
|
6041
6166
|
};
|
|
6042
6167
|
}
|
|
6043
6168
|
liveSemanticKey = live ? buildApprovalSemanticKey(live) : null;
|
|
@@ -6045,7 +6170,8 @@ var PromptResponder = class {
|
|
|
6045
6170
|
return {
|
|
6046
6171
|
ok: false,
|
|
6047
6172
|
reason: "prompt-probe-failed",
|
|
6048
|
-
details: error instanceof Error ? error.message : String(error)
|
|
6173
|
+
details: error instanceof Error ? error.message : String(error),
|
|
6174
|
+
deliveryPhase
|
|
6049
6175
|
};
|
|
6050
6176
|
}
|
|
6051
6177
|
if (liveSemanticKey !== state.paneSemanticKey) {
|
|
@@ -6053,32 +6179,147 @@ var PromptResponder = class {
|
|
|
6053
6179
|
return {
|
|
6054
6180
|
ok: false,
|
|
6055
6181
|
reason: "prompt-superseded",
|
|
6056
|
-
details: "active chooser semantics differ from the mobile prompt"
|
|
6182
|
+
details: "active chooser semantics differ from the mobile prompt",
|
|
6183
|
+
deliveryPhase
|
|
6057
6184
|
};
|
|
6058
6185
|
}
|
|
6186
|
+
deliveryPhase = "option_attempted";
|
|
6059
6187
|
if (isNamedKey(key)) {
|
|
6060
6188
|
await this.sendKey(target, key);
|
|
6061
6189
|
} else {
|
|
6062
6190
|
await this.typeLiteral(target, key);
|
|
6063
6191
|
}
|
|
6192
|
+
deliveryPhase = "option_sent";
|
|
6193
|
+
await delay(50);
|
|
6194
|
+
}
|
|
6195
|
+
if (isQuestionPrompt && isWriteInOption && followUpText && followUpText.trim().length > 0) {
|
|
6196
|
+
let writeInReady = false;
|
|
6197
|
+
if (this.paneObserver) {
|
|
6198
|
+
for (let probeAttempt = 0; probeAttempt < 10; probeAttempt++) {
|
|
6199
|
+
await delay(100);
|
|
6200
|
+
try {
|
|
6201
|
+
const liveSnap = await this.paneObserver.captureSnapshot();
|
|
6202
|
+
if (liveSnap && isWriteInInputActive(liveSnap, state.matchedPaneHeader, expectedStep)) {
|
|
6203
|
+
writeInReady = true;
|
|
6204
|
+
break;
|
|
6205
|
+
}
|
|
6206
|
+
} catch {
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
if (!writeInReady) {
|
|
6210
|
+
return {
|
|
6211
|
+
ok: false,
|
|
6212
|
+
reason: "prompt-probe-failed",
|
|
6213
|
+
details: "terminal pane failed to transition to write-in input line",
|
|
6214
|
+
deliveryPhase
|
|
6215
|
+
};
|
|
6216
|
+
}
|
|
6217
|
+
} else {
|
|
6218
|
+
await delay(250);
|
|
6219
|
+
}
|
|
6220
|
+
if (beforeTypeText) {
|
|
6221
|
+
try {
|
|
6222
|
+
await beforeTypeText();
|
|
6223
|
+
} catch (err) {
|
|
6224
|
+
return {
|
|
6225
|
+
ok: false,
|
|
6226
|
+
reason: "tmux-failed",
|
|
6227
|
+
details: err instanceof Error ? err.message : String(err),
|
|
6228
|
+
deliveryPhase: "option_sent"
|
|
6229
|
+
// Do not advance phase if hook throws (Claude R4-N2)
|
|
6230
|
+
};
|
|
6231
|
+
}
|
|
6232
|
+
}
|
|
6233
|
+
const postHookPrompt = this.detector.getPendingPrompt(promptId);
|
|
6234
|
+
if (postHookPrompt !== state || !postHookPrompt.submitMap || !postHookPrompt.submitMap[optionNumber]) {
|
|
6235
|
+
return { ok: false, reason: "prompt-expired", deliveryPhase: "option_sent" };
|
|
6236
|
+
}
|
|
6237
|
+
if (this.paneObserver) {
|
|
6238
|
+
try {
|
|
6239
|
+
const postHookSnap = await this.paneObserver.captureSnapshot();
|
|
6240
|
+
if (!postHookSnap || !isWriteInInputActive(postHookSnap, state.matchedPaneHeader, expectedStep)) {
|
|
6241
|
+
return {
|
|
6242
|
+
ok: false,
|
|
6243
|
+
reason: "prompt-probe-failed",
|
|
6244
|
+
details: "write-in input field disappeared during callback before typing",
|
|
6245
|
+
deliveryPhase: "option_sent"
|
|
6246
|
+
};
|
|
6247
|
+
}
|
|
6248
|
+
} catch (err) {
|
|
6249
|
+
return {
|
|
6250
|
+
ok: false,
|
|
6251
|
+
reason: "prompt-probe-failed",
|
|
6252
|
+
details: err instanceof Error ? err.message : String(err),
|
|
6253
|
+
deliveryPhase: "option_sent"
|
|
6254
|
+
};
|
|
6255
|
+
}
|
|
6256
|
+
const postSnapPrompt1 = this.detector.getPendingPrompt(promptId);
|
|
6257
|
+
if (postSnapPrompt1 !== state || !postSnapPrompt1.submitMap || !postSnapPrompt1.submitMap[optionNumber]) {
|
|
6258
|
+
return { ok: false, reason: "prompt-expired", deliveryPhase: "option_sent" };
|
|
6259
|
+
}
|
|
6260
|
+
}
|
|
6261
|
+
deliveryPhase = "text_attempted";
|
|
6262
|
+
const cleanText = followUpText.replace(/[\r\n]+/g, " ").trim();
|
|
6263
|
+
await this.typeLiteral(target, cleanText);
|
|
6264
|
+
deliveryPhase = "text_typed";
|
|
6265
|
+
await delay(50);
|
|
6266
|
+
if (this.paneObserver) {
|
|
6267
|
+
try {
|
|
6268
|
+
const preEnterSnap = await this.paneObserver.captureSnapshot();
|
|
6269
|
+
if (!preEnterSnap || !isWriteInInputActive(preEnterSnap, state.matchedPaneHeader, expectedStep)) {
|
|
6270
|
+
return {
|
|
6271
|
+
ok: false,
|
|
6272
|
+
reason: "prompt-probe-failed",
|
|
6273
|
+
details: "write-in input field disappeared before confirmation",
|
|
6274
|
+
deliveryPhase: "text_typed"
|
|
6275
|
+
};
|
|
6276
|
+
}
|
|
6277
|
+
} catch (err) {
|
|
6278
|
+
return {
|
|
6279
|
+
ok: false,
|
|
6280
|
+
reason: "prompt-probe-failed",
|
|
6281
|
+
details: err instanceof Error ? err.message : String(err),
|
|
6282
|
+
deliveryPhase: "text_typed"
|
|
6283
|
+
};
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
6286
|
+
const postSnapPrompt = this.detector.getPendingPrompt(promptId);
|
|
6287
|
+
if (postSnapPrompt !== state || !postSnapPrompt.submitMap || !postSnapPrompt.submitMap[optionNumber]) {
|
|
6288
|
+
return { ok: false, reason: "prompt-expired", deliveryPhase: "text_typed" };
|
|
6289
|
+
}
|
|
6290
|
+
deliveryPhase = "enter_attempted";
|
|
6291
|
+
await this.sendKey(target, "Enter");
|
|
6292
|
+
deliveryPhase = "completed";
|
|
6064
6293
|
}
|
|
6065
6294
|
this.detector.resolvePrompt(promptId);
|
|
6066
6295
|
logger.info("Sent approval reply via tmux", {
|
|
6067
6296
|
promptId,
|
|
6068
6297
|
optionNumber,
|
|
6069
|
-
keysSent: keys
|
|
6298
|
+
keysSent: keys,
|
|
6299
|
+
hasFollowUpText: Boolean(followUpText),
|
|
6300
|
+
deliveryPhase
|
|
6070
6301
|
});
|
|
6071
|
-
return { ok: true };
|
|
6302
|
+
return { ok: true, deliveryPhase: "completed" };
|
|
6072
6303
|
} catch (error) {
|
|
6073
6304
|
return {
|
|
6074
6305
|
ok: false,
|
|
6075
6306
|
reason: "tmux-failed",
|
|
6076
|
-
details: error instanceof Error ? error.message : String(error)
|
|
6307
|
+
details: error instanceof Error ? error.message : String(error),
|
|
6308
|
+
deliveryPhase
|
|
6077
6309
|
};
|
|
6078
6310
|
}
|
|
6079
6311
|
}
|
|
6080
6312
|
try {
|
|
6081
6313
|
await this.typeLiteral(target, optionNumber);
|
|
6314
|
+
await delay(50);
|
|
6315
|
+
await this.sendKey(target, "Enter");
|
|
6316
|
+
if (followUpText && followUpText.trim().length > 0) {
|
|
6317
|
+
await delay(250);
|
|
6318
|
+
const cleanText = followUpText.replace(/[\r\n]+/g, " ").trim();
|
|
6319
|
+
await this.typeLiteral(target, cleanText);
|
|
6320
|
+
await delay(50);
|
|
6321
|
+
await this.sendKey(target, "Enter");
|
|
6322
|
+
}
|
|
6082
6323
|
return { ok: true };
|
|
6083
6324
|
} catch (error) {
|
|
6084
6325
|
return {
|
|
@@ -6795,6 +7036,7 @@ ${commandErrorText(error, "stdout")}`;
|
|
|
6795
7036
|
}
|
|
6796
7037
|
var McpServer = class _McpServer {
|
|
6797
7038
|
constructor(options) {
|
|
7039
|
+
this.lastNativeLogConversation = null;
|
|
6798
7040
|
/** Per-session causal floor for transcript-event timestamps emitted right
|
|
6799
7041
|
* after a MOBILE prompt. The mobile USER_PROMPT is created by the iOS app
|
|
6800
7042
|
* (real ms clock) and never flows through prepareEventTimestamp, while the
|
|
@@ -6987,6 +7229,15 @@ var McpServer = class _McpServer {
|
|
|
6987
7229
|
this.tmuxTarget = options.tmuxTarget ?? process.env.CODEVIBE_AGY_TMUX_TARGET ?? null;
|
|
6988
7230
|
this.wrapperPid = options.wrapperPid ?? null;
|
|
6989
7231
|
this.cliLogPath = options.cliLogPath ?? null;
|
|
7232
|
+
this.nativeCliLogPath = options.nativeCliLogPath ?? null;
|
|
7233
|
+
this.nativeCliLogIdentity = null;
|
|
7234
|
+
if (this.nativeCliLogPath) {
|
|
7235
|
+
const st = fs7.lstatSync(this.nativeCliLogPath);
|
|
7236
|
+
if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 || st.uid !== process.getuid?.()) {
|
|
7237
|
+
throw Error("AGY_NATIVE_LOG_NOT_OWNED");
|
|
7238
|
+
}
|
|
7239
|
+
this.nativeCliLogIdentity = { dev: st.dev, ino: st.ino };
|
|
7240
|
+
}
|
|
6990
7241
|
this.launchSettleTimeoutMs = options.launchSettleTimeoutMs ?? LAUNCH_SETTLE_TIMEOUT_MS;
|
|
6991
7242
|
this.appSyncClient = options.appSyncClient ?? new import_codevibe_core7.AppSyncClient();
|
|
6992
7243
|
this.toolActivity = options.toolActivityIntegration === void 0 ? new AgyToolActivityIntegration({
|
|
@@ -7614,13 +7865,14 @@ var McpServer = class _McpServer {
|
|
|
7614
7865
|
}
|
|
7615
7866
|
}
|
|
7616
7867
|
/**
|
|
7617
|
-
*
|
|
7618
|
-
*
|
|
7619
|
-
*
|
|
7620
|
-
* another live CodeVibe daemon has claimed is never "the active one" here,
|
|
7621
|
-
* so another session's busy log cannot make us drop our own turn.
|
|
7868
|
+
* Production launchers supply a pinned native log. Hookless routing admits
|
|
7869
|
+
* only its positively identified main transcript. The unbound pre-F1 rule
|
|
7870
|
+
* below remains for legacy/test callers, not durable source allocation.
|
|
7622
7871
|
*/
|
|
7623
7872
|
legacyAcceptsConversation(conversationId) {
|
|
7873
|
+
if (this.nativeCliLogPath) {
|
|
7874
|
+
return !this.claims.isClaimedElsewhere(conversationId) && this.boundNativeLogConversation() === conversationId;
|
|
7875
|
+
}
|
|
7624
7876
|
if (process.env.JEST_WORKER_ID && !this.cliLogPath) {
|
|
7625
7877
|
return true;
|
|
7626
7878
|
}
|
|
@@ -7634,6 +7886,38 @@ var McpServer = class _McpServer {
|
|
|
7634
7886
|
}
|
|
7635
7887
|
return activeId === conversationId;
|
|
7636
7888
|
}
|
|
7889
|
+
/** Never resolve the shared cli.log symlink or scan for another process's log. */
|
|
7890
|
+
boundNativeLogConversation() {
|
|
7891
|
+
if (!this.nativeCliLogPath || !this.nativeCliLogIdentity) return null;
|
|
7892
|
+
let fd;
|
|
7893
|
+
try {
|
|
7894
|
+
fd = fs7.openSync(this.nativeCliLogPath, fs7.constants.O_RDONLY | fs7.constants.O_NOFOLLOW);
|
|
7895
|
+
const st = fs7.fstatSync(fd), expected = this.nativeCliLogIdentity;
|
|
7896
|
+
if (!st.isFile() || st.nlink !== 1 || st.dev !== expected.dev || st.ino !== expected.ino) return null;
|
|
7897
|
+
const matches = [...fs7.readFileSync(fd, "utf8").matchAll(/(?:Created conversation|Streaming conversation|Starting conversation update stream for)\s+([0-9a-fA-F-]{36})/g)];
|
|
7898
|
+
const checked = /* @__PURE__ */ new Set();
|
|
7899
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
7900
|
+
const conversationId = matches[i][1];
|
|
7901
|
+
if (checked.has(conversationId)) continue;
|
|
7902
|
+
checked.add(conversationId);
|
|
7903
|
+
if (this.router.roleOf(conversationId) !== "subagent" && !this.claims.isClaimedElsewhere(conversationId) && this.readConversationFirstType(conversationId) === "USER_INPUT") return conversationId;
|
|
7904
|
+
}
|
|
7905
|
+
return null;
|
|
7906
|
+
} catch {
|
|
7907
|
+
return null;
|
|
7908
|
+
} finally {
|
|
7909
|
+
if (fd !== void 0) fs7.closeSync(fd);
|
|
7910
|
+
}
|
|
7911
|
+
}
|
|
7912
|
+
refreshNativeLogOwnership() {
|
|
7913
|
+
const conversationId = this.boundNativeLogConversation();
|
|
7914
|
+
if (!conversationId || conversationId === this.lastNativeLogConversation) return;
|
|
7915
|
+
this.lastNativeLogConversation = conversationId;
|
|
7916
|
+
if (this.router.roleOf(conversationId) !== "subagent" && !this.claims.isClaimedElsewhere(conversationId)) {
|
|
7917
|
+
logger.info("Agy launch log identified its native conversation", { conversationId, ownership: "process-bound-log" });
|
|
7918
|
+
this.transcriptTailer.retryOwnedConversation(conversationId);
|
|
7919
|
+
}
|
|
7920
|
+
}
|
|
7637
7921
|
/**
|
|
7638
7922
|
* F1 moved the runtime dir under ~/.codevibe, so it outlives reboots. The
|
|
7639
7923
|
* wrapper's EXIT trap removes it, except when the outer terminal closed while
|
|
@@ -7644,6 +7928,12 @@ var McpServer = class _McpServer {
|
|
|
7644
7928
|
removeOrphanedRuntimeDir() {
|
|
7645
7929
|
if (!this.runtimeDir || this.wrapperPid === null || isPidAlive(this.wrapperPid)) return;
|
|
7646
7930
|
try {
|
|
7931
|
+
if (this.nativeCliLogPath && this.nativeCliLogIdentity) {
|
|
7932
|
+
const st = fs7.lstatSync(this.nativeCliLogPath);
|
|
7933
|
+
if (st.isFile() && !st.isSymbolicLink() && st.nlink === 1 && st.dev === this.nativeCliLogIdentity.dev && st.ino === this.nativeCliLogIdentity.ino) {
|
|
7934
|
+
fs7.unlinkSync(this.nativeCliLogPath);
|
|
7935
|
+
}
|
|
7936
|
+
}
|
|
7647
7937
|
fs7.rmdirSync(this.runtimeDir);
|
|
7648
7938
|
logger.info("Removed runtime dir left by a closed terminal", { runtimeDir: this.runtimeDir });
|
|
7649
7939
|
} catch (error) {
|
|
@@ -7715,6 +8005,7 @@ var McpServer = class _McpServer {
|
|
|
7715
8005
|
});
|
|
7716
8006
|
if (role === "main") {
|
|
7717
8007
|
this.observedMainConversationIds.add(conversationId);
|
|
8008
|
+
this.transcriptTailer.retryOwnedConversation(conversationId);
|
|
7718
8009
|
if (!wasAlreadyAdmitted) {
|
|
7719
8010
|
await this.drainHeldHookDeliveries(conversationId, "process", delivery.initialNumSteps);
|
|
7720
8011
|
}
|
|
@@ -7763,6 +8054,7 @@ var McpServer = class _McpServer {
|
|
|
7763
8054
|
logger.warn("PostToolUse prompt resolution failed", { error: String(error) });
|
|
7764
8055
|
});
|
|
7765
8056
|
this.router.onPostToolUse(conversationId);
|
|
8057
|
+
if (this.router.roleOf(conversationId) === "main") this.transcriptTailer.retryOwnedConversation(conversationId);
|
|
7766
8058
|
}
|
|
7767
8059
|
resolveLingeringQuestionPrompts(conversationId) {
|
|
7768
8060
|
const lingeringPrompts = this.approvalDetector.getPendingPromptsForConversation(conversationId).filter((p) => p.kind === "question");
|
|
@@ -8188,8 +8480,9 @@ var McpServer = class _McpServer {
|
|
|
8188
8480
|
const selection = await this.selectNativeSource(session);
|
|
8189
8481
|
if (!this.started || this.session !== session) throw Error("SYNC_SOURCE_OWNER_CHANGED");
|
|
8190
8482
|
if (selection.protocol === "LEGACY") return null;
|
|
8191
|
-
|
|
8192
|
-
|
|
8483
|
+
const ownsConversation = () => !this.claims.isClaimedElsewhere(source.conversationId) && (this.router.roleOf(source.conversationId) === "main" || this.router.roleOf(source.conversationId) === void 0 && this.boundNativeLogConversation() === source.conversationId);
|
|
8484
|
+
if (!ownsConversation()) {
|
|
8485
|
+
throw Object.assign(Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE"), { code: "SYNC_SOURCE_OWNERSHIP_PENDING" });
|
|
8193
8486
|
}
|
|
8194
8487
|
if (!this.toolActivity?.isActive(session.sessionId)) {
|
|
8195
8488
|
if (this.nativeToolStart && this.nativeToolStart.session !== session) throw Error("SYNC_SOURCE_OWNER_CHANGED");
|
|
@@ -8203,7 +8496,12 @@ var McpServer = class _McpServer {
|
|
|
8203
8496
|
}
|
|
8204
8497
|
await this.nativeToolStart.pending;
|
|
8205
8498
|
}
|
|
8206
|
-
if (!
|
|
8499
|
+
if (!ownsConversation()) {
|
|
8500
|
+
throw Object.assign(Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE"), { code: "SYNC_SOURCE_OWNERSHIP_PENDING" });
|
|
8501
|
+
}
|
|
8502
|
+
if (!this.toolActivity?.isActive(session.sessionId) || this.session !== session || !this.started) {
|
|
8503
|
+
throw Error("SYNC_SOURCE_OWNERSHIP_UNAVAILABLE");
|
|
8504
|
+
}
|
|
8207
8505
|
const streamSha256 = crypto3.createHash("sha256").update(JSON.stringify([source.filePath, source.conversationId])).digest("hex");
|
|
8208
8506
|
const key = `${selection.sessionScope.sessionId}:${selection.sessionScope.sessionGenerationId}:${streamSha256}`;
|
|
8209
8507
|
const existing = this.nativeSources.get(key);
|
|
@@ -8247,7 +8545,7 @@ var McpServer = class _McpServer {
|
|
|
8247
8545
|
this.assertNativeOwner(owner);
|
|
8248
8546
|
if ((await owner.cursor.open(0)).byteOffset >= source.endOffset) return;
|
|
8249
8547
|
const admission = { outputs: [null, null] };
|
|
8250
|
-
await this.handleTranscriptEmit(emit, "
|
|
8548
|
+
await this.handleTranscriptEmit(emit, "process", admission);
|
|
8251
8549
|
this.assertNativeOwner(owner);
|
|
8252
8550
|
const echoFrame = {
|
|
8253
8551
|
streamSha256: owner.streamSha256,
|
|
@@ -9060,7 +9358,7 @@ var McpServer = class _McpServer {
|
|
|
9060
9358
|
if (bound && this.observedMainConversationIds.has(bound)) return bound;
|
|
9061
9359
|
if (this.observedMainConversationIds.size === 0) return null;
|
|
9062
9360
|
const cliLogPath = this.cliLogPath ?? path8.join(os7.homedir(), ".gemini", "antigravity-cli", "cli.log");
|
|
9063
|
-
const cliActive = getActiveConversationFromCliLog(cliLogPath);
|
|
9361
|
+
const cliActive = this.nativeCliLogPath ? this.boundNativeLogConversation() : getActiveConversationFromCliLog(cliLogPath);
|
|
9064
9362
|
if (cliActive && this.observedMainConversationIds.has(cliActive)) {
|
|
9065
9363
|
return cliActive;
|
|
9066
9364
|
}
|
|
@@ -9086,9 +9384,7 @@ var McpServer = class _McpServer {
|
|
|
9086
9384
|
let e1rec;
|
|
9087
9385
|
if (e1existing) {
|
|
9088
9386
|
e1rec = e1existing.record;
|
|
9089
|
-
|
|
9090
|
-
e1rec.mobileActionable = true;
|
|
9091
|
-
}
|
|
9387
|
+
e1rec.mobileActionable = !isDisplayOnly;
|
|
9092
9388
|
} else {
|
|
9093
9389
|
e1rec = this.newE1Raise(session.sessionId, !isDisplayOnly);
|
|
9094
9390
|
const oldPromptId = state.promptId;
|
|
@@ -9372,22 +9668,119 @@ var McpServer = class _McpServer {
|
|
|
9372
9668
|
}
|
|
9373
9669
|
}
|
|
9374
9670
|
await this.trackDurableMobileEcho(session, evt, intent.option, "primary");
|
|
9375
|
-
const
|
|
9376
|
-
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9671
|
+
const isQuestionPrompt = activePrompt.kind === "question";
|
|
9672
|
+
if (isQuestionPrompt) {
|
|
9673
|
+
let armedWriteInEcho = false;
|
|
9674
|
+
const optionResult = intent.followUpText ? await this.promptResponder.sendApprovalReply(
|
|
9675
|
+
activePrompt.promptId,
|
|
9676
|
+
intent.option,
|
|
9677
|
+
intent.followUpText,
|
|
9678
|
+
async () => {
|
|
9679
|
+
armedWriteInEcho = true;
|
|
9680
|
+
await this.trackDurableMobileEcho(session, evt, intent.followUpText, "free_form");
|
|
9681
|
+
}
|
|
9682
|
+
) : await this.promptResponder.sendApprovalReply(
|
|
9683
|
+
activePrompt.promptId,
|
|
9684
|
+
intent.option
|
|
9685
|
+
);
|
|
9686
|
+
if (!optionResult.ok) {
|
|
9687
|
+
if (optionResult.reason === "write-in-text-required") {
|
|
9688
|
+
await this.emitPromptSafetyNotification(
|
|
9689
|
+
session,
|
|
9690
|
+
optionResult.details || 'Please provide your write-in text along with the option number (e.g. "4, your response").'
|
|
9691
|
+
);
|
|
9692
|
+
} else if (optionResult.reason === "write-in-already-active") {
|
|
9693
|
+
await this.emitPromptSafetyNotification(
|
|
9694
|
+
session,
|
|
9695
|
+
optionResult.details || "Write-in input field is already open on desktop; finish or cancel on desktop before selecting an option."
|
|
9696
|
+
);
|
|
9697
|
+
} else if (optionResult.reason === "follow-up-not-allowed") {
|
|
9698
|
+
await this.emitPromptSafetyNotification(
|
|
9699
|
+
session,
|
|
9700
|
+
optionResult.details || `Option ${intent.option} does not accept follow-up text.`
|
|
9701
|
+
);
|
|
9702
|
+
} else {
|
|
9703
|
+
const phase = optionResult.deliveryPhase ?? "none";
|
|
9704
|
+
let message;
|
|
9705
|
+
if (phase !== "none") {
|
|
9706
|
+
message = "Your answer was partially entered on desktop. Please complete or cancel the prompt directly on desktop.";
|
|
9707
|
+
} else if (optionResult.reason === "prompt-expired" || optionResult.reason === "prompt-superseded") {
|
|
9708
|
+
message = "The question prompt on desktop has expired or changed. Please review on desktop.";
|
|
9709
|
+
} else if (optionResult.reason === "prompt-probe-failed") {
|
|
9710
|
+
message = "Write-in input field was not ready on desktop. Please try again or respond on desktop.";
|
|
9711
|
+
} else if (optionResult.reason === "no-tmux-target" || optionResult.reason === "tmux-failed") {
|
|
9712
|
+
message = optionResult.details || "Desktop terminal session is unreachable. Please respond directly on desktop.";
|
|
9713
|
+
} else if (optionResult.reason === "display-only-question-active") {
|
|
9714
|
+
message = "This question requires desktop input. Please respond directly on desktop.";
|
|
9715
|
+
} else if (optionResult.reason === "invalid-option") {
|
|
9716
|
+
message = optionResult.details || `Option '${intent.option}' is not valid for the current question.`;
|
|
9717
|
+
} else {
|
|
9718
|
+
message = optionResult.details || `Failed to submit response to question prompt (${optionResult.reason}).`;
|
|
9719
|
+
}
|
|
9720
|
+
await this.emitPromptSafetyNotification(session, message);
|
|
9721
|
+
}
|
|
9722
|
+
if (!(0, import_codevibe_core7.requiresSyncInputDeliveryReceipt)(evt)) {
|
|
9723
|
+
const phase = optionResult.deliveryPhase ?? "none";
|
|
9724
|
+
if (phase === "none") {
|
|
9725
|
+
this.mobileDeduper.forget(session.sessionId, intent.option);
|
|
9726
|
+
if (armedWriteInEcho && intent.followUpText) {
|
|
9727
|
+
this.mobileDeduper.forget(session.sessionId, intent.followUpText);
|
|
9728
|
+
}
|
|
9729
|
+
} else if (phase === "option_sent" && armedWriteInEcho && intent.followUpText) {
|
|
9730
|
+
this.mobileDeduper.forget(session.sessionId, intent.followUpText);
|
|
9731
|
+
}
|
|
9732
|
+
}
|
|
9733
|
+
return;
|
|
9734
|
+
}
|
|
9735
|
+
await this.resolveE1PluginApproval(session.sessionId, activePrompt.promptId);
|
|
9736
|
+
await this.markExecuted(evt);
|
|
9381
9737
|
return;
|
|
9382
|
-
}
|
|
9383
|
-
|
|
9384
|
-
|
|
9738
|
+
} else {
|
|
9739
|
+
const optionResult = await this.promptResponder.sendApprovalReply(
|
|
9740
|
+
activePrompt.promptId,
|
|
9741
|
+
intent.option
|
|
9742
|
+
);
|
|
9743
|
+
if (!optionResult.ok) {
|
|
9744
|
+
if (!(0, import_codevibe_core7.requiresSyncInputDeliveryReceipt)(evt)) {
|
|
9745
|
+
const phase = optionResult.deliveryPhase ?? "none";
|
|
9746
|
+
if (phase === "none") {
|
|
9747
|
+
this.mobileDeduper.forget(session.sessionId, intent.option);
|
|
9748
|
+
}
|
|
9749
|
+
}
|
|
9750
|
+
return;
|
|
9751
|
+
}
|
|
9752
|
+
await this.resolveE1PluginApproval(session.sessionId, activePrompt.promptId);
|
|
9753
|
+
if (intent.followUpText) {
|
|
9754
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
9755
|
+
const freeFormResult = await this.promptResponder.sendFreeFormWhenNoApprovalActive(
|
|
9756
|
+
intent.followUpText,
|
|
9757
|
+
() => this.trackDurableMobileEcho(session, evt, intent.followUpText, "free_form")
|
|
9758
|
+
);
|
|
9759
|
+
if (!freeFormResult.ok) {
|
|
9760
|
+
if (!(0, import_codevibe_core7.requiresSyncInputDeliveryReceipt)(evt)) {
|
|
9761
|
+
this.mobileDeduper.forget(session.sessionId, intent.followUpText);
|
|
9762
|
+
}
|
|
9763
|
+
logger.warn("Follow-up text send failed for approval", { reason: freeFormResult.reason });
|
|
9764
|
+
return;
|
|
9765
|
+
}
|
|
9766
|
+
}
|
|
9385
9767
|
await this.markExecuted(evt);
|
|
9386
9768
|
return;
|
|
9387
9769
|
}
|
|
9388
|
-
promptContent = intent.followUpText;
|
|
9389
|
-
await new Promise((resolve2) => setTimeout(resolve2, 250));
|
|
9390
9770
|
} else {
|
|
9771
|
+
const activeQuestionPrompt = pendingPrompts.find((p) => p.kind === "question");
|
|
9772
|
+
if (activeQuestionPrompt) {
|
|
9773
|
+
const isDisplayOnly = Object.keys(activeQuestionPrompt.submitMap || {}).length === 0;
|
|
9774
|
+
logger.warn("Non-option input received while an interactive question prompt is active; rejecting dismissal", {
|
|
9775
|
+
sessionId: session.sessionId,
|
|
9776
|
+
promptId: activeQuestionPrompt.promptId,
|
|
9777
|
+
isDisplayOnly,
|
|
9778
|
+
content: promptContent.slice(0, 64)
|
|
9779
|
+
});
|
|
9780
|
+
const message = isDisplayOnly ? "The active terminal question prompt is display-only on mobile. Please select an option directly in your terminal." : "An interactive question prompt is active in your terminal. Please select one of the advertised options or answer directly in your terminal.";
|
|
9781
|
+
await this.emitPromptSafetyNotification(session, message);
|
|
9782
|
+
return;
|
|
9783
|
+
}
|
|
9391
9784
|
const hasDisplayOnlyPrompt = pendingPrompts.some(
|
|
9392
9785
|
(p) => Object.keys(p.submitMap || {}).length === 0
|
|
9393
9786
|
);
|
|
@@ -9596,6 +9989,7 @@ var McpServer = class _McpServer {
|
|
|
9596
9989
|
const state = await this.tmuxSessionState(tmuxTarget);
|
|
9597
9990
|
if (state === "alive") {
|
|
9598
9991
|
this.tmuxLifecycleObservedAlive = true;
|
|
9992
|
+
this.refreshNativeLogOwnership();
|
|
9599
9993
|
return;
|
|
9600
9994
|
}
|
|
9601
9995
|
const ownershipEstablished = this.tmuxLifecycleObservedAlive || Date.now() >= this.tmuxLifecycleInitDeadlineMs;
|
|
@@ -9923,6 +10317,7 @@ async function main() {
|
|
|
9923
10317
|
bearerToken,
|
|
9924
10318
|
preferredPort: args.port,
|
|
9925
10319
|
runtimeDir: args.runtimeDir,
|
|
10320
|
+
nativeCliLogPath: args.runtimeDir ? path8.join(args.runtimeDir, "native-cli.log") : void 0,
|
|
9926
10321
|
tmuxTarget: process.env.CODEVIBE_AGY_TMUX_TARGET ?? void 0,
|
|
9927
10322
|
wrapperPid: process.env.CODEVIBE_AGY_WRAPPER_PID ? parseInt(process.env.CODEVIBE_AGY_WRAPPER_PID, 10) : void 0
|
|
9928
10323
|
});
|
|
@@ -10057,6 +10452,7 @@ var __testing = {
|
|
|
10057
10452
|
0 && (module.exports = {
|
|
10058
10453
|
ApprovalDetector,
|
|
10059
10454
|
McpServer,
|
|
10455
|
+
PromptResponder,
|
|
10060
10456
|
SessionNotFoundError,
|
|
10061
10457
|
StoredAuthFailureError,
|
|
10062
10458
|
TERMINAL_SHUTDOWN_SIGNALS,
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
# Companion limitations:
|
|
51
51
|
# - Requires tmux. Print mode (`--print` / `-p`) is NOT supported.
|
|
52
52
|
# - Requires Node.js 22 or newer for the MCP server.
|
|
53
|
+
# - Requires agy 1.4.3+ advertising --log-file for launch ownership.
|
|
53
54
|
#
|
|
54
55
|
|
|
55
56
|
set -e
|
|
@@ -182,6 +183,37 @@ case "${1:-}" in
|
|
|
182
183
|
;;
|
|
183
184
|
esac
|
|
184
185
|
|
|
186
|
+
# Companion owns the log used as native transcript ownership proof. Do not
|
|
187
|
+
# silently override a user's log or let a second flag redirect that proof.
|
|
188
|
+
for _CV_ARG in "$@"; do
|
|
189
|
+
case "$_CV_ARG" in
|
|
190
|
+
--log-file|--log-file=*|-log-file|-log-file=*)
|
|
191
|
+
echo 'Error: CodeVibe Companion owns --log-file for session isolation. Run agy directly to use a custom log.' >&2
|
|
192
|
+
exit 2
|
|
193
|
+
;;
|
|
194
|
+
esac
|
|
195
|
+
done
|
|
196
|
+
unset _CV_ARG
|
|
197
|
+
|
|
198
|
+
# Require the tested native contract before installer, runtime, or session work.
|
|
199
|
+
# Metadata passthrough above and direct `agy` use remain unchanged.
|
|
200
|
+
require_native_sync_support() {
|
|
201
|
+
local version="$1" binary="$2" help major minor patch
|
|
202
|
+
if [[ "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
|
203
|
+
major=$((10#${BASH_REMATCH[1]}))
|
|
204
|
+
minor=$((10#${BASH_REMATCH[2]}))
|
|
205
|
+
patch=$((10#${BASH_REMATCH[3]}))
|
|
206
|
+
if (( major > 1 || (major == 1 && (minor > 4 || (minor == 4 && patch >= 3))) )); then
|
|
207
|
+
if help="$("$binary" --help 2>&1)" &&
|
|
208
|
+
[[ "$help" =~ (^|[[:space:]])--log-file([=[:space:]]|$) ]]; then
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
echo 'Error: CodeVibe Companion requires Antigravity CLI 1.4.3+ with --log-file support. Update agy, then retry. Direct agy use is unchanged.' >&2
|
|
214
|
+
return 1
|
|
215
|
+
}
|
|
216
|
+
|
|
185
217
|
# ─── Environment probes ───────────────────────────────────────────────
|
|
186
218
|
_CV_NODE_VER="missing"
|
|
187
219
|
command -v node >/dev/null 2>&1 && _CV_NODE_VER="$(node -v 2>/dev/null | cv_sanitize)"
|
|
@@ -191,7 +223,7 @@ command -v tmux >/dev/null 2>&1 && _CV_TMUX_VER="$(tmux -V 2>/dev/null | cv_sani
|
|
|
191
223
|
[ -z "$_CV_TMUX_VER" ] && _CV_TMUX_VER="unknown"
|
|
192
224
|
_CV_AGY_BIN="$(resolve_agy_bin)"
|
|
193
225
|
_CV_AGY_VER="missing"
|
|
194
|
-
[ -n "$_CV_AGY_BIN" ] && _CV_AGY_VER="$("$_CV_AGY_BIN" --version 2>/dev/null
|
|
226
|
+
[ -n "$_CV_AGY_BIN" ] && _CV_AGY_VER="$(cv_sanitize "$("$_CV_AGY_BIN" --version 2>/dev/null)")"
|
|
195
227
|
[ -z "$_CV_AGY_VER" ] && _CV_AGY_VER="unknown"
|
|
196
228
|
_CV_OS_VER="$(uname -s | cv_sanitize)"
|
|
197
229
|
[ -z "$_CV_OS_VER" ] && _CV_OS_VER="unknown"
|
|
@@ -262,6 +294,11 @@ if [ -z "$_CV_AGY_BIN" ] || ! [ -f "$_CV_AGY_BIN" ] || ! [ -x "$_CV_AGY_BIN" ];
|
|
|
262
294
|
exit 1
|
|
263
295
|
fi
|
|
264
296
|
|
|
297
|
+
if ! require_native_sync_support "$_CV_AGY_VER" "$_CV_AGY_BIN"; then
|
|
298
|
+
cv_failed "agy_sync_contract_unsupported"
|
|
299
|
+
exit 2
|
|
300
|
+
fi
|
|
301
|
+
|
|
265
302
|
if ! command -v node &> /dev/null; then
|
|
266
303
|
echo "Error: Node.js is required but not installed."
|
|
267
304
|
echo "Install Node.js 22+ from: https://nodejs.org"
|
|
@@ -348,6 +385,14 @@ rm -f "$RUNTIME_DIR/conn.json" "$RUNTIME_DIR/claims.json"
|
|
|
348
385
|
# Also tighten the runtime base — best-effort, no fatal on failure.
|
|
349
386
|
chmod 0700 "$RUNTIME_BASE" 2>/dev/null || true
|
|
350
387
|
|
|
388
|
+
# The native process and daemon share only this launch's fresh regular log.
|
|
389
|
+
# Refuse a stale PID-reuse file rather than trusting its conversation identity.
|
|
390
|
+
NATIVE_CLI_LOG="$RUNTIME_DIR/native-cli.log"
|
|
391
|
+
if ! (umask 077; set -o noclobber; : > "$NATIVE_CLI_LOG"); then
|
|
392
|
+
echo 'Error: this Companion runtime already contains a native log; refusing ambiguous ownership.' >&2
|
|
393
|
+
exit 1
|
|
394
|
+
fi
|
|
395
|
+
|
|
351
396
|
log "Wrapper PID=$WRAPPER_PID; token=$(printf '%s' "$BEARER_TOKEN" | cut -c1-8)...; port=$MCP_PORT; runtime=$RUNTIME_DIR"
|
|
352
397
|
|
|
353
398
|
# Export env vars consumed by the MCP server + any in-tree tooling.
|
|
@@ -481,7 +526,8 @@ escaped_exit_file="$(escape_sq "$_CV_AGY_EXIT_FILE")"
|
|
|
481
526
|
escaped_conn_file="$(escape_sq "$RUNTIME_DIR/conn.json")"
|
|
482
527
|
escaped_mcp_log="$(escape_sq "$MCP_LOG_FILE")"
|
|
483
528
|
escaped_node_bin="$(escape_sq "$(command -v node)")"
|
|
484
|
-
|
|
529
|
+
escaped_native_log="$(escape_sq "$NATIVE_CLI_LOG")"
|
|
530
|
+
AGY_CMD="'$escaped_bin' --log-file '$escaped_native_log'"
|
|
485
531
|
for arg in "$@"; do
|
|
486
532
|
escaped_arg="$(escape_sq "$arg")"
|
|
487
533
|
AGY_CMD="$AGY_CMD '$escaped_arg'"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantiya/codevibe-antigravity-plugin",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.87",
|
|
4
4
|
"description": "Control Antigravity CLI from your iPhone and Android — real-time sync, approve file edits, send prompts by voice. Part of CodeVibe.",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"codevibe": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"node": ">=22.0.0"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@quantiya/codevibe-core": "^2.0.
|
|
49
|
+
"@quantiya/codevibe-core": "^2.0.76",
|
|
50
50
|
"chokidar": "^5.0.0",
|
|
51
51
|
"dotenv": "^16.6.1",
|
|
52
52
|
"express": "^5.1.0",
|