@miniaxolotl/grove-opencode-plugin 0.5.0 → 1.2.2
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 -1
- package/dist/bridge.js +30 -7
- package/dist/cli.js +506 -32
- package/dist/hook.js +38 -4
- package/dist/index.js +38 -4
- package/dist/index.js.map +3 -3
- package/package.json +28 -28
package/dist/cli.js
CHANGED
|
@@ -6801,7 +6801,6 @@ var require_dist = __commonJS({
|
|
|
6801
6801
|
|
|
6802
6802
|
// packages/mcp/src/cli.ts
|
|
6803
6803
|
import { createServer } from "node:http";
|
|
6804
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
6805
6804
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
6806
6805
|
import { parseArgs } from "node:util";
|
|
6807
6806
|
|
|
@@ -21951,10 +21950,14 @@ var rememberSchema = external_exports.object({
|
|
|
21951
21950
|
capture: external_exports.enum(["explicit", "automatic"]).default("explicit"),
|
|
21952
21951
|
archived: external_exports.boolean().default(false),
|
|
21953
21952
|
kind: kindSchema.default("fact"),
|
|
21953
|
+
autoKind: external_exports.boolean().default(false),
|
|
21954
21954
|
projectId: uuidSchema.nullable().default(null),
|
|
21955
21955
|
taskId: external_exports.string().min(1).max(128).optional(),
|
|
21956
|
+
retentionDays: external_exports.number().int().min(1).max(365).nullable().optional(),
|
|
21956
21957
|
source: sourceSchema.default({ agent: "api" })
|
|
21957
21958
|
}).strict().superRefine((input, ctx) => {
|
|
21959
|
+
if (input.autoKind && input.kind === "handoff")
|
|
21960
|
+
ctx.addIssue({ code: "custom", message: "Handoff kind is explicit" });
|
|
21958
21961
|
if (Boolean(input.id) !== Boolean(input.expectedVersion)) {
|
|
21959
21962
|
ctx.addIssue({
|
|
21960
21963
|
code: "custom",
|
|
@@ -21976,6 +21979,11 @@ var rememberSchema = external_exports.object({
|
|
|
21976
21979
|
var listSchema = external_exports.object({
|
|
21977
21980
|
projectId: uuidSchema.nullable().optional(),
|
|
21978
21981
|
kind: kindSchema.optional(),
|
|
21982
|
+
tag: external_exports.string().trim().min(1).max(40).optional(),
|
|
21983
|
+
sourceAgent: sourceSchema.shape.agent.removeDefault().optional(),
|
|
21984
|
+
updatedAfter: external_exports.iso.datetime().optional(),
|
|
21985
|
+
sort: external_exports.enum(["updated_desc", "updated_asc", "created_desc"]).optional(),
|
|
21986
|
+
lifetime: external_exports.enum(["permanent", "ephemeral"]).optional(),
|
|
21979
21987
|
status: external_exports.enum(["active", "archived", "expired", "all"]).default("active"),
|
|
21980
21988
|
query: external_exports.string().trim().max(1e3).optional(),
|
|
21981
21989
|
cursor: external_exports.string().max(2048).optional(),
|
|
@@ -22019,20 +22027,36 @@ var grantSchema = external_exports.object({
|
|
|
22019
22027
|
var connectionSchema = grantSchema.extend({
|
|
22020
22028
|
clientId: external_exports.string().min(1).max(500)
|
|
22021
22029
|
});
|
|
22030
|
+
var agentSchema = external_exports.enum(["opencode", "claude", "codex"]);
|
|
22031
|
+
var agentNames = {
|
|
22032
|
+
opencode: "OpenCode",
|
|
22033
|
+
claude: "Claude Code",
|
|
22034
|
+
codex: "Codex"
|
|
22035
|
+
};
|
|
22036
|
+
var connectSchema = external_exports.object({
|
|
22037
|
+
agent: agentSchema,
|
|
22038
|
+
project: projectSchema.nullable()
|
|
22039
|
+
}).strict();
|
|
22040
|
+
var approveConnectSchema = grantSchema.extend({
|
|
22041
|
+
currentProject: external_exports.boolean().default(true)
|
|
22042
|
+
});
|
|
22022
22043
|
var memorySchema = external_exports.object({
|
|
22023
22044
|
id: uuidSchema,
|
|
22024
22045
|
projectId: uuidSchema.nullable(),
|
|
22025
22046
|
content: external_exports.string(),
|
|
22026
22047
|
kind: kindSchema,
|
|
22048
|
+
autoKind: external_exports.boolean().optional(),
|
|
22027
22049
|
status: external_exports.enum(["active", "archived"]),
|
|
22028
22050
|
version: external_exports.number().int(),
|
|
22029
22051
|
source: sourceSchema,
|
|
22030
22052
|
taskId: external_exports.string().nullable(),
|
|
22053
|
+
retentionDays: external_exports.number().int().nullable().optional(),
|
|
22031
22054
|
expiresAt: external_exports.string().nullable(),
|
|
22032
22055
|
purgeAt: external_exports.string().nullable(),
|
|
22033
22056
|
createdAt: external_exports.string(),
|
|
22034
22057
|
updatedAt: external_exports.string(),
|
|
22035
|
-
indexing: external_exports.enum(["pending", "ready", "failed"])
|
|
22058
|
+
indexing: external_exports.enum(["disabled", "pending", "ready", "failed"]),
|
|
22059
|
+
enrichment: external_exports.custom().optional()
|
|
22036
22060
|
});
|
|
22037
22061
|
var GroveError = class extends Error {
|
|
22038
22062
|
constructor(code, message, status = 400) {
|
|
@@ -22247,8 +22271,8 @@ var updateConfig = (change) => withStoreLock(async () => {
|
|
|
22247
22271
|
generation: (config2.generation ?? 0) + 1
|
|
22248
22272
|
});
|
|
22249
22273
|
});
|
|
22250
|
-
async function configuredOrigin(override) {
|
|
22251
|
-
const origin = override ?? process.env.GROVE_API_URL ?? (await readConfig())
|
|
22274
|
+
async function configuredOrigin(override, agent, fallback) {
|
|
22275
|
+
const origin = override ?? (process.env.GROVE_API_URL || void 0) ?? activeOrigin(await readConfig(), agent) ?? fallback;
|
|
22252
22276
|
if (!origin)
|
|
22253
22277
|
throw new GroveError(
|
|
22254
22278
|
"not_configured",
|
|
@@ -22256,6 +22280,20 @@ async function configuredOrigin(override) {
|
|
|
22256
22280
|
);
|
|
22257
22281
|
return apiOrigin(origin);
|
|
22258
22282
|
}
|
|
22283
|
+
function activeOrigin(config2, agent) {
|
|
22284
|
+
return agent && config2.origins?.[agent] || config2.origin;
|
|
22285
|
+
}
|
|
22286
|
+
function pinOrigins(config2) {
|
|
22287
|
+
config2.origins ??= {};
|
|
22288
|
+
for (const agent of ["cli", "opencode", "claude", "codex"]) {
|
|
22289
|
+
if (config2.origins[agent]) continue;
|
|
22290
|
+
const origins = Object.keys(config2.accounts ?? {}).filter(
|
|
22291
|
+
(origin) => config2.accounts?.[origin]?.[agent]
|
|
22292
|
+
);
|
|
22293
|
+
const selected = origins.includes(config2.origin ?? "") ? config2.origin : origins.length === 1 ? origins[0] : void 0;
|
|
22294
|
+
if (selected) config2.origins[agent] = selected;
|
|
22295
|
+
}
|
|
22296
|
+
}
|
|
22259
22297
|
function credentialPath(origin, agent, accountId) {
|
|
22260
22298
|
return join(
|
|
22261
22299
|
configDirectory(),
|
|
@@ -22292,6 +22330,8 @@ async function saveCredentials(value) {
|
|
|
22292
22330
|
value
|
|
22293
22331
|
);
|
|
22294
22332
|
const config2 = await readConfig();
|
|
22333
|
+
pinOrigins(config2);
|
|
22334
|
+
config2.origins[value.agent] = value.origin;
|
|
22295
22335
|
config2.origin = value.origin;
|
|
22296
22336
|
config2.accounts ??= {};
|
|
22297
22337
|
config2.accounts[value.origin] = {
|
|
@@ -22302,6 +22342,26 @@ async function saveCredentials(value) {
|
|
|
22302
22342
|
await writeConfig(config2);
|
|
22303
22343
|
});
|
|
22304
22344
|
}
|
|
22345
|
+
async function activateCredentials(origin, agent, accountId) {
|
|
22346
|
+
await withStoreLock(async () => {
|
|
22347
|
+
const credentials = await loadCredentials(origin, agent);
|
|
22348
|
+
if (credentials.accountId !== accountId)
|
|
22349
|
+
throw new GroveError(
|
|
22350
|
+
"account_changed",
|
|
22351
|
+
"Grove account changed; run connect again",
|
|
22352
|
+
409
|
|
22353
|
+
);
|
|
22354
|
+
const config2 = await readConfig();
|
|
22355
|
+
pinOrigins(config2);
|
|
22356
|
+
if (config2.origin !== origin || config2.origins[agent] !== origin)
|
|
22357
|
+
await writeConfig({
|
|
22358
|
+
...config2,
|
|
22359
|
+
origins: { ...config2.origins, [agent]: origin },
|
|
22360
|
+
origin,
|
|
22361
|
+
generation: (config2.generation ?? 0) + 1
|
|
22362
|
+
});
|
|
22363
|
+
});
|
|
22364
|
+
}
|
|
22305
22365
|
async function clearCredentials(origin, agent) {
|
|
22306
22366
|
await withStoreLock(async () => {
|
|
22307
22367
|
const config2 = await readConfig();
|
|
@@ -22419,7 +22479,7 @@ async function bearer(origin, agent, signal) {
|
|
|
22419
22479
|
}, signal);
|
|
22420
22480
|
}
|
|
22421
22481
|
async function nodeClient(agent = "cli", origin, timeoutMs = 1e4) {
|
|
22422
|
-
const endpoint = await configuredOrigin(origin);
|
|
22482
|
+
const endpoint = await configuredOrigin(origin, agent);
|
|
22423
22483
|
return new ApiClient(
|
|
22424
22484
|
endpoint,
|
|
22425
22485
|
(signal) => bearer(endpoint, agent, signal),
|
|
@@ -30592,7 +30652,7 @@ function createMemoryMcp(execute2) {
|
|
|
30592
30652
|
{
|
|
30593
30653
|
name: "remember",
|
|
30594
30654
|
schema: rememberSchema,
|
|
30595
|
-
description: "Save a concise durable memory, or correct one by id and expectedVersion. Reuse operationId when retrying. Handoffs require taskId and expire after 30 days (purged after 90)."
|
|
30655
|
+
description: "Save a concise durable memory, or correct one by id and expectedVersion. Set autoKind=true to let the configured decision model select a durable kind; otherwise the supplied kind is preserved. Reuse operationId when retrying. Handoffs require taskId and expire after 30 days (purged after 90)."
|
|
30596
30656
|
},
|
|
30597
30657
|
{
|
|
30598
30658
|
name: "recall",
|
|
@@ -30602,7 +30662,7 @@ function createMemoryMcp(execute2) {
|
|
|
30602
30662
|
{
|
|
30603
30663
|
name: "memories",
|
|
30604
30664
|
schema: listSchema,
|
|
30605
|
-
description: "Inspect memories with IDs, current versions, provenance, status filters and cursor pagination. Use the returned version for corrections/deletion."
|
|
30665
|
+
description: "Inspect memories with IDs, current versions, provenance, generated titles/tags/relationships, status and tag filters, hybrid text/semantic search and cursor pagination. Use the returned version for corrections/deletion."
|
|
30606
30666
|
},
|
|
30607
30667
|
{
|
|
30608
30668
|
name: "forget",
|
|
@@ -30660,8 +30720,8 @@ function createMemoryMcp(execute2) {
|
|
|
30660
30720
|
|
|
30661
30721
|
// packages/mcp/src/bridge.ts
|
|
30662
30722
|
async function startBridge(agent = "cli") {
|
|
30663
|
-
const client = await nodeClient(agent);
|
|
30664
30723
|
const server = createMemoryMcp(async (tool, raw, signal) => {
|
|
30724
|
+
const client = await nodeClient(agent);
|
|
30665
30725
|
const input = { ...raw };
|
|
30666
30726
|
if (tool === "remember")
|
|
30667
30727
|
input.source = { ...input.source, agent };
|
|
@@ -30685,7 +30745,8 @@ async function startBridge(agent = "cli") {
|
|
|
30685
30745
|
import { homedir as homedir2 } from "node:os";
|
|
30686
30746
|
import { join as join2 } from "node:path";
|
|
30687
30747
|
import { readFile as readFile2, mkdir as mkdir2, writeFile as writeFile2, rename as rename2 } from "node:fs/promises";
|
|
30688
|
-
import { spawn } from "node:child_process";
|
|
30748
|
+
import { spawn, execFile as execFile2 } from "node:child_process";
|
|
30749
|
+
import { promisify as promisify2 } from "node:util";
|
|
30689
30750
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
30690
30751
|
|
|
30691
30752
|
// node_modules/.pnpm/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
@@ -32060,9 +32121,117 @@ function applyEdits(text, edits) {
|
|
|
32060
32121
|
return text;
|
|
32061
32122
|
}
|
|
32062
32123
|
|
|
32124
|
+
// packages/grove-claude-plugin/package.json
|
|
32125
|
+
var package_default = {
|
|
32126
|
+
name: "@miniaxolotl/grove-claude-plugin",
|
|
32127
|
+
version: "1.2.2",
|
|
32128
|
+
type: "module",
|
|
32129
|
+
description: "Grove private memory tools and skills for Claude Code",
|
|
32130
|
+
license: "MIT",
|
|
32131
|
+
repository: {
|
|
32132
|
+
type: "git",
|
|
32133
|
+
url: "https://github.com/miniaxolotl/grove.git"
|
|
32134
|
+
},
|
|
32135
|
+
files: [
|
|
32136
|
+
".claude-plugin",
|
|
32137
|
+
".mcp.json",
|
|
32138
|
+
"hooks",
|
|
32139
|
+
"skills",
|
|
32140
|
+
"dist",
|
|
32141
|
+
"README.md"
|
|
32142
|
+
],
|
|
32143
|
+
publishConfig: {
|
|
32144
|
+
access: "public"
|
|
32145
|
+
},
|
|
32146
|
+
engines: {
|
|
32147
|
+
node: ">=22"
|
|
32148
|
+
}
|
|
32149
|
+
};
|
|
32150
|
+
|
|
32151
|
+
// packages/grove-codex-plugin/package.json
|
|
32152
|
+
var package_default2 = {
|
|
32153
|
+
name: "@miniaxolotl/grove-codex-plugin",
|
|
32154
|
+
version: "1.2.2",
|
|
32155
|
+
type: "module",
|
|
32156
|
+
description: "Grove private memory tools and skills for Codex",
|
|
32157
|
+
license: "MIT",
|
|
32158
|
+
repository: {
|
|
32159
|
+
type: "git",
|
|
32160
|
+
url: "https://github.com/miniaxolotl/grove.git"
|
|
32161
|
+
},
|
|
32162
|
+
files: [
|
|
32163
|
+
"plugin.json",
|
|
32164
|
+
"mcp.json",
|
|
32165
|
+
"hooks",
|
|
32166
|
+
"skills",
|
|
32167
|
+
"dist",
|
|
32168
|
+
"README.md"
|
|
32169
|
+
],
|
|
32170
|
+
publishConfig: {
|
|
32171
|
+
access: "public"
|
|
32172
|
+
},
|
|
32173
|
+
engines: {
|
|
32174
|
+
node: ">=22"
|
|
32175
|
+
}
|
|
32176
|
+
};
|
|
32177
|
+
|
|
32063
32178
|
// packages/mcp/src/install.ts
|
|
32179
|
+
function activationInstructions(agent) {
|
|
32180
|
+
if (agent === "opencode")
|
|
32181
|
+
return {
|
|
32182
|
+
restartRequired: false,
|
|
32183
|
+
message: "OpenCode loads Grove automatically. Check its MCP tools; if Grove has not appeared, restart OpenCode."
|
|
32184
|
+
};
|
|
32185
|
+
if (agent === "claude")
|
|
32186
|
+
return {
|
|
32187
|
+
restartRequired: false,
|
|
32188
|
+
message: "In Claude Code, run /reload-plugins to load Grove, or start a new session."
|
|
32189
|
+
};
|
|
32190
|
+
return {
|
|
32191
|
+
restartRequired: true,
|
|
32192
|
+
message: "Restart Codex to load Grove. Review its optional hooks in Codex to enable automatic recall."
|
|
32193
|
+
};
|
|
32194
|
+
}
|
|
32195
|
+
async function nativeInstallation(agent) {
|
|
32196
|
+
try {
|
|
32197
|
+
const { stdout } = await promisify2(execFile2)(
|
|
32198
|
+
agent,
|
|
32199
|
+
["plugin", "list", "--json"],
|
|
32200
|
+
{ timeout: 1e4, maxBuffer: 1024 * 1024 }
|
|
32201
|
+
);
|
|
32202
|
+
const value = JSON.parse(stdout);
|
|
32203
|
+
const plugins = agent === "claude" ? value : value.installed;
|
|
32204
|
+
const plugin = Array.isArray(plugins) ? plugins.find(
|
|
32205
|
+
(plugin2) => (agent === "claude" ? plugin2.id : plugin2.pluginId) === "grove@grove" && (agent !== "claude" || plugin2.scope === "user")
|
|
32206
|
+
) : void 0;
|
|
32207
|
+
if (!plugin) return "missing";
|
|
32208
|
+
const expected = (agent === "claude" ? package_default : package_default2).version;
|
|
32209
|
+
const actual = typeof plugin.version === "string" && /^\d+\.\d+\.\d+$/.test(plugin.version) ? plugin.version.split(".").map(Number) : [];
|
|
32210
|
+
const target = expected.split(".").map(Number);
|
|
32211
|
+
const difference = actual.length === 3 ? actual.map((part, index) => part - target[index]).find((part) => part !== 0) ?? 0 : -1;
|
|
32212
|
+
return plugin.enabled === true && difference >= 0 ? "ready" : "update";
|
|
32213
|
+
} catch {
|
|
32214
|
+
return "missing";
|
|
32215
|
+
}
|
|
32216
|
+
}
|
|
32217
|
+
async function requireClient(agent) {
|
|
32218
|
+
try {
|
|
32219
|
+
await promisify2(execFile2)(agent, ["--version"], {
|
|
32220
|
+
timeout: 1e4,
|
|
32221
|
+
maxBuffer: 64 * 1024
|
|
32222
|
+
});
|
|
32223
|
+
} catch {
|
|
32224
|
+
throw new GroveError(
|
|
32225
|
+
"client_missing",
|
|
32226
|
+
`Install ${agent} and make sure its command is on PATH, then run grove connect ${agent} again.`
|
|
32227
|
+
);
|
|
32228
|
+
}
|
|
32229
|
+
}
|
|
32064
32230
|
var run = (command, args) => new Promise((resolve, reject) => {
|
|
32065
|
-
const child = spawn(command, args, {
|
|
32231
|
+
const child = spawn(command, args, {
|
|
32232
|
+
stdio: ["inherit", 2, 2],
|
|
32233
|
+
shell: false
|
|
32234
|
+
});
|
|
32066
32235
|
child.once(
|
|
32067
32236
|
"error",
|
|
32068
32237
|
() => reject(new GroveError("client_missing", `${command} is not installed`))
|
|
@@ -32077,9 +32246,24 @@ var run = (command, args) => new Promise((resolve, reject) => {
|
|
|
32077
32246
|
async function installIntegration(agent, remove = false) {
|
|
32078
32247
|
if (agent === "cli")
|
|
32079
32248
|
throw new GroveError("invalid_client", "Choose an agent client");
|
|
32249
|
+
const native = !remove && agent !== "opencode" ? await nativeInstallation(agent) : "missing";
|
|
32250
|
+
if (native === "ready") {
|
|
32251
|
+
console.error("\u2713 Grove integration is already installed");
|
|
32252
|
+
return;
|
|
32253
|
+
}
|
|
32080
32254
|
if (agent === "claude") {
|
|
32081
32255
|
if (remove) return run("claude", ["plugin", "uninstall", "grove@grove"]);
|
|
32082
32256
|
await run("claude", ["plugin", "marketplace", "add", "miniaxolotl/grove"]);
|
|
32257
|
+
if (native === "update") {
|
|
32258
|
+
await run("claude", ["plugin", "marketplace", "update", "grove"]);
|
|
32259
|
+
await run("claude", [
|
|
32260
|
+
"plugin",
|
|
32261
|
+
"update",
|
|
32262
|
+
"grove@grove",
|
|
32263
|
+
"--scope",
|
|
32264
|
+
"user"
|
|
32265
|
+
]);
|
|
32266
|
+
}
|
|
32083
32267
|
await run("claude", [
|
|
32084
32268
|
"plugin",
|
|
32085
32269
|
"install",
|
|
@@ -32090,10 +32274,9 @@ async function installIntegration(agent, remove = false) {
|
|
|
32090
32274
|
} else if (agent === "codex") {
|
|
32091
32275
|
if (remove) return run("codex", ["plugin", "remove", "grove@grove"]);
|
|
32092
32276
|
await run("codex", ["plugin", "marketplace", "add", "miniaxolotl/grove"]);
|
|
32277
|
+
if (native === "update")
|
|
32278
|
+
await run("codex", ["plugin", "marketplace", "upgrade", "grove"]);
|
|
32093
32279
|
await run("codex", ["plugin", "add", "grove@grove"]);
|
|
32094
|
-
console.log(
|
|
32095
|
-
"Grove installed. Review and trust its optional hooks in Codex before automatic recall runs."
|
|
32096
|
-
);
|
|
32097
32280
|
} else {
|
|
32098
32281
|
const directory = join2(
|
|
32099
32282
|
process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config"),
|
|
@@ -32125,6 +32308,12 @@ async function installIntegration(agent, remove = false) {
|
|
|
32125
32308
|
const existing = Array.isArray(config2.plugins) ? config2.plugins : [];
|
|
32126
32309
|
const entries = existing.filter((entry) => !isGrove(entry));
|
|
32127
32310
|
if (!remove) entries.push(existing.find(isGrove) ?? name);
|
|
32311
|
+
if (!remove && existing.filter(isGrove).length === 1 || remove && !existing.some(isGrove)) {
|
|
32312
|
+
console.error(
|
|
32313
|
+
remove ? "Grove plugin entry is already removed" : "\u2713 Grove integration is already configured"
|
|
32314
|
+
);
|
|
32315
|
+
return;
|
|
32316
|
+
}
|
|
32128
32317
|
source = applyEdits(
|
|
32129
32318
|
source,
|
|
32130
32319
|
modify(source, ["plugins"], entries, {
|
|
@@ -32135,15 +32324,250 @@ async function installIntegration(agent, remove = false) {
|
|
|
32135
32324
|
const temporary = `${path}.${randomUUID2()}.tmp`;
|
|
32136
32325
|
await writeFile2(temporary, source, { flag: "wx", mode: 384 });
|
|
32137
32326
|
await rename2(temporary, path);
|
|
32327
|
+
console.error(
|
|
32328
|
+
remove ? "Grove plugin entry removed" : "\u2713 Grove integration configured"
|
|
32329
|
+
);
|
|
32330
|
+
}
|
|
32331
|
+
if (!remove && agent !== "opencode" && await nativeInstallation(agent) !== "ready")
|
|
32332
|
+
throw new GroveError(
|
|
32333
|
+
"install_incomplete",
|
|
32334
|
+
`Grove was approved, but ${agent} has not enabled the required plugin version. Check its plugin settings, then run grove connect ${agent} again to finish.`
|
|
32335
|
+
);
|
|
32336
|
+
}
|
|
32337
|
+
|
|
32338
|
+
// packages/mcp/src/browser.ts
|
|
32339
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
32340
|
+
function openBrowser(url2, noBrowser) {
|
|
32341
|
+
console.error(`Continue in your browser: ${url2.href}`);
|
|
32342
|
+
if (noBrowser) return;
|
|
32343
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
32344
|
+
const browser = spawn2(command, [url2.href], { stdio: "ignore" });
|
|
32345
|
+
browser.on("error", () => {
|
|
32346
|
+
});
|
|
32347
|
+
browser.unref();
|
|
32348
|
+
}
|
|
32349
|
+
|
|
32350
|
+
// packages/mcp/src/connect.ts
|
|
32351
|
+
import { setTimeout as delay2 } from "node:timers/promises";
|
|
32352
|
+
var cloudOrigin = "https://api.usegrove.cloud";
|
|
32353
|
+
async function waitForApproval(client, attempt, signal, interval = 2e3) {
|
|
32354
|
+
let retryInterval = interval;
|
|
32355
|
+
let recovering = false;
|
|
32356
|
+
while (attempt.status === "pending") {
|
|
32357
|
+
try {
|
|
32358
|
+
await delay2(retryInterval, void 0, { signal });
|
|
32359
|
+
attempt = await client.request(
|
|
32360
|
+
`/v1/connect/${attempt.id}`,
|
|
32361
|
+
{ signal }
|
|
32362
|
+
);
|
|
32363
|
+
retryInterval = interval;
|
|
32364
|
+
if (recovering) console.error("\u2713 Connection restored; continuing setup");
|
|
32365
|
+
recovering = false;
|
|
32366
|
+
} catch (error46) {
|
|
32367
|
+
if (signal.aborted)
|
|
32368
|
+
throw new GroveError(
|
|
32369
|
+
"approval_timeout",
|
|
32370
|
+
"Approval timed out. Run the same grove connect command to resume.",
|
|
32371
|
+
408
|
|
32372
|
+
);
|
|
32373
|
+
if (error46 instanceof GroveError && (error46.status >= 500 || ["timeout", "unreachable"].includes(error46.code))) {
|
|
32374
|
+
if (!recovering)
|
|
32375
|
+
console.error(
|
|
32376
|
+
"Grove is temporarily unavailable. Reconnecting automatically\u2026"
|
|
32377
|
+
);
|
|
32378
|
+
recovering = true;
|
|
32379
|
+
retryInterval = Math.min(retryInterval * 2, 8e3);
|
|
32380
|
+
continue;
|
|
32381
|
+
}
|
|
32382
|
+
if (error46 instanceof GroveError && error46.code === "connection_unavailable")
|
|
32383
|
+
throw new GroveError(
|
|
32384
|
+
error46.code,
|
|
32385
|
+
"Connection access changed or this setup was replaced. Run the same grove connect command to resume with your current access.",
|
|
32386
|
+
error46.status
|
|
32387
|
+
);
|
|
32388
|
+
throw error46;
|
|
32389
|
+
}
|
|
32390
|
+
}
|
|
32391
|
+
if (attempt.status !== "approved")
|
|
32392
|
+
throw new GroveError(
|
|
32393
|
+
attempt.status === "cancelled" ? "connection_cancelled" : "connection_expired",
|
|
32394
|
+
`Connection ${attempt.status}. Run grove connect when you are ready.`,
|
|
32395
|
+
409
|
|
32396
|
+
);
|
|
32397
|
+
return attempt;
|
|
32398
|
+
}
|
|
32399
|
+
async function connectAgent(options, login2) {
|
|
32400
|
+
const { agent, noBrowser } = options;
|
|
32401
|
+
const origin = apiOrigin(options.origin);
|
|
32402
|
+
const environmentKey = !!(process.env.GROVE_API_KEY && process.env.GROVE_API_URL && apiOrigin(process.env.GROVE_API_URL) === origin);
|
|
32403
|
+
await requireClient(agent);
|
|
32404
|
+
console.error(`\u2713 ${agentNames[agent]} found`);
|
|
32405
|
+
const project = await projectIdentity(process.cwd());
|
|
32406
|
+
const client = await nodeClient(agent, origin);
|
|
32407
|
+
let attempt;
|
|
32408
|
+
const start = async () => {
|
|
32409
|
+
attempt = await client.request("/v1/connect", {
|
|
32410
|
+
body: { agent, project }
|
|
32411
|
+
});
|
|
32412
|
+
const url2 = new URL(attempt.approvalUrl);
|
|
32413
|
+
apiOrigin(url2.origin);
|
|
32414
|
+
if (url2.username || url2.password || url2.pathname !== `/app/connect/${attempt.id}` || url2.search || url2.hash)
|
|
32415
|
+
throw new GroveError(
|
|
32416
|
+
"invalid_response",
|
|
32417
|
+
"Grove returned an invalid connection link",
|
|
32418
|
+
502
|
|
32419
|
+
);
|
|
32420
|
+
return attempt.status === "pending" ? url2.href : void 0;
|
|
32421
|
+
};
|
|
32422
|
+
let me;
|
|
32423
|
+
try {
|
|
32424
|
+
me = await client.request("/v1/me");
|
|
32425
|
+
} catch (error46) {
|
|
32426
|
+
if (!(error46 instanceof GroveError) || !["sign_in_required", "unauthorized", "connection_revoked"].includes(
|
|
32427
|
+
error46.code
|
|
32428
|
+
) || environmentKey)
|
|
32429
|
+
throw error46;
|
|
32430
|
+
if ((await loadCredentials(origin, agent).catch(() => void 0))?.key)
|
|
32431
|
+
throw new GroveError(
|
|
32432
|
+
"sign_in_required",
|
|
32433
|
+
"This API key is unavailable. Run grove login --key-stdin with a new key, then retry connect.",
|
|
32434
|
+
401
|
|
32435
|
+
);
|
|
32436
|
+
me = await login2(origin, agent, false, noBrowser, {
|
|
32437
|
+
freshClient: error46.code === "connection_revoked" || error46.code === "unauthorized",
|
|
32438
|
+
afterLogin: start
|
|
32439
|
+
});
|
|
32440
|
+
}
|
|
32441
|
+
console.error("\u2713 Signed in");
|
|
32442
|
+
if (!attempt) {
|
|
32443
|
+
const approvalUrl = await start();
|
|
32444
|
+
if (approvalUrl) openBrowser(new URL(approvalUrl), noBrowser);
|
|
32445
|
+
}
|
|
32446
|
+
if (attempt.status === "pending") {
|
|
32447
|
+
console.error(
|
|
32448
|
+
`Approve ${agentNames[agent]} in your browser. Waiting for access\u2026`
|
|
32449
|
+
);
|
|
32450
|
+
console.error(attempt.approvalUrl);
|
|
32451
|
+
}
|
|
32452
|
+
const approved = await waitForApproval(
|
|
32453
|
+
client,
|
|
32454
|
+
attempt,
|
|
32455
|
+
AbortSignal.timeout(10 * 6e4)
|
|
32456
|
+
);
|
|
32457
|
+
const verified = await client.request("/v1/me");
|
|
32458
|
+
if (verified.accountId !== me.accountId || approved.connectionId !== me.connectionId || verified.connectionId !== approved.connectionId)
|
|
32459
|
+
throw new GroveError(
|
|
32460
|
+
"account_changed",
|
|
32461
|
+
"The Grove account changed. Run grove connect again.",
|
|
32462
|
+
409
|
|
32463
|
+
);
|
|
32464
|
+
if (!verified.capabilities.length || approved.projectId && !verified.grant?.allProjects && !verified.grant?.projectIds.includes(approved.projectId))
|
|
32465
|
+
throw new GroveError(
|
|
32466
|
+
"connection_scope",
|
|
32467
|
+
"Connection access changed. Run grove connect again.",
|
|
32468
|
+
403
|
|
32469
|
+
);
|
|
32470
|
+
const recallVerified = verified.capabilities.includes("memory:read");
|
|
32471
|
+
if (recallVerified) {
|
|
32472
|
+
await client.request("/v1/recall", {
|
|
32473
|
+
body: {
|
|
32474
|
+
projectId: approved.projectId ?? (verified.grant?.personal ? null : verified.grant?.projectIds[0] ?? null),
|
|
32475
|
+
includePersonal: !!verified.grant?.personal,
|
|
32476
|
+
limit: 1,
|
|
32477
|
+
maxCharacters: 256
|
|
32478
|
+
}
|
|
32479
|
+
});
|
|
32480
|
+
}
|
|
32481
|
+
if (!environmentKey) await activateCredentials(origin, agent, me.accountId);
|
|
32482
|
+
console.error(
|
|
32483
|
+
recallVerified ? "\u2713 Memory access verified" : "\u2713 Grant verified (read access not selected)"
|
|
32484
|
+
);
|
|
32485
|
+
console.error(
|
|
32486
|
+
approved.projectId ? `\u2713 Project linked: ${project?.name}` : verified.grant?.personal ? "\u2713 Personal memory access ready; no current project linked" : "\u2713 Selected scopes ready; current project access was not selected"
|
|
32487
|
+
);
|
|
32488
|
+
await installIntegration(agent);
|
|
32489
|
+
if (environmentKey)
|
|
32490
|
+
console.error(
|
|
32491
|
+
"Launch your agent with the same GROVE_API_URL and GROVE_API_KEY environment."
|
|
32492
|
+
);
|
|
32493
|
+
const activation = activationInstructions(agent);
|
|
32494
|
+
if (options.json)
|
|
32138
32495
|
console.log(
|
|
32139
|
-
|
|
32496
|
+
JSON.stringify({
|
|
32497
|
+
status: "connected",
|
|
32498
|
+
origin,
|
|
32499
|
+
agent,
|
|
32500
|
+
accountId: me.accountId,
|
|
32501
|
+
connectionId: approved.connectionId,
|
|
32502
|
+
projectId: approved.projectId,
|
|
32503
|
+
capabilities: verified.capabilities,
|
|
32504
|
+
recallVerified,
|
|
32505
|
+
restartRequired: activation.restartRequired,
|
|
32506
|
+
hostVerified: false,
|
|
32507
|
+
nextStep: activation.message
|
|
32508
|
+
})
|
|
32140
32509
|
);
|
|
32510
|
+
else {
|
|
32511
|
+
const prompt = verified.capabilities.includes("memory:write") ? verified.grant?.personal ? "Use Grove to remember that I prefer concise answers in my personal memories." : "Use Grove to save a confirmed decision in an allowed project." : "Recall my available memories from Grove.";
|
|
32512
|
+
console.log(
|
|
32513
|
+
`You're connected to Grove.
|
|
32514
|
+
${activation.message}
|
|
32515
|
+
Ask your agent: "${prompt}"`
|
|
32516
|
+
);
|
|
32517
|
+
}
|
|
32518
|
+
}
|
|
32519
|
+
|
|
32520
|
+
// packages/mcp/src/select-agent.ts
|
|
32521
|
+
import { createInterface } from "node:readline/promises";
|
|
32522
|
+
async function selectAgent(interactive) {
|
|
32523
|
+
const agents = ["opencode", "claude", "codex"];
|
|
32524
|
+
const available = (await Promise.all(
|
|
32525
|
+
agents.map(async (agent) => {
|
|
32526
|
+
try {
|
|
32527
|
+
await requireClient(agent);
|
|
32528
|
+
return agent;
|
|
32529
|
+
} catch {
|
|
32530
|
+
return void 0;
|
|
32531
|
+
}
|
|
32532
|
+
})
|
|
32533
|
+
)).filter((agent) => agent !== void 0);
|
|
32534
|
+
if (!available.length)
|
|
32535
|
+
throw new GroveError(
|
|
32536
|
+
"client_missing",
|
|
32537
|
+
"Install OpenCode, Claude Code or Codex, then run grove connect. Setup guide: https://usegrove.cloud/install"
|
|
32538
|
+
);
|
|
32539
|
+
if (available.length === 1) return available[0];
|
|
32540
|
+
if (!interactive)
|
|
32541
|
+
throw new GroveError(
|
|
32542
|
+
"choose_client",
|
|
32543
|
+
`Choose an agent: ${available.map((agent) => `grove connect ${agent}`).join(" or ")}`
|
|
32544
|
+
);
|
|
32545
|
+
console.error("Welcome to Grove. Which agent would you like to connect?");
|
|
32546
|
+
available.forEach(
|
|
32547
|
+
(agent, index) => console.error(` ${index + 1}. ${agentNames[agent]}`)
|
|
32548
|
+
);
|
|
32549
|
+
const input = createInterface({
|
|
32550
|
+
input: process.stdin,
|
|
32551
|
+
output: process.stderr
|
|
32552
|
+
});
|
|
32553
|
+
try {
|
|
32554
|
+
for (; ; ) {
|
|
32555
|
+
const answer = (await input.question("Choose a number or name [1]: ")).trim().toLowerCase();
|
|
32556
|
+
const selected = available[Number(answer || "1") - 1] ?? available.find(
|
|
32557
|
+
(agent) => agent === answer || agentNames[agent].toLowerCase() === answer
|
|
32558
|
+
);
|
|
32559
|
+
if (selected) return selected;
|
|
32560
|
+
console.error("Choose one of the installed agents above.");
|
|
32561
|
+
}
|
|
32562
|
+
} finally {
|
|
32563
|
+
input.close();
|
|
32141
32564
|
}
|
|
32142
32565
|
}
|
|
32143
32566
|
|
|
32144
32567
|
// packages/mcp/src/cli.ts
|
|
32145
32568
|
var help = `Grove \u2014 private memory across your agents
|
|
32146
32569
|
|
|
32570
|
+
grove connect [opencode|claude|codex] [--url <self-hosted origin>] [--no-browser]
|
|
32147
32571
|
grove init --url <origin> --client opencode|claude|codex
|
|
32148
32572
|
grove login --url <origin> --client <client> [--key-stdin | --no-browser]
|
|
32149
32573
|
grove status [--client <client>] [--json]
|
|
@@ -32154,9 +32578,10 @@ var help = `Grove \u2014 private memory across your agents
|
|
|
32154
32578
|
grove mcp --stdio [--client <client>]
|
|
32155
32579
|
grove serve (requires @miniaxolotl/grove-server or use Docker)
|
|
32156
32580
|
|
|
32581
|
+
Run grove connect to choose an installed agent, create an account or sign in, and finish setup.
|
|
32157
32582
|
Credentials stay in your user configuration, never in project files.
|
|
32158
32583
|
Use GROVE_API_URL + GROVE_API_KEY for headless clients. Logs use stderr in stdio mode.`;
|
|
32159
|
-
async function login(origin, agent, keyStdin, noBrowser) {
|
|
32584
|
+
async function login(origin, agent, keyStdin, noBrowser, options = {}) {
|
|
32160
32585
|
if (keyStdin) {
|
|
32161
32586
|
let key = "";
|
|
32162
32587
|
for await (const chunk of process.stdin) {
|
|
@@ -32179,6 +32604,7 @@ async function login(origin, agent, keyStdin, noBrowser) {
|
|
|
32179
32604
|
fail = reject;
|
|
32180
32605
|
});
|
|
32181
32606
|
const previous = await loadCredentials(origin, agent).catch(() => void 0);
|
|
32607
|
+
let callbackResponse;
|
|
32182
32608
|
const callbackPort = {
|
|
32183
32609
|
cli: 34141,
|
|
32184
32610
|
opencode: 34142,
|
|
@@ -32191,23 +32617,17 @@ async function login(origin, agent, keyStdin, noBrowser) {
|
|
|
32191
32617
|
origin,
|
|
32192
32618
|
agent,
|
|
32193
32619
|
redirectUrl,
|
|
32194
|
-
client: previous?.redirectUrl === redirectUrl ? previous.client : void 0
|
|
32620
|
+
client: !options.freshClient && previous?.redirectUrl === redirectUrl ? previous.client : void 0
|
|
32195
32621
|
},
|
|
32196
32622
|
async (url2) => {
|
|
32197
|
-
|
|
32198
|
-
if (noBrowser) return;
|
|
32199
|
-
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
|
|
32200
|
-
const browser = spawn2(command, [url2.href], { stdio: "ignore" });
|
|
32201
|
-
browser.on("error", () => {
|
|
32202
|
-
});
|
|
32203
|
-
browser.unref();
|
|
32623
|
+
openBrowser(url2, noBrowser);
|
|
32204
32624
|
}
|
|
32205
32625
|
);
|
|
32206
32626
|
const server = createServer((request, response) => {
|
|
32207
32627
|
const url2 = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
32208
32628
|
response.setHeader("Content-Type", "text/plain");
|
|
32209
32629
|
response.setHeader("Cache-Control", "no-store");
|
|
32210
|
-
if (url2.pathname !== "/callback" || !provider.checkState(url2.searchParams.get("state"))) {
|
|
32630
|
+
if (url2.pathname !== "/callback" || callbackResponse || !provider.checkState(url2.searchParams.get("state"))) {
|
|
32211
32631
|
response.writeHead(400).end("Invalid authorization callback");
|
|
32212
32632
|
return;
|
|
32213
32633
|
}
|
|
@@ -32219,7 +32639,9 @@ async function login(origin, agent, keyStdin, noBrowser) {
|
|
|
32219
32639
|
);
|
|
32220
32640
|
return;
|
|
32221
32641
|
}
|
|
32222
|
-
|
|
32642
|
+
callbackResponse = response;
|
|
32643
|
+
if (!options.afterLogin)
|
|
32644
|
+
response.end("Authorization received. Return to your terminal.");
|
|
32223
32645
|
finish(result);
|
|
32224
32646
|
});
|
|
32225
32647
|
await new Promise((resolve, reject) => {
|
|
@@ -32236,17 +32658,25 @@ async function login(origin, agent, keyStdin, noBrowser) {
|
|
|
32236
32658
|
});
|
|
32237
32659
|
void code.catch(() => {
|
|
32238
32660
|
});
|
|
32661
|
+
console.error(
|
|
32662
|
+
"Create your Grove account or sign in in the browser. Setup will continue here automatically."
|
|
32663
|
+
);
|
|
32239
32664
|
const timeout = setTimeout(
|
|
32240
|
-
() => fail(
|
|
32241
|
-
|
|
32665
|
+
() => fail(
|
|
32666
|
+
new GroveError(
|
|
32667
|
+
"login_timeout",
|
|
32668
|
+
"Sign-in timed out. Run the same command to try again."
|
|
32669
|
+
)
|
|
32670
|
+
),
|
|
32671
|
+
10 * 6e4
|
|
32242
32672
|
);
|
|
32243
32673
|
try {
|
|
32244
|
-
const
|
|
32674
|
+
const authOptions = {
|
|
32245
32675
|
serverUrl: `${origin}/mcp`,
|
|
32246
32676
|
fetchFn: (url2, init) => fetch(url2, { ...init, signal: AbortSignal.timeout(1e4) })
|
|
32247
32677
|
};
|
|
32248
|
-
if (await auth(provider,
|
|
32249
|
-
await auth(provider, { ...
|
|
32678
|
+
if (await auth(provider, authOptions) === "REDIRECT")
|
|
32679
|
+
await auth(provider, { ...authOptions, authorizationCode: await code });
|
|
32250
32680
|
const token = provider.data.tokens?.access_token;
|
|
32251
32681
|
if (!token)
|
|
32252
32682
|
throw new GroveError("login_failed", "No access token was returned");
|
|
@@ -32258,8 +32688,23 @@ async function login(origin, agent, keyStdin, noBrowser) {
|
|
|
32258
32688
|
...provider.data,
|
|
32259
32689
|
accountId: me.accountId
|
|
32260
32690
|
});
|
|
32691
|
+
const continuation = await options.afterLogin?.(me);
|
|
32692
|
+
if (callbackResponse && !callbackResponse.writableEnded) {
|
|
32693
|
+
if (continuation)
|
|
32694
|
+
callbackResponse.writeHead(303, { Location: continuation });
|
|
32695
|
+
await new Promise(
|
|
32696
|
+
(done) => callbackResponse.end(
|
|
32697
|
+
continuation ? "Continue to Grove to approve access." : "Access ready. Return to your terminal to finish setup.",
|
|
32698
|
+
done
|
|
32699
|
+
)
|
|
32700
|
+
);
|
|
32701
|
+
}
|
|
32261
32702
|
return me;
|
|
32262
32703
|
} finally {
|
|
32704
|
+
if (callbackResponse && !callbackResponse.writableEnded)
|
|
32705
|
+
callbackResponse.end(
|
|
32706
|
+
"Connection could not be completed. Return to your terminal and run grove connect again."
|
|
32707
|
+
);
|
|
32263
32708
|
clearTimeout(timeout);
|
|
32264
32709
|
server.closeAllConnections();
|
|
32265
32710
|
server.close();
|
|
@@ -32287,11 +32732,35 @@ async function main() {
|
|
|
32287
32732
|
);
|
|
32288
32733
|
return;
|
|
32289
32734
|
}
|
|
32290
|
-
const command = positionals[0];
|
|
32735
|
+
const command = positionals[0] ?? (process.stdin.isTTY && !values.json ? "connect" : void 0);
|
|
32291
32736
|
if (!command || values.help) {
|
|
32292
32737
|
console.log(help);
|
|
32293
32738
|
return;
|
|
32294
32739
|
}
|
|
32740
|
+
if (command === "connect") {
|
|
32741
|
+
const selected = positionals[1] ?? (values.client !== "cli" ? values.client : await selectAgent(!!process.stdin.isTTY && !values.json));
|
|
32742
|
+
const parsed = agentSchema.safeParse(selected);
|
|
32743
|
+
if (!parsed.success || positionals.length > 2 || positionals[1] && values.client !== "cli" && values.client !== positionals[1])
|
|
32744
|
+
throw new GroveError(
|
|
32745
|
+
"invalid_client",
|
|
32746
|
+
"Use grove connect opencode, claude or codex"
|
|
32747
|
+
);
|
|
32748
|
+
if (values["key-stdin"] || values.stdio)
|
|
32749
|
+
throw new GroveError(
|
|
32750
|
+
"invalid_command",
|
|
32751
|
+
"Use grove login --key-stdin for API keys, or grove mcp --stdio for the bridge"
|
|
32752
|
+
);
|
|
32753
|
+
await connectAgent(
|
|
32754
|
+
{
|
|
32755
|
+
agent: parsed.data,
|
|
32756
|
+
origin: values.url ?? (process.env.GROVE_API_URL || cloudOrigin),
|
|
32757
|
+
noBrowser: !!values["no-browser"],
|
|
32758
|
+
json: !!values.json
|
|
32759
|
+
},
|
|
32760
|
+
login
|
|
32761
|
+
);
|
|
32762
|
+
return;
|
|
32763
|
+
}
|
|
32295
32764
|
const agent = values.client;
|
|
32296
32765
|
if (!["cli", "opencode", "claude", "codex"].includes(agent))
|
|
32297
32766
|
throw new GroveError(
|
|
@@ -32330,7 +32799,11 @@ async function main() {
|
|
|
32330
32799
|
await installIntegration(agent, true);
|
|
32331
32800
|
return;
|
|
32332
32801
|
}
|
|
32333
|
-
const origin = await configuredOrigin(
|
|
32802
|
+
const origin = await configuredOrigin(
|
|
32803
|
+
values.url,
|
|
32804
|
+
agent,
|
|
32805
|
+
command === "login" || command === "init" ? cloudOrigin : void 0
|
|
32806
|
+
);
|
|
32334
32807
|
if (command === "login" || command === "init") {
|
|
32335
32808
|
const me = await login(
|
|
32336
32809
|
origin,
|
|
@@ -32357,6 +32830,7 @@ async function main() {
|
|
|
32357
32830
|
"Choose --client opencode, claude or codex to install"
|
|
32358
32831
|
);
|
|
32359
32832
|
await installIntegration(agent);
|
|
32833
|
+
console.error(activationInstructions(agent).message);
|
|
32360
32834
|
if (me.capabilities.includes("memory:read"))
|
|
32361
32835
|
console.log(
|
|
32362
32836
|
JSON.stringify({
|