@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/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()).origin;
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, { stdio: "inherit", shell: false });
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
- remove ? "Grove plugin entry removed" : "Grove plugin configured. Restart OpenCode to load it."
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
- console.error(`Sign in: ${url2.href}`);
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
- response.end("Authorization received. Return to your terminal.");
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(new GroveError("login_timeout", "Sign-in timed out")),
32241
- 18e4
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 options = {
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, options) === "REDIRECT")
32249
- await auth(provider, { ...options, authorizationCode: await code });
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(values.url);
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({