@quantiya/codevibe-antigravity-plugin 2.0.5 → 2.0.7
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/.env.example +7 -6
- package/README.md +18 -22
- package/dist/server.js +185 -41
- package/{bin/codevibe-agy → libexec/companion-launcher} +51 -56
- package/package.json +6 -6
package/.env.example
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CodeVibe Antigravity Companion Plugin Configuration
|
|
2
2
|
# ======================================
|
|
3
3
|
# NOTE: You typically don't need to edit this file manually.
|
|
4
|
-
# Configuration is auto-set after running 'codevibe
|
|
4
|
+
# Configuration is auto-set after running 'codevibe login'.
|
|
5
5
|
#
|
|
6
6
|
# Copy this file to .env.production or .env.development and fill in values.
|
|
7
7
|
|
|
@@ -19,10 +19,11 @@ COGNITO_USER_POOL_ID=us-east-1_xxxxxxxxx
|
|
|
19
19
|
COGNITO_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
20
20
|
COGNITO_DOMAIN=your-domain.auth.us-east-1.amazoncognito.com
|
|
21
21
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
# Antigravity CLI Configuration
|
|
23
|
+
# Optional absolute path to an executable agy binary. Invalid values fall back
|
|
24
|
+
# to an executable agy found on PATH.
|
|
25
|
+
# AGY_PATH=/absolute/path/to/agy
|
|
25
26
|
|
|
26
27
|
# Logging Configuration
|
|
27
|
-
LOG_FILE=/tmp/codevibe-
|
|
28
|
+
LOG_FILE=/tmp/codevibe-agy-mcp.log
|
|
28
29
|
LOG_LEVEL=info
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Control Google Antigravity CLI from your iPhone and Android.** See your agent's work, approve file edits, dictate prompts by voice — all from your phone, anywhere you are.
|
|
4
4
|
|
|
5
|
-
🌐 **[quantiya.ai/codevibe](https://quantiya.ai/codevibe)** — landing page, demo video, and
|
|
5
|
+
🌐 **[quantiya.ai/codevibe](https://quantiya.ai/codevibe)** — landing page, demo video, and stable 1.x installer
|
|
6
6
|
|
|
7
7
|
📱 **[Download on the App Store](https://apps.apple.com/app/id6756500217)** · **[Get it on Google Play](https://play.google.com/store/apps/details?id=ai.quantiya.app.codevibe)**
|
|
8
8
|
|
|
@@ -19,38 +19,35 @@
|
|
|
19
19
|
- **🔔 Push notifications** — get notified when your agent needs input
|
|
20
20
|
- **🔒 End-to-end encrypted** — AES-256-GCM with ECDH key exchange
|
|
21
21
|
- **🔓 Locked screen support** — works even when your computer screen is locked (via tmux)
|
|
22
|
-
- **🧠 Multi-agent ready** — Antigravity sessions appear alongside Claude
|
|
22
|
+
- **🧠 Multi-agent ready** — Antigravity sessions appear alongside Claude and Codex sessions in the same app
|
|
23
23
|
|
|
24
|
-
## Install
|
|
24
|
+
## Install the 2.x candidate
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Download the **[iOS app](https://apps.apple.com/app/id6756500217)** or **[Android app](https://play.google.com/store/apps/details?id=ai.quantiya.app.codevibe)**, sign in with the same Apple or Google account, and run:
|
|
31
|
-
|
|
32
|
-
```bash
|
|
33
|
-
codevibe-agy
|
|
27
|
+
npm install -g @quantiya/codevibe@next
|
|
28
|
+
codevibe login
|
|
29
|
+
codevibe --agent agy
|
|
34
30
|
```
|
|
35
31
|
|
|
36
|
-
Your session appears on your phone automatically.
|
|
32
|
+
Install Node.js 22+, tmux, and Antigravity CLI first. Then download the **[iOS app](https://apps.apple.com/app/id6756500217)** or **[Android app](https://play.google.com/store/apps/details?id=ai.quantiya.app.codevibe)** and sign in with the same Apple or Google account. Your session appears on your phone automatically.
|
|
37
33
|
|
|
38
|
-
###
|
|
34
|
+
### Stable 1.x installer
|
|
39
35
|
|
|
40
36
|
```bash
|
|
41
|
-
|
|
42
|
-
codevibe login
|
|
43
|
-
codevibe-agy
|
|
37
|
+
curl -fsSL https://quantiya.ai/codevibe/install.sh | bash
|
|
44
38
|
```
|
|
45
39
|
|
|
40
|
+
The hosted installer remains on stable 1.x while 2.x is validated. It does not
|
|
41
|
+
install the unified `codevibe --agent` interface yet.
|
|
42
|
+
|
|
46
43
|
## Requirements
|
|
47
44
|
|
|
48
45
|
- **macOS, Linux, or WSL Ubuntu** — Windows without WSL is not supported
|
|
49
|
-
- **Node.js**
|
|
46
|
+
- **Node.js** 22.0.0+
|
|
50
47
|
- **tmux** — `brew install tmux` on macOS, `apt install tmux` on Linux/WSL
|
|
51
48
|
- **Antigravity CLI (`agy`)** — install from [Google's documentation](https://antigravity.google/docs)
|
|
52
49
|
|
|
53
|
-
> Antigravity CLI is distributed by Google as a binary; it is not on npm. After installing `agy`, run `codevibe
|
|
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.
|
|
54
51
|
|
|
55
52
|
## How it works
|
|
56
53
|
|
|
@@ -70,10 +67,10 @@ A single launch session covers the entire wrapper lifetime, so `/resume` command
|
|
|
70
67
|
## CLI commands
|
|
71
68
|
|
|
72
69
|
```bash
|
|
73
|
-
codevibe
|
|
74
|
-
codevibe
|
|
75
|
-
codevibe
|
|
76
|
-
codevibe
|
|
70
|
+
codevibe --agent agy # Start agy with mobile sync
|
|
71
|
+
codevibe login # Authenticate via browser OAuth
|
|
72
|
+
codevibe status # Check auth status
|
|
73
|
+
codevibe logout # Sign out
|
|
77
74
|
```
|
|
78
75
|
|
|
79
76
|
## Troubleshooting
|
|
@@ -104,7 +101,6 @@ codevibe update
|
|
|
104
101
|
- **[@quantiya/codevibe](https://www.npmjs.com/package/@quantiya/codevibe)** — meta-package (install this, not this plugin directly)
|
|
105
102
|
- **[@quantiya/codevibe-core](https://www.npmjs.com/package/@quantiya/codevibe-core)** — shared library used by all plugins
|
|
106
103
|
- **[@quantiya/codevibe-claude-plugin](https://www.npmjs.com/package/@quantiya/codevibe-claude-plugin)** — Claude Code support
|
|
107
|
-
- **[@quantiya/codevibe-gemini-plugin](https://www.npmjs.com/package/@quantiya/codevibe-gemini-plugin)** — Gemini CLI support
|
|
108
104
|
- **[@quantiya/codevibe-codex-plugin](https://www.npmjs.com/package/@quantiya/codevibe-codex-plugin)** — Codex CLI support
|
|
109
105
|
|
|
110
106
|
## License
|
package/dist/server.js
CHANGED
|
@@ -378,7 +378,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
378
378
|
this.watcher.on("unlink", (filePath) => this.onFileUnlinked(filePath));
|
|
379
379
|
this.watcher.on("error", (error) => {
|
|
380
380
|
logger.error("Chokidar error", { error });
|
|
381
|
-
this.
|
|
381
|
+
this.emitError(error);
|
|
382
382
|
});
|
|
383
383
|
this.watcher.on("ready", () => {
|
|
384
384
|
logger.info("Transcript tailer ready (chokidar initial scan complete)");
|
|
@@ -388,6 +388,26 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
388
388
|
if (this.flushTimer.unref) this.flushTimer.unref();
|
|
389
389
|
this.isWatching = true;
|
|
390
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* (#638 G4) SAFE 'error' emission. `EventEmitter.emit('error', …)` with NO
|
|
393
|
+
* registered listener THROWS synchronously — inside a chokidar callback that
|
|
394
|
+
* becomes an uncaughtException, and inside the async read chain it becomes a
|
|
395
|
+
* rejection; either way the F6 symmetric process guard can classify it fatal
|
|
396
|
+
* and kill the daemon over a non-fatal tailing fault (over-death). The server
|
|
397
|
+
* attaches an 'error' listener before start() (primary path); this guard
|
|
398
|
+
* covers the residual windows — the doStop teardown detaches listeners
|
|
399
|
+
* BEFORE the tailer stops, and any embedder that never attached one.
|
|
400
|
+
*/
|
|
401
|
+
emitError(error) {
|
|
402
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
403
|
+
if (this.listenerCount("error") > 0) {
|
|
404
|
+
this.emit("error", err);
|
|
405
|
+
} else {
|
|
406
|
+
logger.error("Transcript tailer error (no listener attached \u2014 logged, non-fatal)", {
|
|
407
|
+
error: String(err)
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
391
411
|
/**
|
|
392
412
|
* Stop watching. Flushes offsets one last time before closing chokidar.
|
|
393
413
|
* Idempotent.
|
|
@@ -594,7 +614,13 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
594
614
|
return inflight;
|
|
595
615
|
}
|
|
596
616
|
const generation = this.readGeneration;
|
|
597
|
-
const read = this.drainReadQueue(filePath, conversationId, generation)
|
|
617
|
+
const read = this.drainReadQueue(filePath, conversationId, generation).catch((error) => {
|
|
618
|
+
logger.error("Transcript read chain failed (non-fatal) \u2014 next change event retries", {
|
|
619
|
+
filePath,
|
|
620
|
+
conversationId,
|
|
621
|
+
error: String(error)
|
|
622
|
+
});
|
|
623
|
+
});
|
|
598
624
|
this.activeReads.set(filePath, read);
|
|
599
625
|
void read.finally(() => {
|
|
600
626
|
if (this.activeReads.get(filePath) === read) {
|
|
@@ -651,7 +677,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
651
677
|
});
|
|
652
678
|
} catch (error) {
|
|
653
679
|
logger.error("createReadStream failed", { filePath, error });
|
|
654
|
-
this.
|
|
680
|
+
this.emitError(error);
|
|
655
681
|
return;
|
|
656
682
|
}
|
|
657
683
|
let buffered = entry.carryover;
|
|
@@ -684,7 +710,7 @@ var TranscriptTailer = class extends import_events.EventEmitter {
|
|
|
684
710
|
}
|
|
685
711
|
} catch (error) {
|
|
686
712
|
logger.error("Error reading transcript stream", { filePath, error });
|
|
687
|
-
this.
|
|
713
|
+
this.emitError(error);
|
|
688
714
|
return;
|
|
689
715
|
}
|
|
690
716
|
if (generation !== this.readGeneration) return;
|
|
@@ -2813,6 +2839,16 @@ var McpServer = class {
|
|
|
2813
2839
|
* collapses v8's per-conv disabledConversations Set because there's
|
|
2814
2840
|
* only ONE backend session per wrapper. */
|
|
2815
2841
|
this.sessionDisabled = false;
|
|
2842
|
+
/** (#638 G2) Terminal fail-closed-by-absence marker: createLaunchSession hit
|
|
2843
|
+
* ENCRYPTED_SESSION_NO_KEY for this identity. Retrying cannot derive a key
|
|
2844
|
+
* this device does not have, so ensureLaunchSession must NOT re-bootstrap —
|
|
2845
|
+
* the no-session/no-plaintext outcome is the correct terminal state. Logged
|
|
2846
|
+
* once at the catch; reset on doStop() alongside sessionDisabled. */
|
|
2847
|
+
this.launchKeyUnavailable = false;
|
|
2848
|
+
/** (#638 G2) Single-flight guard for ensureLaunchSession(): concurrent
|
|
2849
|
+
* activity-triggered re-bootstraps share ONE createLaunchSession attempt
|
|
2850
|
+
* instead of double-creating backend rows. Null when no attempt in flight. */
|
|
2851
|
+
this.ensureLaunchInFlight = null;
|
|
2816
2852
|
/** Registry of agy conv UUIDs the wrapper has observed this lifecycle.
|
|
2817
2853
|
* Populated from handleConversationDiscovered + handleTranscriptEmit
|
|
2818
2854
|
* (after isMainConversation passes). Used by:
|
|
@@ -2850,6 +2886,15 @@ var McpServer = class {
|
|
|
2850
2886
|
* out mid-cleanup — separating the bounded "wait for create" from the
|
|
2851
2887
|
* always-awaited "mark INACTIVE". (#142 Stage 2 R1 iter-2 HIGH.) */
|
|
2852
2888
|
this.pendingLaunchSessionId = null;
|
|
2889
|
+
/** (#638 H4) Set by doStop() when a stop runs while a createLaunchSession may
|
|
2890
|
+
* still be in flight (the bounded Phase-0 settle can expire mid-create). The
|
|
2891
|
+
* create's post-await tail includes this in its bail check, so a create that
|
|
2892
|
+
* resolves AFTER doStop's settle bound still deactivates its just-created row
|
|
2893
|
+
* itself — the settlement obligation survives until the create CONCLUSIVELY
|
|
2894
|
+
* finishes, instead of doStop "completing" it with a missing-row INACTIVE
|
|
2895
|
+
* write the backend rejected. Reset on the next start() (a stale prior-
|
|
2896
|
+
* lifecycle create is additionally caught by the gen mismatch). */
|
|
2897
|
+
this.stopRequestedDuringCreate = false;
|
|
2853
2898
|
/** Listener references kept so doStop() can detach them. Without this,
|
|
2854
2899
|
* a server restart (test mode or future hot-reload) accumulates
|
|
2855
2900
|
* listeners on the shared module instances. (Stage 2 HIGH finding
|
|
@@ -2907,11 +2952,12 @@ var McpServer = class {
|
|
|
2907
2952
|
error_message: "no stored OAuth tokens"
|
|
2908
2953
|
});
|
|
2909
2954
|
throw new Error(
|
|
2910
|
-
"codevibe-antigravity-plugin: no OAuth tokens \u2014 run `codevibe
|
|
2955
|
+
"codevibe-antigravity-plugin: no OAuth tokens \u2014 run `codevibe login` first"
|
|
2911
2956
|
);
|
|
2912
2957
|
}
|
|
2913
2958
|
this.started = true;
|
|
2914
2959
|
this.lifecycleGen++;
|
|
2960
|
+
this.stopRequestedDuringCreate = false;
|
|
2915
2961
|
this.registerSignalHandlers();
|
|
2916
2962
|
await (0, import_codevibe_core4.registerDeviceEncryptionKey)(this.appSyncClient, logger);
|
|
2917
2963
|
if (!this.started) return { httpPort: 0 };
|
|
@@ -2946,6 +2992,9 @@ var McpServer = class {
|
|
|
2946
2992
|
if (!this.started) {
|
|
2947
2993
|
return { httpPort: 0 };
|
|
2948
2994
|
}
|
|
2995
|
+
this.addListener(this.transcriptTailer, "error", (err) => {
|
|
2996
|
+
logger.error("transcript tailer error (non-fatal)", { error: String(err) });
|
|
2997
|
+
});
|
|
2949
2998
|
this.addListener(this.transcriptTailer, "event", (emit) => {
|
|
2950
2999
|
void this.handleTranscriptEmit(emit).catch((err) => {
|
|
2951
3000
|
logger.error("handleTranscriptEmit failed", { error: String(err) });
|
|
@@ -3008,8 +3057,16 @@ var McpServer = class {
|
|
|
3008
3057
|
}
|
|
3009
3058
|
if (this.pendingLaunchSessionId) {
|
|
3010
3059
|
const pendingId = this.pendingLaunchSessionId;
|
|
3011
|
-
this.
|
|
3012
|
-
await this.deactivateLaunchRow(pendingId);
|
|
3060
|
+
this.stopRequestedDuringCreate = true;
|
|
3061
|
+
const deactivated = await this.deactivateLaunchRow(pendingId);
|
|
3062
|
+
if (deactivated) {
|
|
3063
|
+
if (this.pendingLaunchSessionId === pendingId) this.pendingLaunchSessionId = null;
|
|
3064
|
+
} else {
|
|
3065
|
+
logger.error(
|
|
3066
|
+
"doStop: INACTIVE write for the pending launch row FAILED (row may not exist yet \u2014 create still in flight). Keeping the settlement obligation: if the create resolves in-process its tail deactivates the row; if the process exits first, an ACTIVE row may leak (SIGKILL-class residual) (#638 H4)",
|
|
3067
|
+
{ sessionId: pendingId }
|
|
3068
|
+
);
|
|
3069
|
+
}
|
|
3013
3070
|
}
|
|
3014
3071
|
if (this.subscription) {
|
|
3015
3072
|
try {
|
|
@@ -3070,6 +3127,8 @@ var McpServer = class {
|
|
|
3070
3127
|
}
|
|
3071
3128
|
this.session = null;
|
|
3072
3129
|
this.sessionDisabled = false;
|
|
3130
|
+
this.launchKeyUnavailable = false;
|
|
3131
|
+
this.ensureLaunchInFlight = null;
|
|
3073
3132
|
this.observedMainConversationIds.clear();
|
|
3074
3133
|
await fireDaemonBeacon("daemon_init_step", {
|
|
3075
3134
|
step: "shutdown",
|
|
@@ -3079,6 +3138,7 @@ var McpServer = class {
|
|
|
3079
3138
|
async handleConversationDiscovered(conversationId) {
|
|
3080
3139
|
if (!this.started) return;
|
|
3081
3140
|
if (this.sessionDisabled) return;
|
|
3141
|
+
await this.ensureLaunchSession();
|
|
3082
3142
|
if (!this.session) return;
|
|
3083
3143
|
if (!this.isMainConversation(conversationId)) {
|
|
3084
3144
|
logger.debug("Ignoring discovery for non-main conversation (subagent)", { conversationId });
|
|
@@ -3098,17 +3158,25 @@ var McpServer = class {
|
|
|
3098
3158
|
* can't clobber or resurrect ACTIVE. (Separate from the ACTIVE-vs-INACTIVE
|
|
3099
3159
|
* per-session ordering that core 1.0.29's status-write chain handles.)
|
|
3100
3160
|
* (#142 Stage 2 R1 iter-2.)
|
|
3161
|
+
*
|
|
3162
|
+
* (#638 H4) Returns whether the INACTIVE write actually SUCCEEDED. A swallowed
|
|
3163
|
+
* failure here is NOT a settled obligation — the row may not exist yet (a
|
|
3164
|
+
* create still in flight) or the network may be down — so callers must keep
|
|
3165
|
+
* their settlement marker/obligation alive on `false` instead of treating the
|
|
3166
|
+
* attempt as completion. The log stays inside so no failure is ever silent.
|
|
3101
3167
|
*/
|
|
3102
3168
|
async deactivateLaunchRow(sessionId) {
|
|
3103
3169
|
try {
|
|
3104
3170
|
this.appSyncClient.stopHeartbeat(sessionId);
|
|
3105
3171
|
await this.appSyncClient.updateSession({ sessionId, status: "INACTIVE" });
|
|
3106
3172
|
logger.info("Marked superseded/interrupted launch session INACTIVE", { sessionId });
|
|
3173
|
+
return true;
|
|
3107
3174
|
} catch (err) {
|
|
3108
3175
|
logger.warn("Failed to mark superseded/interrupted launch session INACTIVE", {
|
|
3109
3176
|
sessionId,
|
|
3110
3177
|
error: String(err)
|
|
3111
3178
|
});
|
|
3179
|
+
return false;
|
|
3112
3180
|
}
|
|
3113
3181
|
}
|
|
3114
3182
|
/**
|
|
@@ -3143,7 +3211,7 @@ var McpServer = class {
|
|
|
3143
3211
|
);
|
|
3144
3212
|
sessionKey = result.sessionKey ?? null;
|
|
3145
3213
|
} catch (err) {
|
|
3146
|
-
this.pendingLaunchSessionId = null;
|
|
3214
|
+
if (this.pendingLaunchSessionId === sessionId) this.pendingLaunchSessionId = null;
|
|
3147
3215
|
const msg = String(err);
|
|
3148
3216
|
if (msg.includes("session-limit-exceeded")) {
|
|
3149
3217
|
logger.warn("Free-tier session limit reached \u2014 mobile sync disabled for this wrapper", { sessionId });
|
|
@@ -3151,12 +3219,25 @@ var McpServer = class {
|
|
|
3151
3219
|
this.sessionDisabled = true;
|
|
3152
3220
|
return;
|
|
3153
3221
|
}
|
|
3154
|
-
|
|
3222
|
+
if (err?.code === "ENCRYPTED_SESSION_NO_KEY") {
|
|
3223
|
+
logger.error("createLaunchSession: ENCRYPTED_SESSION_NO_KEY \u2014 no key for the encrypted session; no session created, mobile sync fails closed (no plaintext) (#638)", { sessionId });
|
|
3224
|
+
if (gen !== this.lifecycleGen || !this.started) return;
|
|
3225
|
+
this.launchKeyUnavailable = true;
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
logger.error("createLaunchSession failed (non-fatal; next activity re-attempts via ensureLaunchSession \u2014 #638 G2)", { sessionId, error: msg });
|
|
3155
3229
|
return;
|
|
3156
3230
|
}
|
|
3157
|
-
if (gen !== this.lifecycleGen || !this.started) {
|
|
3158
|
-
await this.deactivateLaunchRow(sessionId);
|
|
3159
|
-
if (
|
|
3231
|
+
if (gen !== this.lifecycleGen || !this.started || this.stopRequestedDuringCreate) {
|
|
3232
|
+
const deactivated = await this.deactivateLaunchRow(sessionId);
|
|
3233
|
+
if (deactivated) {
|
|
3234
|
+
if (this.pendingLaunchSessionId === sessionId) this.pendingLaunchSessionId = null;
|
|
3235
|
+
} else {
|
|
3236
|
+
logger.error(
|
|
3237
|
+
"createLaunchSession bail: INACTIVE write for the just-created row FAILED \u2014 settlement obligation remains open (marker kept); the row may leak ACTIVE if nothing retries before exit (#638 H4)",
|
|
3238
|
+
{ sessionId }
|
|
3239
|
+
);
|
|
3240
|
+
}
|
|
3160
3241
|
return;
|
|
3161
3242
|
}
|
|
3162
3243
|
this.session = {
|
|
@@ -3169,7 +3250,14 @@ var McpServer = class {
|
|
|
3169
3250
|
createdAt: /* @__PURE__ */ new Date(),
|
|
3170
3251
|
subscriptionActive: false,
|
|
3171
3252
|
metadata: { wrapperPid: this.wrapperPid ?? void 0, launch: true },
|
|
3172
|
-
sessionKey
|
|
3253
|
+
sessionKey,
|
|
3254
|
+
// (#638) A null key here means a genuinely unencrypted/legacy session
|
|
3255
|
+
// (resumeOrCreateSession THROWS ENCRYPTED_SESSION_NO_KEY for an encrypted
|
|
3256
|
+
// session this device can't key, handled in the catch above — no session
|
|
3257
|
+
// is created there). So `!!sessionKey` correctly classifies this row; if a
|
|
3258
|
+
// later path ever nulls the key on an encrypted session, encryptOutbound
|
|
3259
|
+
// fails closed instead of leaking plaintext.
|
|
3260
|
+
sessionIsEncrypted: !!sessionKey
|
|
3173
3261
|
};
|
|
3174
3262
|
this.pendingLaunchSessionId = null;
|
|
3175
3263
|
try {
|
|
@@ -3206,6 +3294,43 @@ var McpServer = class {
|
|
|
3206
3294
|
});
|
|
3207
3295
|
logger.info("Launch session created", { sessionId, userId });
|
|
3208
3296
|
}
|
|
3297
|
+
/**
|
|
3298
|
+
* (#638 G2) Single-flight re-bootstrap of the launch session on first
|
|
3299
|
+
* subsequent activity. createLaunchSession() runs once from start(); before
|
|
3300
|
+
* this fix a transient failure there (slow/absent network at daemon start —
|
|
3301
|
+
* the incident) left `this.session = null` forever: every emit site guards
|
|
3302
|
+
* `if (!this.session) return` with no re-bootstrap trigger, so the daemon
|
|
3303
|
+
* lived but never synced (permanently headless).
|
|
3304
|
+
*
|
|
3305
|
+
* Called from the emit-relevant entry points (transcript event handling /
|
|
3306
|
+
* prompt handling / mobile-event handling) BEFORE their `!this.session`
|
|
3307
|
+
* guards. Semantics preserved:
|
|
3308
|
+
* - session present → returns immediately (fast path, no work);
|
|
3309
|
+
* - session-limit-exceeded → sessionDisabled stays TERMINAL (no retry);
|
|
3310
|
+
* - ENCRYPTED_SESSION_NO_KEY → launchKeyUnavailable stays TERMINAL for this
|
|
3311
|
+
* identity (fail-closed by absence, logged once);
|
|
3312
|
+
* - ONLY generic/transient errors are retried, single-flight per activity
|
|
3313
|
+
* arrival (inherently rate-bounded — one attempt per incoming event, and
|
|
3314
|
+
* concurrent arrivals share one attempt);
|
|
3315
|
+
* - lifecycleGen/started guards are honored by createLaunchSession itself
|
|
3316
|
+
* (gen captured at entry, rechecked after every await).
|
|
3317
|
+
*/
|
|
3318
|
+
async ensureLaunchSession() {
|
|
3319
|
+
if (this.session) return;
|
|
3320
|
+
if (!this.started || this.sessionDisabled || this.launchKeyUnavailable) return;
|
|
3321
|
+
if (this.ensureLaunchInFlight) {
|
|
3322
|
+
await this.ensureLaunchInFlight;
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
const attempt = this.createLaunchSession();
|
|
3326
|
+
this.ensureLaunchInFlight = attempt;
|
|
3327
|
+
this.launchSessionPromise = attempt;
|
|
3328
|
+
try {
|
|
3329
|
+
await attempt;
|
|
3330
|
+
} finally {
|
|
3331
|
+
if (this.ensureLaunchInFlight === attempt) this.ensureLaunchInFlight = null;
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3209
3334
|
isMainConversation(conversationId) {
|
|
3210
3335
|
if (process.env.JEST_WORKER_ID && !this.cliLogPath) {
|
|
3211
3336
|
return true;
|
|
@@ -3224,6 +3349,7 @@ var McpServer = class {
|
|
|
3224
3349
|
if (this.sessionDisabled) {
|
|
3225
3350
|
return;
|
|
3226
3351
|
}
|
|
3352
|
+
await this.ensureLaunchSession();
|
|
3227
3353
|
if (!this.session) {
|
|
3228
3354
|
return;
|
|
3229
3355
|
}
|
|
@@ -3271,7 +3397,9 @@ var McpServer = class {
|
|
|
3271
3397
|
}
|
|
3272
3398
|
}
|
|
3273
3399
|
try {
|
|
3274
|
-
|
|
3400
|
+
const outbound = this.encryptOutbound(session, input);
|
|
3401
|
+
if (!outbound) continue;
|
|
3402
|
+
await this.appSyncClient.createEvent(outbound);
|
|
3275
3403
|
} catch (err) {
|
|
3276
3404
|
logger.error("createEvent failed", {
|
|
3277
3405
|
sessionId: session.sessionId,
|
|
@@ -3282,37 +3410,39 @@ var McpServer = class {
|
|
|
3282
3410
|
}
|
|
3283
3411
|
}
|
|
3284
3412
|
/**
|
|
3285
|
-
*
|
|
3286
|
-
*
|
|
3287
|
-
*
|
|
3288
|
-
* (
|
|
3413
|
+
* (#638) THE single encrypt-or-fail-closed decision for EVERY mobile-visible
|
|
3414
|
+
* emit, delegated to @quantiya/codevibe-core's encryptForEmit so all three
|
|
3415
|
+
* companion agents share ONE chokepoint. Returns a new CreateEventInput
|
|
3416
|
+
* (never mutates the input), or NULL when the session IS encrypted but has no
|
|
3417
|
+
* derivable key — the caller MUST DROP the event (fail-closed). Previously
|
|
3418
|
+
* this method returned `input` unchanged for ANY keyless session, leaking
|
|
3419
|
+
* cleartext for an encrypted-but-keyless session; the `sessionIsEncrypted`
|
|
3420
|
+
* flag now distinguishes a genuinely unencrypted session (plaintext OK) from
|
|
3421
|
+
* an encrypted keyless one (drop).
|
|
3289
3422
|
*/
|
|
3290
3423
|
encryptOutbound(session, input) {
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
);
|
|
3300
|
-
encryptedMetadata = { encrypted: encryptedMetaStr };
|
|
3301
|
-
}
|
|
3302
|
-
return {
|
|
3303
|
-
...input,
|
|
3304
|
-
content: encryptedContent,
|
|
3305
|
-
metadata: encryptedMetadata,
|
|
3306
|
-
isEncrypted: true
|
|
3307
|
-
};
|
|
3308
|
-
} catch (err) {
|
|
3309
|
-
logger.error("Outbound encryption failed \u2014 DROPPING event to avoid plaintext leak", {
|
|
3424
|
+
const enc = (0, import_codevibe_core4.encryptForEmit)(
|
|
3425
|
+
session.sessionKey ?? null,
|
|
3426
|
+
session.sessionIsEncrypted ?? false,
|
|
3427
|
+
input.content,
|
|
3428
|
+
input.metadata
|
|
3429
|
+
);
|
|
3430
|
+
if (enc === null) {
|
|
3431
|
+
logger.error("No session key for ENCRYPTED session \u2014 DROPPING event to avoid plaintext leak (#638)", {
|
|
3310
3432
|
sessionId: session.sessionId,
|
|
3311
|
-
type: input.type
|
|
3312
|
-
error: String(err)
|
|
3433
|
+
type: input.type
|
|
3313
3434
|
});
|
|
3314
|
-
|
|
3435
|
+
return null;
|
|
3436
|
+
}
|
|
3437
|
+
if (!enc.isEncrypted) {
|
|
3438
|
+
return input;
|
|
3315
3439
|
}
|
|
3440
|
+
return {
|
|
3441
|
+
...input,
|
|
3442
|
+
content: enc.content,
|
|
3443
|
+
metadata: enc.metadata,
|
|
3444
|
+
isEncrypted: true
|
|
3445
|
+
};
|
|
3316
3446
|
}
|
|
3317
3447
|
/**
|
|
3318
3448
|
* If the inbound mobile event is marked isEncrypted, decrypt content +
|
|
@@ -3357,6 +3487,7 @@ var McpServer = class {
|
|
|
3357
3487
|
async handlePromptCandidate(cand) {
|
|
3358
3488
|
if (!this.started) return;
|
|
3359
3489
|
if (this.sessionDisabled) return;
|
|
3490
|
+
await this.ensureLaunchSession();
|
|
3360
3491
|
if (!this.session) return;
|
|
3361
3492
|
const parsed = parseApprovalSnapshot(cand.snapshot);
|
|
3362
3493
|
if (!parsed) return;
|
|
@@ -3446,7 +3577,9 @@ var McpServer = class {
|
|
|
3446
3577
|
})
|
|
3447
3578
|
};
|
|
3448
3579
|
try {
|
|
3449
|
-
|
|
3580
|
+
const outbound = this.encryptOutbound(session, input);
|
|
3581
|
+
if (!outbound) return;
|
|
3582
|
+
await this.appSyncClient.createEvent(outbound);
|
|
3450
3583
|
} catch (err) {
|
|
3451
3584
|
logger.error("createEvent INTERACTIVE_PROMPT failed", {
|
|
3452
3585
|
promptId: state.promptId,
|
|
@@ -3542,6 +3675,7 @@ var McpServer = class {
|
|
|
3542
3675
|
async handleMobileEvent(evt) {
|
|
3543
3676
|
if (!this.started) return;
|
|
3544
3677
|
if (this.sessionDisabled) return;
|
|
3678
|
+
await this.ensureLaunchSession();
|
|
3545
3679
|
if (!this.session) return;
|
|
3546
3680
|
const session = this.session;
|
|
3547
3681
|
if (evt.source !== import_codevibe_core4.EventSource.MOBILE) return;
|
|
@@ -3705,6 +3839,11 @@ var McpServer = class {
|
|
|
3705
3839
|
timestamp: (0, import_codevibe_core4.prepareEventTimestamp)({ orderingKey: payload.sessionId })
|
|
3706
3840
|
};
|
|
3707
3841
|
const finalInput = this.encryptOutbound(session, input);
|
|
3842
|
+
if (!finalInput) {
|
|
3843
|
+
throw new Error(
|
|
3844
|
+
`fail-closed: encrypted session ${payload.sessionId} has no session key \u2014 event dropped (#638)`
|
|
3845
|
+
);
|
|
3846
|
+
}
|
|
3708
3847
|
const created = await this.appSyncClient.createEvent(finalInput);
|
|
3709
3848
|
return { eventId: created.eventId };
|
|
3710
3849
|
}
|
|
@@ -3846,6 +3985,11 @@ async function main() {
|
|
|
3846
3985
|
tmuxTarget: process.env.CODEVIBE_AGY_TMUX_TARGET ?? void 0,
|
|
3847
3986
|
wrapperPid: process.env.CODEVIBE_AGY_WRAPPER_PID ? parseInt(process.env.CODEVIBE_AGY_WRAPPER_PID, 10) : void 0
|
|
3848
3987
|
});
|
|
3988
|
+
(0, import_codevibe_core4.installDaemonProcessGuards)(logger, {
|
|
3989
|
+
onFatal: () => {
|
|
3990
|
+
void server.stop().finally(() => process.exit(1));
|
|
3991
|
+
}
|
|
3992
|
+
});
|
|
3849
3993
|
try {
|
|
3850
3994
|
const { httpPort } = await server.start();
|
|
3851
3995
|
logger.info("codevibe-antigravity-plugin ready", { httpPort });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
#
|
|
3
|
-
#
|
|
3
|
+
# Private CodeVibe Companion launcher for Antigravity CLI (agy), using a tmux
|
|
4
4
|
# session bridged to the CodeVibe MCP server for mobile sync.
|
|
5
5
|
#
|
|
6
6
|
# Architecture (DESIGN.md §3 + §5.2):
|
|
@@ -34,19 +34,15 @@
|
|
|
34
34
|
# server's signal handler), kill MCP server, remove runtime dir.
|
|
35
35
|
#
|
|
36
36
|
# Usage:
|
|
37
|
-
# codevibe
|
|
38
|
-
# codevibe-agy login # Sign in via browser
|
|
39
|
-
# codevibe-agy logout # Sign out
|
|
40
|
-
# codevibe-agy status # Show auth status
|
|
41
|
-
# codevibe-agy reset-device # Reset device key (re-pair)
|
|
37
|
+
# codevibe --agent agy [-- agy-args...]
|
|
42
38
|
#
|
|
43
39
|
# Environment:
|
|
44
40
|
# ENVIRONMENT Set to 'production' (default) or 'development'
|
|
45
41
|
# AGY_PATH Path to agy binary (default: $(command -v agy))
|
|
46
42
|
#
|
|
47
|
-
#
|
|
43
|
+
# Companion limitations:
|
|
48
44
|
# - Requires tmux. Print mode (`--print` / `-p`) is NOT supported.
|
|
49
|
-
# - Requires Node.js
|
|
45
|
+
# - Requires Node.js 22 or newer for the MCP server.
|
|
50
46
|
#
|
|
51
47
|
|
|
52
48
|
set -e
|
|
@@ -62,14 +58,6 @@ case "${ENVIRONMENT:-}" in
|
|
|
62
58
|
esac
|
|
63
59
|
CODEVIBE_TMPDIR="${TMPDIR:-/tmp}"
|
|
64
60
|
|
|
65
|
-
# ─── Config dir (agent-detection marker) ──────────────────────────────
|
|
66
|
-
# Ensure ~/.codevibe-antigravity exists on EVERY invocation (including the
|
|
67
|
-
# fast `--version` path and normal sessions). The CodeVibe 2.0 companion
|
|
68
|
-
# agent-detection probe marks agy "healthy" only when this dir exists
|
|
69
|
-
# (matching the claude/codex convention where the config dir signals
|
|
70
|
-
# "set up"). Non-fatal — a mkdir failure must never break the wrapper.
|
|
71
|
-
mkdir -p "$HOME/.codevibe-antigravity" 2>/dev/null || true
|
|
72
|
-
|
|
73
61
|
# Resolve plugin dir via symlink-aware traversal (npm globals symlink
|
|
74
62
|
# bin/ into /usr/local/bin/).
|
|
75
63
|
SOURCE="${BASH_SOURCE[0]}"
|
|
@@ -84,8 +72,8 @@ PLUGIN_DIR="$(dirname "$SCRIPT_DIR")"
|
|
|
84
72
|
# ─── PATH augmentation ───────────────────────────────────────────────
|
|
85
73
|
# When the install one-liner runs in a fresh terminal, Homebrew's
|
|
86
74
|
# installer writes the shellenv eval into ~/.zprofile (etc.) but the
|
|
87
|
-
# user's current shell hasn't sourced it yet.
|
|
88
|
-
#
|
|
75
|
+
# user's current shell hasn't sourced it yet. CodeVibe Companion launches
|
|
76
|
+
# in that terminal then fail tmux/node/agy discovery because
|
|
89
77
|
# /opt/homebrew/bin isn't on PATH. Prepend common locations so the
|
|
90
78
|
# wrapper recovers without forcing a new terminal.
|
|
91
79
|
_CV_NEW_PATHS=""
|
|
@@ -145,38 +133,45 @@ cv_failed() {
|
|
|
145
133
|
cv_telem "wrapper_failed" "\"reason\":\"$1\",\"lifetime_seconds\":$(( $(date +%s) - _CV_STARTED_AT ))"
|
|
146
134
|
}
|
|
147
135
|
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
136
|
+
# AGY_PATH is an optional preferred executable, not a poison pill. If it is
|
|
137
|
+
# unset or invalid, fall back to the same PATH lookup used by agent discovery.
|
|
138
|
+
# Requiring a regular executable file keeps this contract aligned with
|
|
139
|
+
# codevibe-core's isExecutableFile() check.
|
|
140
|
+
resolve_agy_bin() {
|
|
141
|
+
if [ -n "${AGY_PATH:-}" ] && [ -f "$AGY_PATH" ] && [ -x "$AGY_PATH" ]; then
|
|
142
|
+
printf '%s\n' "$AGY_PATH"
|
|
143
|
+
return 0
|
|
144
|
+
fi
|
|
145
|
+
command -v agy 2>/dev/null || true
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# ─── Internal diagnostic: short-circuit before heavy boot ─────────────
|
|
149
|
+
# Keep this off the public argv surface so `codevibe --agent agy -- --version`
|
|
150
|
+
# reaches the vendor CLI unchanged.
|
|
151
|
+
if [ "${CODEVIBE_COMPANION_DIAGNOSTIC:-}" = "version" ]; then
|
|
152
|
+
_CV_VER="$(node -p "require('$PLUGIN_DIR/package.json').version" 2>/dev/null || echo unknown)"
|
|
153
|
+
echo "codevibe companion agy $_CV_VER"
|
|
154
|
+
_CV_AGY_BIN="$(resolve_agy_bin)"
|
|
155
|
+
if [ -n "$_CV_AGY_BIN" ] && [ -f "$_CV_AGY_BIN" ] && [ -x "$_CV_AGY_BIN" ]; then
|
|
156
|
+
_CV_AGY_RAW="$("$_CV_AGY_BIN" --version 2>/dev/null | head -1)"
|
|
157
|
+
[ -n "$_CV_AGY_RAW" ] && echo "agy $_CV_AGY_RAW"
|
|
158
|
+
fi
|
|
159
|
+
exit 0
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# Metadata commands are intentionally short-lived and do not create a
|
|
163
|
+
# Companion session. Run them directly in the foreground so a TTY invocation
|
|
164
|
+
# cannot race a detached tmux session that exits before attach. Arguments and
|
|
165
|
+
# the vendor exit status are preserved exactly.
|
|
166
|
+
case "${1:-}" in
|
|
167
|
+
--version|-v|--help|-h)
|
|
168
|
+
_CV_DIRECT_AGY_BIN="$(resolve_agy_bin)"
|
|
169
|
+
if [ -z "$_CV_DIRECT_AGY_BIN" ] || ! [ -f "$_CV_DIRECT_AGY_BIN" ] || ! [ -x "$_CV_DIRECT_AGY_BIN" ]; then
|
|
170
|
+
echo "Error: Antigravity CLI (`agy`) is not installed or not executable." >&2
|
|
171
|
+
echo "Install instructions: https://antigravity.google/docs" >&2
|
|
172
|
+
exit 127
|
|
179
173
|
fi
|
|
174
|
+
exec "$_CV_DIRECT_AGY_BIN" "$@"
|
|
180
175
|
;;
|
|
181
176
|
esac
|
|
182
177
|
|
|
@@ -187,7 +182,7 @@ command -v node >/dev/null 2>&1 && _CV_NODE_VER="$(node -v 2>/dev/null | cv_sani
|
|
|
187
182
|
_CV_TMUX_VER="missing"
|
|
188
183
|
command -v tmux >/dev/null 2>&1 && _CV_TMUX_VER="$(tmux -V 2>/dev/null | cv_sanitize)"
|
|
189
184
|
[ -z "$_CV_TMUX_VER" ] && _CV_TMUX_VER="unknown"
|
|
190
|
-
_CV_AGY_BIN="$
|
|
185
|
+
_CV_AGY_BIN="$(resolve_agy_bin)"
|
|
191
186
|
_CV_AGY_VER="missing"
|
|
192
187
|
[ -n "$_CV_AGY_BIN" ] && _CV_AGY_VER="$("$_CV_AGY_BIN" --version 2>/dev/null | cv_sanitize)"
|
|
193
188
|
[ -z "$_CV_AGY_VER" ] && _CV_AGY_VER="unknown"
|
|
@@ -213,7 +208,7 @@ log() {
|
|
|
213
208
|
for arg in "$@"; do
|
|
214
209
|
case "$arg" in
|
|
215
210
|
--print|-p|--prompt|--print=*|--prompt=*|-p=*)
|
|
216
|
-
echo "Error:
|
|
211
|
+
echo "Error: the Antigravity Companion route does not support --print / -p / --prompt."
|
|
217
212
|
echo "Run \"agy ...\" directly for non-interactive use; mobile sync is interactive-only."
|
|
218
213
|
cv_failed "print_mode_unsupported"
|
|
219
214
|
sleep 1
|
|
@@ -231,7 +226,7 @@ if ! command -v tmux &> /dev/null; then
|
|
|
231
226
|
exit 1
|
|
232
227
|
fi
|
|
233
228
|
|
|
234
|
-
if [ -z "$_CV_AGY_BIN" ] || ! [ -x "$_CV_AGY_BIN" ]; then
|
|
229
|
+
if [ -z "$_CV_AGY_BIN" ] || ! [ -f "$_CV_AGY_BIN" ] || ! [ -x "$_CV_AGY_BIN" ]; then
|
|
235
230
|
echo "Error: Antigravity CLI (\`agy\`) is not installed or not executable."
|
|
236
231
|
echo "Install instructions: https://antigravity.google/docs"
|
|
237
232
|
cv_failed "agy_missing"
|
|
@@ -241,7 +236,7 @@ fi
|
|
|
241
236
|
|
|
242
237
|
if ! command -v node &> /dev/null; then
|
|
243
238
|
echo "Error: Node.js is required but not installed."
|
|
244
|
-
echo "Install Node.js
|
|
239
|
+
echo "Install Node.js 22+ from: https://nodejs.org"
|
|
245
240
|
cv_failed "node_missing"
|
|
246
241
|
sleep 1
|
|
247
242
|
exit 1
|
|
@@ -459,8 +454,8 @@ while [ \$i -lt 100 ] && [ ! -f '$escaped_conn_file' ]; do
|
|
|
459
454
|
i=\$((i + 1))
|
|
460
455
|
done
|
|
461
456
|
if [ ! -f '$escaped_conn_file' ]; then
|
|
462
|
-
echo '
|
|
463
|
-
echo '
|
|
457
|
+
echo 'CodeVibe Antigravity Companion: MCP server not ready after 10s; running agy without mobile sync.' >&2
|
|
458
|
+
echo 'CodeVibe Antigravity Companion: see $escaped_mcp_log for details.' >&2
|
|
464
459
|
fi
|
|
465
460
|
export ENVIRONMENT='$ENVIRONMENT'
|
|
466
461
|
$AGY_CMD
|
|
@@ -517,7 +512,7 @@ if ! kill -0 "$MCP_PID" 2>/dev/null; then
|
|
|
517
512
|
tail -3 "$MCP_LOG_FILE" 2>/dev/null | grep -v '^\[' | head -3
|
|
518
513
|
echo ""
|
|
519
514
|
echo "Server failed to start. Check $MCP_LOG_FILE for details."
|
|
520
|
-
echo "Common causes: not signed in (\`codevibe
|
|
515
|
+
echo "Common causes: not signed in (\`codevibe login\`), port in use, ENV mismatch."
|
|
521
516
|
# tmux session is already created — destroy it so the user doesn't
|
|
522
517
|
# attach to a degraded session that will time-out waiting for conn.json.
|
|
523
518
|
tmux kill-session -t "$SESSION_NAME" 2>/dev/null || true
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantiya/codevibe-antigravity-plugin",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
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
|
-
"
|
|
7
|
-
"
|
|
6
|
+
"codevibe": {
|
|
7
|
+
"companionLauncher": "./libexec/companion-launcher"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist/**/*.js",
|
|
11
|
-
"
|
|
11
|
+
"libexec",
|
|
12
12
|
"antigravity-plugin.json",
|
|
13
13
|
"README.md",
|
|
14
14
|
".env.example"
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://quantiya.ai/codevibe",
|
|
47
47
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
48
|
+
"node": ">=22.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@quantiya/codevibe-core": "2.0.
|
|
51
|
+
"@quantiya/codevibe-core": "2.0.7",
|
|
52
52
|
"chokidar": "^5.0.0",
|
|
53
53
|
"dotenv": "^16.6.1",
|
|
54
54
|
"express": "^5.1.0",
|