@nail00749/agent-gvozd 0.2.0 → 0.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.
Files changed (4) hide show
  1. package/dist/cli.js +200 -115
  2. package/dist/index.js +26 -23
  3. package/dist/tui.js +180 -56
  4. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -988,7 +988,7 @@ var i = `${styleText("gray", S_BAR)} `;
988
988
 
989
989
  // src/cli.ts
990
990
  import { realpathSync as realpathSync5 } from "node:fs";
991
- import { join as join11 } from "node:path";
991
+ import { join as join12 } from "node:path";
992
992
  import { fileURLToPath as fileURLToPath2 } from "node:url";
993
993
 
994
994
  // src/config.ts
@@ -7230,8 +7230,8 @@ function loadConfig(projectDirectory, options = {}) {
7230
7230
  }
7231
7231
 
7232
7232
  // src/cli/doctor.ts
7233
- import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "node:fs";
7234
- import { join as join4, resolve as resolve4 } from "node:path";
7233
+ import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "node:fs";
7234
+ import { join as join5, resolve as resolve4 } from "node:path";
7235
7235
 
7236
7236
  // src/tool-permissions.ts
7237
7237
  function family(command, ...variants) {
@@ -7337,7 +7337,7 @@ function buildAgentPermissions(agent, mcpServers) {
7337
7337
 
7338
7338
  // src/release-metadata.ts
7339
7339
  var PACKAGE_NAME = "@nail00749/agent-gvozd";
7340
- var PACKAGE_VERSION = "0.2.0";
7340
+ var PACKAGE_VERSION = "0.2.2";
7341
7341
  var PACKAGE_SPEC = `${PACKAGE_NAME}@${PACKAGE_VERSION}`;
7342
7342
  var SUPPORTED_OPENCODE_VERSION = "2.0.*";
7343
7343
  var CONFIG_SCHEMA_VERSION = 2;
@@ -7434,12 +7434,15 @@ function redactDiagnostic(error) {
7434
7434
 
7435
7435
  // src/cli/opencode.ts
7436
7436
  import { spawn } from "node:child_process";
7437
- import { isAbsolute as isAbsolute4 } from "node:path";
7437
+ import { closeSync, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync } from "node:fs";
7438
+ import { isAbsolute as isAbsolute4, join as join4 } from "node:path";
7439
+ import { tmpdir } from "node:os";
7438
7440
  var MAX_OUTPUT_BYTES = 64 * 1024;
7439
7441
  var AGENT_OUTPUT_BYTES = 4 * 1024 * 1024;
7440
7442
  var DEFAULT_TIMEOUT_MS = 15000;
7441
7443
  var defaultProcessRunner = {
7442
- run(executable, args, timeoutMs = DEFAULT_TIMEOUT_MS, maxOutputBytes = MAX_OUTPUT_BYTES) {
7444
+ run(executable, args, timeoutMs = DEFAULT_TIMEOUT_MS, maxOutputBytes = MAX_OUTPUT_BYTES, options) {
7445
+ const stdoutFile = options?.stdoutFile;
7443
7446
  const appendBounded = (current, chunk) => {
7444
7447
  if (Buffer.byteLength(current) >= maxOutputBytes)
7445
7448
  return current;
@@ -7448,11 +7451,21 @@ var defaultProcessRunner = {
7448
7451
  };
7449
7452
  return new Promise((resolve, reject) => {
7450
7453
  const grouped = process.platform !== "win32";
7454
+ let stdoutFd;
7455
+ try {
7456
+ if (stdoutFile)
7457
+ stdoutFd = openSync(stdoutFile, "w");
7458
+ } catch (error) {
7459
+ reject(error);
7460
+ return;
7461
+ }
7451
7462
  const child = spawn(executable, [...args], {
7452
7463
  detached: grouped,
7453
7464
  shell: false,
7454
- stdio: ["ignore", "pipe", "pipe"]
7465
+ stdio: ["ignore", stdoutFile ? stdoutFd : "pipe", "pipe"]
7455
7466
  });
7467
+ if (stdoutFd !== undefined)
7468
+ closeSync(stdoutFd);
7456
7469
  let stdout = "";
7457
7470
  let stderr = "";
7458
7471
  let settled = false;
@@ -7495,10 +7508,10 @@ var defaultProcessRunner = {
7495
7508
  settled = true;
7496
7509
  clearTimers();
7497
7510
  if (destroy) {
7498
- child.stdout.off("data", onStdout);
7499
- child.stderr.off("data", onStderr);
7500
- child.stdout.destroy();
7501
- child.stderr.destroy();
7511
+ child.stderr?.off("data", onStderr);
7512
+ child.stderr?.destroy();
7513
+ child.stdout?.off("data", onStdout);
7514
+ child.stdout?.destroy();
7502
7515
  }
7503
7516
  reject(timeoutError());
7504
7517
  };
@@ -7513,18 +7526,20 @@ var defaultProcessRunner = {
7513
7526
  hardDeadline.unref();
7514
7527
  }, timeoutMs);
7515
7528
  timer.unref();
7516
- child.stdout.on("data", onStdout);
7517
- child.stderr.on("data", onStderr);
7529
+ if (child.stdout)
7530
+ child.stdout.on("data", onStdout);
7531
+ if (child.stderr)
7532
+ child.stderr.on("data", onStderr);
7518
7533
  child.once("error", (error) => {
7519
7534
  clearTimers();
7520
7535
  if (settled)
7521
7536
  return;
7522
7537
  settled = true;
7523
7538
  if (timedOut) {
7524
- child.stdout.off("data", onStdout);
7525
- child.stderr.off("data", onStderr);
7526
- child.stdout.destroy();
7527
- child.stderr.destroy();
7539
+ child.stderr?.off("data", onStderr);
7540
+ child.stderr?.destroy();
7541
+ child.stdout?.off("data", onStdout);
7542
+ child.stdout?.destroy();
7528
7543
  }
7529
7544
  reject(error);
7530
7545
  });
@@ -7537,7 +7552,16 @@ var defaultProcessRunner = {
7537
7552
  return;
7538
7553
  }
7539
7554
  settled = true;
7540
- resolve({ code: code ?? 1, stdout, stderr });
7555
+ const finish = (stdoutText) => resolve({ code: code ?? 1, stdout: stdoutText, stderr });
7556
+ if (stdoutFile) {
7557
+ try {
7558
+ finish(bounded(readFileSync3(stdoutFile, "utf8"), maxOutputBytes));
7559
+ } catch (error) {
7560
+ reject(error);
7561
+ }
7562
+ return;
7563
+ }
7564
+ finish(stdout);
7541
7565
  });
7542
7566
  });
7543
7567
  }
@@ -7545,8 +7569,8 @@ var defaultProcessRunner = {
7545
7569
  function bounded(value, maxOutputBytes = MAX_OUTPUT_BYTES) {
7546
7570
  return Buffer.from(value).subarray(0, maxOutputBytes).toString("utf8");
7547
7571
  }
7548
- async function checked(runner, executable, args, timeoutMs, maxOutputBytes) {
7549
- const result = await runner.run(executable, args, timeoutMs, maxOutputBytes);
7572
+ async function checked(runner, executable, args, timeoutMs, maxOutputBytes, options) {
7573
+ const result = await runner.run(executable, args, timeoutMs, maxOutputBytes, options);
7550
7574
  if (result.code !== 0) {
7551
7575
  const detail = redactDiagnostic(bounded(result.stderr).trim());
7552
7576
  throw new Error(`OpenCode command exited ${result.code}${detail ? `: ${detail}` : ""}`);
@@ -7616,8 +7640,14 @@ function createClient(executable, runner) {
7616
7640
  return checked(runner, executable, spec ? ["plugin", "check", spec] : ["plugin", "check"], 30000);
7617
7641
  },
7618
7642
  async debugAgents() {
7619
- const raw = await checked(runner, executable, ["debug", "agents"], 60000, AGENT_OUTPUT_BYTES);
7620
- return parseAgentIdentifiers(raw);
7643
+ const directory = mkdtempSync(join4(tmpdir(), "gvozd-agents-"));
7644
+ const stdoutFile = join4(directory, "agents.out");
7645
+ try {
7646
+ const raw = await checked(runner, executable, ["debug", "agents"], 60000, AGENT_OUTPUT_BYTES, { stdoutFile });
7647
+ return parseAgentIdentifiers(raw);
7648
+ } finally {
7649
+ rmSync(directory, { recursive: true, force: true });
7650
+ }
7621
7651
  },
7622
7652
  serviceStatus() {
7623
7653
  return checked(runner, executable, ["service", "status"]);
@@ -7724,23 +7754,23 @@ function checkGlobalFiles(config, configRoot) {
7724
7754
  for (const [id, agent] of Object.entries(config.agents).sort(([left], [right]) => left.localeCompare(right))) {
7725
7755
  if (agent.disabled)
7726
7756
  continue;
7727
- const path = join4(configRoot, "agents", `${id}.md`);
7757
+ const path = join5(configRoot, "agents", `${id}.md`);
7728
7758
  if (!safeFile(path)) {
7729
7759
  missing.push(id);
7730
7760
  continue;
7731
7761
  }
7732
- const content = readFileSync3(path, "utf8");
7762
+ const content = readFileSync4(path, "utf8");
7733
7763
  if (!hasGeneratedAgentMarker(content) || content !== renderAgent(agent))
7734
7764
  stale.push(id);
7735
7765
  }
7736
7766
  const enabled = new Set(Object.entries(config.agents).filter(([, agent]) => !agent.disabled).map(([id]) => `${id}.md`));
7737
7767
  const orphans = [];
7738
- const directory = join4(configRoot, "agents");
7768
+ const directory = join5(configRoot, "agents");
7739
7769
  if (existsSync3(directory) && lstatSync3(directory).isDirectory() && !lstatSync3(directory).isSymbolicLink()) {
7740
7770
  for (const entry of readdirSync3(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
7741
7771
  if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
7742
7772
  continue;
7743
- const content = readFileSync3(join4(directory, entry.name), "utf8");
7773
+ const content = readFileSync4(join5(directory, entry.name), "utf8");
7744
7774
  if (hasGeneratedAgentMarker(content))
7745
7775
  orphans.push(entry.name.slice(0, -3));
7746
7776
  }
@@ -7757,10 +7787,10 @@ function checkGlobalFiles(config, configRoot) {
7757
7787
  }
7758
7788
  function checkLegacy(config) {
7759
7789
  const duplicates = [];
7760
- const plugin = join4(config.projectRoot, ".opencode", "plugins", "agent-gvozd", "index.ts");
7790
+ const plugin = join5(config.projectRoot, ".opencode", "plugins", "agent-gvozd", "index.ts");
7761
7791
  if (safeFile(plugin))
7762
7792
  duplicates.push("local plugin");
7763
- const agents = Object.keys(config.agents).filter((id) => safeFile(join4(config.projectRoot, ".opencode", "agents", `${id}.md`)));
7793
+ const agents = Object.keys(config.agents).filter((id) => safeFile(join5(config.projectRoot, ".opencode", "agents", `${id}.md`)));
7764
7794
  if (agents.length > 0)
7765
7795
  duplicates.push(`${agents.length} local agents`);
7766
7796
  if (duplicates.length === 0)
@@ -7811,13 +7841,13 @@ async function runDoctor(input) {
7811
7841
  }
7812
7842
  let config;
7813
7843
  let globalConfig;
7814
- const configPath = join4(input.configRoot, "gvozd", "config.jsonc");
7815
- const schemaPath = join4(input.configRoot, "gvozd", "schema.json");
7844
+ const configPath = join5(input.configRoot, "gvozd", "config.jsonc");
7845
+ const schemaPath = join5(input.configRoot, "gvozd", "schema.json");
7816
7846
  try {
7817
7847
  if (!safeFile(configPath) || !safeFile(schemaPath))
7818
7848
  throw new Error("global config.jsonc or schema.json is missing");
7819
7849
  const schemaErrors = [];
7820
- const schema = parse2(readFileSync3(schemaPath, "utf8"), schemaErrors);
7850
+ const schema = parse2(readFileSync4(schemaPath, "utf8"), schemaErrors);
7821
7851
  if (schemaErrors.length > 0 || schema?.["x-agent-gvozd-schema-version"] !== CONFIG_SCHEMA_VERSION || schema?.$comment !== GENERATED_PLUGIN_MARKER) {
7822
7852
  throw new Error("global schema is invalid, incompatible, or unmanaged");
7823
7853
  }
@@ -7885,17 +7915,17 @@ function doctorOperationalFailure(error) {
7885
7915
  }
7886
7916
 
7887
7917
  // src/cli/agents.ts
7888
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
7889
- import { join as join7 } from "node:path";
7918
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
7919
+ import { join as join8 } from "node:path";
7890
7920
 
7891
7921
  // src/cli/config-store.ts
7892
- import { accessSync, closeSync, constants as fsConstants, existsSync as existsSync4, lstatSync as lstatSync5, mkdirSync, openSync, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync } from "node:fs";
7922
+ import { accessSync, closeSync as closeSync2, constants as fsConstants, existsSync as existsSync4, lstatSync as lstatSync5, mkdirSync, openSync as openSync2, readFileSync as readFileSync5, renameSync, unlinkSync, writeFileSync } from "node:fs";
7893
7923
  import { randomUUID } from "node:crypto";
7894
- import { dirname as dirname3, join as join6 } from "node:path";
7924
+ import { dirname as dirname3, join as join7 } from "node:path";
7895
7925
 
7896
7926
  // src/secure-path.ts
7897
7927
  import { lstatSync as lstatSync4, realpathSync as realpathSync3 } from "node:fs";
7898
- import { isAbsolute as isAbsolute5, join as join5, parse as parse6, relative as relative3, resolve as resolve5, sep } from "node:path";
7928
+ import { isAbsolute as isAbsolute5, join as join6, parse as parse6, relative as relative3, resolve as resolve5, sep } from "node:path";
7899
7929
  function secureCanonicalPath(target, label = "Path") {
7900
7930
  if (!isAbsolute5(target))
7901
7931
  throw new Error(`${label} must be absolute: ${target}`);
@@ -7911,7 +7941,7 @@ function secureCanonicalPath(target, label = "Path") {
7911
7941
  throw new Error(`${label} has an unsafe filesystem root: ${root}`);
7912
7942
  const existingDirectories = [{ path: root, stat: rootStat }];
7913
7943
  for (const component of components) {
7914
- current = join5(current, component);
7944
+ current = join6(current, component);
7915
7945
  if (missing) {
7916
7946
  missingSuffix.push(component);
7917
7947
  continue;
@@ -7991,7 +8021,7 @@ function matchesSnapshot(path, expected) {
7991
8021
  if (!existsSync4(path))
7992
8022
  return false;
7993
8023
  const stat = lstatSync5(path);
7994
- return stat.isFile() && !stat.isSymbolicLink() && stat.dev === expected.dev && stat.ino === expected.ino && readFileSync4(path, "utf8") === expected.bytes;
8024
+ return stat.isFile() && !stat.isSymbolicLink() && stat.dev === expected.dev && stat.ino === expected.ino && readFileSync5(path, "utf8") === expected.bytes;
7995
8025
  }
7996
8026
  function atomicWrite(path, content, expected) {
7997
8027
  const canonicalPath = secureCanonicalPath(path, "Managed global config path");
@@ -8002,16 +8032,16 @@ function atomicWrite(path, content, expected) {
8002
8032
  throw new Error(`Global config path changed during write: ${path}`);
8003
8033
  assertWriteable(dirname3(canonicalPath), "Managed global config directory");
8004
8034
  const temporary = `${canonicalPath}.tmp-${process.pid}-${randomUUID()}`;
8005
- const descriptor = openSync(temporary, "wx", 384);
8035
+ const descriptor = openSync2(temporary, "wx", 384);
8006
8036
  try {
8007
8037
  writeFileSync(descriptor, content);
8008
- closeSync(descriptor);
8038
+ closeSync2(descriptor);
8009
8039
  if (!matchesSnapshot(canonicalPath, expected))
8010
8040
  throw new Error(`Refusing to replace concurrently changed file: ${canonicalPath}`);
8011
8041
  renameSync(temporary, canonicalPath);
8012
8042
  } catch (error) {
8013
8043
  try {
8014
- closeSync(descriptor);
8044
+ closeSync2(descriptor);
8015
8045
  } catch {}
8016
8046
  try {
8017
8047
  unlinkSync(temporary);
@@ -8063,7 +8093,7 @@ function snapshot(path) {
8063
8093
  const stat = lstatSync5(path);
8064
8094
  if (!stat.isFile() || stat.isSymbolicLink())
8065
8095
  throw new Error(`Managed global config snapshot target is unsafe: ${path}`);
8066
- return Object.freeze({ path, exists: true, bytes: readFileSync4(path, "utf8"), dev: stat.dev, ino: stat.ino });
8096
+ return Object.freeze({ path, exists: true, bytes: readFileSync5(path, "utf8"), dev: stat.dev, ino: stat.ino });
8067
8097
  }
8068
8098
  function preflightGlobalConfig(configRoot, schemaSource) {
8069
8099
  const canonicalRoot = secureCanonicalPath(configRoot, "OpenCode config root");
@@ -8072,24 +8102,24 @@ function preflightGlobalConfig(configRoot, schemaSource) {
8072
8102
  throw new Error(`OpenCode config root is not a safe directory: ${canonicalRoot}`);
8073
8103
  }
8074
8104
  assertWriteable(canonicalRoot, "OpenCode config root");
8075
- const directory = secureCanonicalPath(join6(canonicalRoot, "gvozd"), "Global Gvozd directory");
8105
+ const directory = secureCanonicalPath(join7(canonicalRoot, "gvozd"), "Global Gvozd directory");
8076
8106
  const directoryStat = existsSync4(directory) ? lstatSync5(directory) : undefined;
8077
8107
  if (directoryStat && (directoryStat.isSymbolicLink() || !directoryStat.isDirectory())) {
8078
8108
  throw new Error(`Global Gvozd path is not a safe directory: ${directory}`);
8079
8109
  }
8080
8110
  assertWriteable(directory, "Global Gvozd directory");
8081
- const configPath = join6(directory, "config.jsonc");
8082
- const schemaPath = join6(directory, "schema.json");
8111
+ const configPath = join7(directory, "config.jsonc");
8112
+ const schemaPath = join7(directory, "schema.json");
8083
8113
  if (existsSync4(configPath)) {
8084
8114
  if (!isRegularFile(configPath))
8085
8115
  throw new Error(`Global Gvozd config is not a safe file: ${configPath}`);
8086
- assertValidJsonc(readFileSync4(configPath, "utf8"), configPath);
8116
+ assertValidJsonc(readFileSync5(configPath, "utf8"), configPath);
8087
8117
  assertWriteable(configPath, "Global Gvozd config");
8088
8118
  }
8089
8119
  if (existsSync4(schemaPath)) {
8090
8120
  if (!isRegularFile(schemaPath))
8091
8121
  throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
8092
- const schema = readFileSync4(schemaPath, "utf8");
8122
+ const schema = readFileSync5(schemaPath, "utf8");
8093
8123
  if (!hasGeneratedSchemaMarker(schema) && !legacySchemaMatches(schema, schemaSource)) {
8094
8124
  throw new Error(`Refusing to overwrite unmanaged Gvozd schema: ${schemaPath}`);
8095
8125
  }
@@ -8102,9 +8132,9 @@ function writeGlobalConfig(input) {
8102
8132
  const canonicalRoot = secureCanonicalPath(input.configRoot, "OpenCode config root");
8103
8133
  if (state.configRoot !== canonicalRoot)
8104
8134
  throw new Error("Global config snapshot belongs to another config root");
8105
- const directory = join6(state.configRoot, "gvozd");
8106
- const configPath = join6(directory, "config.jsonc");
8107
- const schemaPath = join6(directory, "schema.json");
8135
+ const directory = join7(state.configRoot, "gvozd");
8136
+ const configPath = join7(directory, "config.jsonc");
8137
+ const schemaPath = join7(directory, "schema.json");
8108
8138
  const base = state.config.exists ? state.config.bytes : `{
8109
8139
  "$schema": "./schema.json",
8110
8140
  "agents": {}
@@ -8153,21 +8183,21 @@ function setAgentDisabled(source, agentID, disabled) {
8153
8183
  return applyEdits(source, modify(source, ["agents", agentID, "disabled"], disabled, { formattingOptions: formattingOptions2 }));
8154
8184
  }
8155
8185
  function readGlobalConfig(configRoot) {
8156
- const path = join7(configRoot, "gvozd", "config.jsonc");
8186
+ const path = join8(configRoot, "gvozd", "config.jsonc");
8157
8187
  if (!existsSync5(path))
8158
8188
  return `{
8159
8189
  "$schema": "./schema.json",
8160
8190
  "agents": {}
8161
8191
  }
8162
8192
  `;
8163
- const source = readFileSync5(path, "utf8");
8193
+ const source = readFileSync6(path, "utf8");
8164
8194
  assertValidJsonc2(source, path);
8165
8195
  return source;
8166
8196
  }
8167
8197
 
8168
8198
  // src/cli/setup.ts
8169
- import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
8170
- import { dirname as dirname6, join as join9 } from "node:path";
8199
+ import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
8200
+ import { dirname as dirname6, join as join10 } from "node:path";
8171
8201
 
8172
8202
  // src/cli/configure.ts
8173
8203
  function availableProfile(profile, catalog) {
@@ -8238,9 +8268,9 @@ function profileFromAgents(agents, catalog) {
8238
8268
  }
8239
8269
 
8240
8270
  // src/cli/global-sync.ts
8241
- import { accessSync as accessSync2, closeSync as closeSync2, constants as fsConstants2, existsSync as existsSync6, lstatSync as lstatSync6, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync6, readdirSync as readdirSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
8271
+ import { accessSync as accessSync2, closeSync as closeSync3, constants as fsConstants2, existsSync as existsSync6, lstatSync as lstatSync6, mkdirSync as mkdirSync2, openSync as openSync3, readFileSync as readFileSync7, readdirSync as readdirSync4, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
8242
8272
  import { randomUUID as randomUUID2 } from "node:crypto";
8243
- import { dirname as dirname4, join as join8 } from "node:path";
8273
+ import { dirname as dirname4, join as join9 } from "node:path";
8244
8274
  function stat(path) {
8245
8275
  try {
8246
8276
  return lstatSync6(path);
@@ -8274,11 +8304,11 @@ function createFile(path, content) {
8274
8304
  const canonicalPath = secureCanonicalPath(path, "Managed global agent path");
8275
8305
  if (canonicalPath !== path)
8276
8306
  throw new Error(`Managed global agent path changed: ${path}`);
8277
- const descriptor = openSync2(canonicalPath, "wx", 384);
8307
+ const descriptor = openSync3(canonicalPath, "wx", 384);
8278
8308
  try {
8279
8309
  writeFileSync2(descriptor, content);
8280
8310
  } finally {
8281
- closeSync2(descriptor);
8311
+ closeSync3(descriptor);
8282
8312
  }
8283
8313
  }
8284
8314
  function replaceFile(path, content) {
@@ -8297,7 +8327,7 @@ function replaceFile(path, content) {
8297
8327
  }
8298
8328
  function writeManagedAgents(input) {
8299
8329
  const configRoot = secureCanonicalPath(input.configRoot, "OpenCode config root");
8300
- const agentsDirectory = secureCanonicalPath(join8(configRoot, "agents"), "OpenCode agents path");
8330
+ const agentsDirectory = secureCanonicalPath(join9(configRoot, "agents"), "OpenCode agents path");
8301
8331
  const result = { created: [], updated: [], unchanged: [], conflicts: [], removed: [] };
8302
8332
  const writes = [];
8303
8333
  const removals = new Map;
@@ -8316,21 +8346,21 @@ function writeManagedAgents(input) {
8316
8346
  for (const entry of readdirSync4(agentsDirectory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
8317
8347
  if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
8318
8348
  continue;
8319
- const path = join8(agentsDirectory, entry.name);
8349
+ const path = join9(agentsDirectory, entry.name);
8320
8350
  const currentStat = stat(path);
8321
8351
  if (!currentStat || currentStat.isSymbolicLink() || !currentStat.isFile())
8322
8352
  continue;
8323
- if (hasGeneratedAgentMarker(readFileSync6(path, "utf8"))) {
8353
+ if (hasGeneratedAgentMarker(readFileSync7(path, "utf8"))) {
8324
8354
  assertWriteable2(path, "Managed global agent");
8325
8355
  result.removed.push(path);
8326
- removals.set(path, readFileSync6(path, "utf8"));
8356
+ removals.set(path, readFileSync7(path, "utf8"));
8327
8357
  }
8328
8358
  }
8329
8359
  }
8330
8360
  for (const [id, agent] of Object.entries(input.agents).sort(([left], [right]) => left.localeCompare(right))) {
8331
8361
  if (agent.disabled)
8332
8362
  continue;
8333
- const path = join8(agentsDirectory, `${id}.md`);
8363
+ const path = join9(agentsDirectory, `${id}.md`);
8334
8364
  const content = renderAgent(agent);
8335
8365
  const currentStat = stat(path);
8336
8366
  if (!currentStat) {
@@ -8342,7 +8372,7 @@ function writeManagedAgents(input) {
8342
8372
  result.conflicts.push(path);
8343
8373
  continue;
8344
8374
  }
8345
- const current = readFileSync6(path, "utf8");
8375
+ const current = readFileSync7(path, "utf8");
8346
8376
  if (!hasGeneratedAgentMarker(current)) {
8347
8377
  result.conflicts.push(path);
8348
8378
  continue;
@@ -8380,7 +8410,7 @@ function writeManagedAgents(input) {
8380
8410
  if (!currentStat || currentStat.isSymbolicLink() || !currentStat.isFile()) {
8381
8411
  throw new Error(`Refusing to remove a changed or unmanaged global agent: ${path}`);
8382
8412
  }
8383
- const current = readFileSync6(path, "utf8");
8413
+ const current = readFileSync7(path, "utf8");
8384
8414
  if (!hasGeneratedAgentMarker(current) || current !== removals.get(path)) {
8385
8415
  throw new Error(`Refusing to remove a concurrently changed global agent: ${path}`);
8386
8416
  }
@@ -8389,7 +8419,7 @@ function writeManagedAgents(input) {
8389
8419
  for (const write of writes) {
8390
8420
  if (write.replace) {
8391
8421
  const currentStat = stat(write.path);
8392
- const current = currentStat?.isFile() && !currentStat.isSymbolicLink() ? readFileSync6(write.path, "utf8") : undefined;
8422
+ const current = currentStat?.isFile() && !currentStat.isSymbolicLink() ? readFileSync7(write.path, "utf8") : undefined;
8393
8423
  if (!current || !hasGeneratedAgentMarker(current) || current !== write.previous) {
8394
8424
  throw new Error(`Refusing to replace a concurrently changed global agent: ${write.path}`);
8395
8425
  }
@@ -8402,8 +8432,30 @@ function writeManagedAgents(input) {
8402
8432
 
8403
8433
  // src/file-lock.ts
8404
8434
  import { randomUUID as randomUUID3 } from "node:crypto";
8405
- import { accessSync as accessSync3, closeSync as closeSync3, constants, existsSync as existsSync7, fstatSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync3, readFileSync as readFileSync7, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
8435
+ import { accessSync as accessSync3, closeSync as closeSync4, constants, existsSync as existsSync7, fstatSync, lstatSync as lstatSync7, mkdirSync as mkdirSync3, openSync as openSync4, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "node:fs";
8406
8436
  import { dirname as dirname5 } from "node:path";
8437
+ var STALE_LOCK_AGE_MS = 30 * 60 * 1000;
8438
+ function lockOwnerAlive(path) {
8439
+ let contents;
8440
+ try {
8441
+ contents = readFileSync8(path, "utf8");
8442
+ } catch {
8443
+ return true;
8444
+ }
8445
+ const pid = Number(contents.split(":")[0]);
8446
+ if (!Number.isInteger(pid) || pid <= 0)
8447
+ return false;
8448
+ if (pid === process.pid)
8449
+ return true;
8450
+ try {
8451
+ process.kill(pid, 0);
8452
+ return true;
8453
+ } catch (error) {
8454
+ if (error.code === "ESRCH")
8455
+ return false;
8456
+ return true;
8457
+ }
8458
+ }
8407
8459
  function assertSecure(path) {
8408
8460
  const stat = lstatSync7(path);
8409
8461
  if (!stat.isDirectory() || stat.isSymbolicLink())
@@ -8425,22 +8477,48 @@ function acquire(path, operation) {
8425
8477
  `;
8426
8478
  let descriptor;
8427
8479
  try {
8428
- descriptor = openSync3(canonicalPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
8480
+ descriptor = openSync4(canonicalPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
8429
8481
  } catch (error) {
8430
- if (error.code === "EEXIST") {
8482
+ if (error.code !== "EEXIST")
8483
+ throw error;
8484
+ if (removeStaleLock(canonicalPath)) {
8485
+ descriptor = openSync4(canonicalPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
8486
+ } else {
8431
8487
  throw new Error(`Another Gvozd ${operation} is active or left a lock at ${canonicalPath}; inspect it manually and do not delete it while work may be running`);
8432
8488
  }
8433
- throw error;
8434
8489
  }
8435
8490
  const created = fstatSync(descriptor);
8436
8491
  writeFileSync3(descriptor, nonce);
8437
8492
  return { path: canonicalPath, descriptor, nonce, dev: created.dev, ino: created.ino };
8438
8493
  }
8494
+ function removeStaleLock(path) {
8495
+ let stat;
8496
+ try {
8497
+ stat = lstatSync7(path);
8498
+ } catch {
8499
+ return false;
8500
+ }
8501
+ if (!stat.isFile() || stat.isSymbolicLink())
8502
+ return false;
8503
+ if (Date.now() - stat.mtimeMs < STALE_LOCK_AGE_MS)
8504
+ return false;
8505
+ if (lockOwnerAlive(path))
8506
+ return false;
8507
+ try {
8508
+ const current = lstatSync7(path);
8509
+ if (current.isSymbolicLink() || !current.isFile() || current.ino !== stat.ino || current.dev !== stat.dev)
8510
+ return false;
8511
+ unlinkSync3(path);
8512
+ return true;
8513
+ } catch {
8514
+ return false;
8515
+ }
8516
+ }
8439
8517
  function release(lock) {
8440
- closeSync3(lock.descriptor);
8518
+ closeSync4(lock.descriptor);
8441
8519
  if (existsSync7(lock.path)) {
8442
8520
  const current = lstatSync7(lock.path);
8443
- if (current.isFile() && !current.isSymbolicLink() && current.dev === lock.dev && current.ino === lock.ino && readFileSync7(lock.path, "utf8") === lock.nonce)
8521
+ if (current.isFile() && !current.isSymbolicLink() && current.dev === lock.dev && current.ino === lock.ino && readFileSync8(lock.path, "utf8") === lock.nonce)
8444
8522
  unlinkSync3(lock.path);
8445
8523
  }
8446
8524
  }
@@ -8470,7 +8548,7 @@ function assertVersion(version) {
8470
8548
  async function selectProfile(input, client, configRoot) {
8471
8549
  const catalog = parseModels(await client.models());
8472
8550
  const config = loadConfig(input.cwd, { configRoot, includeProject: false });
8473
- const hasGlobalConfig = existsSync8(join9(configRoot, "gvozd", "config.jsonc"));
8551
+ const hasGlobalConfig = existsSync8(join10(configRoot, "gvozd", "config.jsonc"));
8474
8552
  return chooseModelProfile({
8475
8553
  catalog,
8476
8554
  ui: input.ui,
@@ -8522,8 +8600,8 @@ async function runSetup(input) {
8522
8600
  const runtimeConfigRoot = input.runtimeConfigRoot ?? resolveOpenCodeConfigRoot();
8523
8601
  assertVersion(await client.version());
8524
8602
  const before = loadConfig(input.cwd, { configRoot, includeProject: false });
8525
- const schemaSource = join9(dirname6(before.sources[0]), "schema.json");
8526
- const packagedSchema = readFileSync8(schemaSource, "utf8");
8603
+ const schemaSource = join10(dirname6(before.sources[0]), "schema.json");
8604
+ const packagedSchema = readFileSync9(schemaSource, "utf8");
8527
8605
  const previewSnapshot = preflightGlobalConfig(configRoot, packagedSchema);
8528
8606
  const preview = writeManagedAgents({ configRoot, agents: before.agents, check: true });
8529
8607
  const profile = await selectProfile(input, client, configRoot);
@@ -8531,17 +8609,17 @@ async function runSetup(input) {
8531
8609
  return { status: "cancelled" };
8532
8610
  input.output?.([
8533
8611
  `Register ${PACKAGE_SPEC}`,
8534
- `Write ${join9(configRoot, "gvozd", "config.jsonc")}`,
8535
- `Write managed agents in ${join9(configRoot, "agents")}`,
8612
+ `Write ${join10(configRoot, "gvozd", "config.jsonc")}`,
8613
+ `Write managed agents in ${join10(configRoot, "agents")}`,
8536
8614
  ...preview.removed.length > 0 ? [`Remove ${preview.removed.length} stale or disabled managed agent(s)`] : []
8537
8615
  ].join(`
8538
8616
  `));
8539
8617
  if (!await confirm2(input, "Run setup?"))
8540
8618
  return { status: "cancelled" };
8541
- return withExclusiveFileLock(join9(configRoot, "gvozd", "setup.lock"), async () => {
8619
+ return withExclusiveFileLock(join10(configRoot, "gvozd", "setup.lock"), async () => {
8542
8620
  if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
8543
8621
  throw new Error("OpenCode config root changed while setup awaited the lock");
8544
- const lockedSchema = readFileSync8(schemaSource, "utf8");
8622
+ const lockedSchema = readFileSync9(schemaSource, "utf8");
8545
8623
  const snapshot = preflightGlobalConfig(configRoot, lockedSchema);
8546
8624
  if (!sameSnapshot(previewSnapshot, snapshot))
8547
8625
  throw new Error("Global Gvozd configuration changed while setup awaited confirmation; review and rerun setup");
@@ -8582,16 +8660,16 @@ async function runConfigure(input) {
8582
8660
  const runtimeConfigRoot = input.runtimeConfigRoot ?? resolveOpenCodeConfigRoot();
8583
8661
  assertVersion(await client.version());
8584
8662
  const config = loadConfig(input.cwd, { configRoot, includeProject: false });
8585
- const schemaSource = join9(dirname6(config.sources[0]), "schema.json");
8586
- const packagedSchema = readFileSync8(schemaSource, "utf8");
8663
+ const schemaSource = join10(dirname6(config.sources[0]), "schema.json");
8664
+ const packagedSchema = readFileSync9(schemaSource, "utf8");
8587
8665
  const previewSnapshot = preflightGlobalConfig(configRoot, packagedSchema);
8588
8666
  const profile = await selectProfile(input, client, configRoot);
8589
8667
  if (!profile || !await confirm2(input, "Apply model configuration?"))
8590
8668
  return { status: "cancelled" };
8591
- return withExclusiveFileLock(join9(configRoot, "gvozd", "setup.lock"), async () => {
8669
+ return withExclusiveFileLock(join10(configRoot, "gvozd", "setup.lock"), async () => {
8592
8670
  if (secureCanonicalPath(configRoot, "OpenCode config root") !== configRoot)
8593
8671
  throw new Error("OpenCode config root changed while configuration awaited the lock");
8594
- const lockedSchema = readFileSync8(schemaSource, "utf8");
8672
+ const lockedSchema = readFileSync9(schemaSource, "utf8");
8595
8673
  const snapshot = preflightGlobalConfig(configRoot, lockedSchema);
8596
8674
  if (!sameSnapshot(previewSnapshot, snapshot))
8597
8675
  throw new Error("Global Gvozd configuration changed while configuration awaited confirmation; review and rerun");
@@ -8615,13 +8693,13 @@ function setupExitCode(result) {
8615
8693
  }
8616
8694
 
8617
8695
  // src/sync.ts
8618
- import { closeSync as closeSync4, lstatSync as lstatSync8, mkdirSync as mkdirSync4, openSync as openSync4, readFileSync as readFileSync9, readdirSync as readdirSync5, realpathSync as realpathSync4, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
8696
+ import { closeSync as closeSync5, lstatSync as lstatSync8, mkdirSync as mkdirSync4, openSync as openSync5, readFileSync as readFileSync10, readdirSync as readdirSync5, realpathSync as realpathSync4, renameSync as renameSync3, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "node:fs";
8619
8697
  import { randomUUID as randomUUID4 } from "node:crypto";
8620
- import { basename, dirname as dirname7, join as join10, relative as relative4 } from "node:path";
8698
+ import { basename, dirname as dirname7, join as join11, relative as relative4 } from "node:path";
8621
8699
  function renderPluginEntrypoint(config, destination) {
8622
8700
  let moduleSpecifier = "agent-gvozd/server";
8623
8701
  if (realpathSync4(config.packageRoot) === realpathSync4(config.projectRoot)) {
8624
- moduleSpecifier = relative4(destination, join10(config.packageRoot, "src", "index")).replaceAll("\\", "/");
8702
+ moduleSpecifier = relative4(destination, join11(config.packageRoot, "src", "index")).replaceAll("\\", "/");
8625
8703
  if (!moduleSpecifier.startsWith("."))
8626
8704
  moduleSpecifier = `./${moduleSpecifier}`;
8627
8705
  }
@@ -8666,7 +8744,7 @@ function stat2(path) {
8666
8744
  function safeDirectory(root, segments, create) {
8667
8745
  let current = realpathSync4(root);
8668
8746
  for (const segment of segments) {
8669
- current = join10(current, segment);
8747
+ current = join11(current, segment);
8670
8748
  const currentStat = stat2(current);
8671
8749
  if (currentStat) {
8672
8750
  if (currentStat.isSymbolicLink() || !currentStat.isDirectory()) {
@@ -8688,11 +8766,11 @@ function assertRegularFile(path) {
8688
8766
  return true;
8689
8767
  }
8690
8768
  function createFile2(path, content) {
8691
- const descriptor = openSync4(path, "wx", 384);
8769
+ const descriptor = openSync5(path, "wx", 384);
8692
8770
  try {
8693
8771
  writeFileSync4(descriptor, content);
8694
8772
  } finally {
8695
- closeSync4(descriptor);
8773
+ closeSync5(descriptor);
8696
8774
  }
8697
8775
  }
8698
8776
  function replaceFile2(path, content) {
@@ -8710,7 +8788,7 @@ function replaceFile2(path, content) {
8710
8788
  function planProjectTemplate(config, result, check) {
8711
8789
  const directory = safeDirectory(config.projectRoot, ["docs", ".gvozd"], !check);
8712
8790
  const writes = [];
8713
- const configPath = join10(directory, "config.jsonc");
8791
+ const configPath = join11(directory, "config.jsonc");
8714
8792
  if (!assertRegularFile(configPath)) {
8715
8793
  result.created.push(configPath);
8716
8794
  writes.push({
@@ -8727,14 +8805,14 @@ function planProjectTemplate(config, result, check) {
8727
8805
  `)
8728
8806
  });
8729
8807
  }
8730
- const schemaSource = join10(dirname7(config.sources[0]), "schema.json");
8731
- const schemaTarget = join10(directory, "schema.json");
8732
- const schema = readFileSync9(schemaSource, "utf8");
8808
+ const schemaSource = join11(dirname7(config.sources[0]), "schema.json");
8809
+ const schemaTarget = join11(directory, "schema.json");
8810
+ const schema = readFileSync10(schemaSource, "utf8");
8733
8811
  if (!assertRegularFile(schemaTarget)) {
8734
8812
  result.created.push(schemaTarget);
8735
8813
  writes.push({ target: schemaTarget, content: schema, replace: false });
8736
8814
  } else {
8737
- const current = readFileSync9(schemaTarget, "utf8");
8815
+ const current = readFileSync10(schemaTarget, "utf8");
8738
8816
  if (current !== schema) {
8739
8817
  if (!hasGeneratedSchemaMarker(current) && !isEquivalentLegacySchema(current, schema)) {
8740
8818
  throw new Error(`Refusing to overwrite an unmanaged project schema: ${schemaTarget}`);
@@ -8759,8 +8837,8 @@ function syncAgentsUnlocked(config, options) {
8759
8837
  for (const entry of readdirSync5(destination, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
8760
8838
  if (!entry.isFile() || !entry.name.endsWith(".md") || enabled.has(entry.name))
8761
8839
  continue;
8762
- const target = join10(destination, entry.name);
8763
- const current = readFileSync9(target, "utf8");
8840
+ const target = join11(destination, entry.name);
8841
+ const current = readFileSync10(target, "utf8");
8764
8842
  if (!hasGeneratedAgentMarker(current))
8765
8843
  continue;
8766
8844
  result.removed.push(target);
@@ -8771,14 +8849,14 @@ function syncAgentsUnlocked(config, options) {
8771
8849
  for (const [id, agent] of Object.entries(config.agents)) {
8772
8850
  if (agent.disabled)
8773
8851
  continue;
8774
- const target = join10(destination, `${id}.md`);
8852
+ const target = join11(destination, `${id}.md`);
8775
8853
  const content = renderAgent(agent);
8776
8854
  if (!assertRegularFile(target)) {
8777
8855
  result.created.push(target);
8778
8856
  writes.push({ target, content, replace: false });
8779
8857
  continue;
8780
8858
  }
8781
- const current = readFileSync9(target, "utf8");
8859
+ const current = readFileSync10(target, "utf8");
8782
8860
  if (current === content) {
8783
8861
  result.unchanged.push(target);
8784
8862
  continue;
@@ -8790,11 +8868,11 @@ function syncAgentsUnlocked(config, options) {
8790
8868
  options.onDiff?.(renderDiff(target, current, content));
8791
8869
  writes.push({ target, content, replace: true, previous: current });
8792
8870
  }
8793
- const pluginTarget = join10(pluginDestination, "index.ts");
8871
+ const pluginTarget = join11(pluginDestination, "index.ts");
8794
8872
  const pluginContent = renderPluginEntrypoint(config, pluginDestination);
8795
8873
  if (!options.devPlugin) {
8796
8874
  if (assertRegularFile(pluginTarget)) {
8797
- const current = readFileSync9(pluginTarget, "utf8");
8875
+ const current = readFileSync10(pluginTarget, "utf8");
8798
8876
  if (hasGeneratedPluginMarker(current)) {
8799
8877
  result.removed.push(pluginTarget);
8800
8878
  removals.set(pluginTarget, current);
@@ -8805,7 +8883,7 @@ function syncAgentsUnlocked(config, options) {
8805
8883
  result.created.push(pluginTarget);
8806
8884
  pluginWrite = { target: pluginTarget, content: pluginContent, replace: false };
8807
8885
  } else {
8808
- const current = readFileSync9(pluginTarget, "utf8");
8886
+ const current = readFileSync10(pluginTarget, "utf8");
8809
8887
  if (current === pluginContent) {
8810
8888
  result.unchanged.push(pluginTarget);
8811
8889
  } else {
@@ -8824,7 +8902,7 @@ function syncAgentsUnlocked(config, options) {
8824
8902
  if (!assertRegularFile(target)) {
8825
8903
  throw new Error(`Refusing to remove a changed or unsafe generated file: ${target}`);
8826
8904
  }
8827
- const current = readFileSync9(target, "utf8");
8905
+ const current = readFileSync10(target, "utf8");
8828
8906
  const generated = target.endsWith("index.ts") ? hasGeneratedPluginMarker(current) : hasGeneratedAgentMarker(current);
8829
8907
  if (!generated || current !== removals.get(target)) {
8830
8908
  throw new Error(`Refusing to remove a concurrently changed generated file: ${target}`);
@@ -8833,27 +8911,27 @@ function syncAgentsUnlocked(config, options) {
8833
8911
  }
8834
8912
  for (const write of writes) {
8835
8913
  if (!write.replace) {
8836
- createFile2(join10(writableDestination, basename(write.target)), write.content);
8914
+ createFile2(join11(writableDestination, basename(write.target)), write.content);
8837
8915
  continue;
8838
8916
  }
8839
8917
  if (!assertRegularFile(write.target)) {
8840
8918
  throw new Error(`Refusing to replace a changed or unsafe agent file: ${write.target}`);
8841
8919
  }
8842
- const current = readFileSync9(write.target, "utf8");
8920
+ const current = readFileSync10(write.target, "utf8");
8843
8921
  if (!hasGeneratedAgentMarker(current) || current !== write.previous) {
8844
8922
  throw new Error(`Refusing to replace a concurrently changed agent file: ${write.target}`);
8845
8923
  }
8846
8924
  replaceFile2(write.target, write.content);
8847
8925
  }
8848
8926
  if (pluginWrite) {
8849
- const target = join10(writablePluginDestination, basename(pluginWrite.target));
8927
+ const target = join11(writablePluginDestination, basename(pluginWrite.target));
8850
8928
  if (!pluginWrite.replace) {
8851
8929
  createFile2(target, pluginWrite.content);
8852
8930
  } else {
8853
8931
  if (!assertRegularFile(target)) {
8854
8932
  throw new Error(`Refusing to replace a changed or unsafe plugin entrypoint: ${target}`);
8855
8933
  }
8856
- const current = readFileSync9(target, "utf8");
8934
+ const current = readFileSync10(target, "utf8");
8857
8935
  if (!hasGeneratedPluginMarker(current) || current !== pluginWrite.previous) {
8858
8936
  throw new Error(`Refusing to replace a concurrently changed plugin entrypoint: ${target}`);
8859
8937
  }
@@ -8862,11 +8940,11 @@ function syncAgentsUnlocked(config, options) {
8862
8940
  }
8863
8941
  const templateDirectory = safeDirectory(config.projectRoot, ["docs", ".gvozd"], true);
8864
8942
  for (const write of templateWrites) {
8865
- const target = join10(templateDirectory, basename(write.target));
8943
+ const target = join11(templateDirectory, basename(write.target));
8866
8944
  if (!write.replace)
8867
8945
  createFile2(target, write.content);
8868
8946
  else {
8869
- if (!assertRegularFile(target) || readFileSync9(target, "utf8") !== write.previous) {
8947
+ if (!assertRegularFile(target) || readFileSync10(target, "utf8") !== write.previous) {
8870
8948
  throw new Error(`Refusing to replace a concurrently changed project schema: ${target}`);
8871
8949
  }
8872
8950
  replaceFile2(target, write.content);
@@ -8881,7 +8959,7 @@ function syncAgents(config, options = {}) {
8881
8959
  if (options.check)
8882
8960
  return syncAgentsUnlocked(config, options);
8883
8961
  const root = realpathSync4(config.projectRoot);
8884
- return withExclusiveFileLockSync(join10(root, ".agent-gvozd-sync.lock"), () => syncAgentsUnlocked(config, options), "sync");
8962
+ return withExclusiveFileLockSync(join11(root, ".agent-gvozd-sync.lock"), () => syncAgentsUnlocked(config, options), "sync");
8885
8963
  }
8886
8964
  function formatSyncResult(result, check) {
8887
8965
  const lines = [check ? "agent-gvozd sync check" : "agent-gvozd sync complete"];
@@ -9017,10 +9095,12 @@ async function runCli(args, io = defaultIO, commands = { setup: runSetup, config
9017
9095
  if (agentID === undefined || agentID.startsWith("--") || rest.length > 2)
9018
9096
  return usage(io);
9019
9097
  const configRoot = resolveOpenCodeConfigRoot();
9020
- preflightGlobalConfig(configRoot);
9021
- const snapshot2 = snapshot(join11(configRoot, "gvozd", "config.jsonc"));
9022
- const next = setAgentDisabled(readGlobalConfig(configRoot), agentID, action === "disable");
9023
- writeManagedGlobalFile(join11(configRoot, "gvozd", "config.jsonc"), next, snapshot2);
9098
+ await withExclusiveFileLock(join12(configRoot, "gvozd", "setup.lock"), async () => {
9099
+ preflightGlobalConfig(configRoot);
9100
+ const snapshot2 = snapshot(join12(configRoot, "gvozd", "config.jsonc"));
9101
+ const next = setAgentDisabled(readGlobalConfig(configRoot), agentID, action === "disable");
9102
+ writeManagedGlobalFile(join12(configRoot, "gvozd", "config.jsonc"), next, snapshot2);
9103
+ });
9024
9104
  io.stdout(`${action}d ${agentID}; run gvozd sync and gvozd doctor to apply`);
9025
9105
  return 0;
9026
9106
  }
@@ -9040,8 +9120,13 @@ async function runCli(args, io = defaultIO, commands = { setup: runSetup, config
9040
9120
  }
9041
9121
  }
9042
9122
  var invokedPath = process.argv[1];
9043
- if (invokedPath && realpathSync5(invokedPath) === realpathSync5(fileURLToPath2(import.meta.url))) {
9044
- process.exitCode = await runCli(process.argv.slice(2));
9123
+ try {
9124
+ if (invokedPath && realpathSync5(invokedPath) === realpathSync5(fileURLToPath2(import.meta.url))) {
9125
+ process.exitCode = await runCli(process.argv.slice(2));
9126
+ }
9127
+ } catch (error) {
9128
+ if (error.code !== "ENOENT")
9129
+ throw error;
9045
9130
  }
9046
9131
  export {
9047
9132
  runCli
package/dist/index.js CHANGED
@@ -78,16 +78,27 @@ function normalizeMcpName(name) {
78
78
  return name.replaceAll(/[^A-Za-z0-9_-]/g, "_");
79
79
  }
80
80
  function wildcardMatch(pattern, value) {
81
- let source = "^";
82
- for (const character of pattern) {
83
- if (character === "*")
84
- source += ".*";
85
- else if (character === "?")
86
- source += ".";
87
- else
88
- source += character.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
81
+ let patternIndex = 0;
82
+ let valueIndex = 0;
83
+ let starPatternIndex = -1;
84
+ let restartValueIndex = 0;
85
+ while (valueIndex < value.length) {
86
+ if (patternIndex < pattern.length && pattern[patternIndex] === "*") {
87
+ starPatternIndex = patternIndex++;
88
+ restartValueIndex = valueIndex;
89
+ } else if (patternIndex < pattern.length && (pattern[patternIndex] === "?" || pattern[patternIndex] === value[valueIndex])) {
90
+ patternIndex++;
91
+ valueIndex++;
92
+ } else if (starPatternIndex >= 0) {
93
+ patternIndex = starPatternIndex + 1;
94
+ valueIndex = ++restartValueIndex;
95
+ } else {
96
+ return false;
97
+ }
89
98
  }
90
- return new RegExp(`${source}$`).test(value);
99
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*")
100
+ patternIndex++;
101
+ return patternIndex === pattern.length;
91
102
  }
92
103
  function explicitMcpAccess(agent, action, resources) {
93
104
  if (resources.length === 0)
@@ -6455,25 +6466,13 @@ var GvozdLeases = Rpc.define({
6455
6466
  function evaluateEffect(rules, action, resource) {
6456
6467
  let matched;
6457
6468
  for (const rule of rules) {
6458
- if (match(rule.action, action) && match(rule.resource, resource))
6469
+ if (wildcardMatch(rule.action, action) && wildcardMatch(rule.resource, resource))
6459
6470
  matched = rule;
6460
6471
  }
6461
6472
  if (!matched)
6462
6473
  return { effect: "unknown", matchedRule: null };
6463
6474
  return { effect: matched.effect, matchedRule: `${matched.action} ${matched.resource}` };
6464
6475
  }
6465
- function match(pattern, value) {
6466
- let source = "^";
6467
- for (const character of pattern) {
6468
- if (character === "*")
6469
- source += ".*";
6470
- else if (character === "?")
6471
- source += ".";
6472
- else
6473
- source += character.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
6474
- }
6475
- return new RegExp(`${source}$`).test(value);
6476
- }
6477
6476
  function evaluateInput(agentRules, input) {
6478
6477
  const results = input.checks.flatMap((request) => (request.resources.length > 0 ? request.resources : ["*"]).map((resource) => {
6479
6478
  const evaluation = agentRules ? evaluateEffect(agentRules, request.action, resource) : { effect: "unknown", matchedRule: null };
@@ -6544,7 +6543,9 @@ function modePermissions(mode) {
6544
6543
  { action: "shell", resource: "git filter-repo*", effect: "deny" },
6545
6544
  { action: "shell", resource: "git rebase*", effect: "deny" },
6546
6545
  { action: "shell", resource: "git checkout --*", effect: "deny" },
6547
- { action: "shell", resource: "git restore*", effect: "deny" }
6546
+ { action: "shell", resource: "git restore*", effect: "deny" },
6547
+ { action: "shell", resource: "git rebase --abort*", effect: "allow" },
6548
+ { action: "shell", resource: "git rebase --continue*", effect: "allow" }
6548
6549
  ];
6549
6550
  }
6550
6551
  if (mode === "strict") {
@@ -6645,6 +6646,8 @@ function enforceFileLeasePermission(event, config, manager) {
6645
6646
  if (event.action !== "shell" && event.action !== "bash")
6646
6647
  return false;
6647
6648
  if (role === "coordinator") {
6649
+ if (!manager.hasActiveLeases())
6650
+ return false;
6648
6651
  return deny(event, `Agent ${event.agent} cannot use shell while file leases enforce structured mutations`);
6649
6652
  }
6650
6653
  if (role === "writer" && safeReadonlyShell(event.agent, event.resources)) {
package/dist/tui.js CHANGED
@@ -9,6 +9,71 @@ import { usePlugin } from "@opencode/plugin/tui";
9
9
 
10
10
  // src/permissions-rpc.ts
11
11
  import { Rpc } from "@opencode/plugin/rpc";
12
+
13
+ // src/tool-permissions.ts
14
+ function family(command, ...variants) {
15
+ return [command, ...variants].map((entry) => ({
16
+ exact: entry,
17
+ wildcard: `${entry} *`
18
+ }));
19
+ }
20
+ function exactOnly(command, ...variants) {
21
+ return [command, ...variants].map((entry) => ({ exact: entry, wildcard: entry }));
22
+ }
23
+ var INSPECTION_COMMANDS = [
24
+ ...family("pwd", "true", "test"),
25
+ ...family("cat", "head", "tail", "wc", "sort", "uniq"),
26
+ ...family("grep", "rg", "find", "diff", "cmp"),
27
+ ...family("ls", "du", "df", "stat", "file", "realpath", "basename", "dirname"),
28
+ ...family("shasum", "sha256sum", "md5sum"),
29
+ ...family("uname", "whoami", "hostname", "date", "printenv"),
30
+ ...family("which", "command -v"),
31
+ ...family("mktemp"),
32
+ ...family("tr", "cut", "paste", "column"),
33
+ ...family("node --version", "python3 --version", "python --version", "deno --version")
34
+ ];
35
+ var TOOLCHAIN_COMMANDS = [
36
+ ...family("bun test", "bun run test", "bun --version"),
37
+ ...family("bun run typecheck", "bun run lint", "bun run build", "bun run check"),
38
+ ...family("tsc --noEmit", "npx tsc --noEmit"),
39
+ ...family("eslint", "biome check", "prettier --check"),
40
+ ...family("npm test", "npm run test", "npm run typecheck", "npm run lint", "npm run build"),
41
+ ...family("pnpm test", "pnpm run test", "pnpm run build"),
42
+ ...family("yarn test", "yarn build"),
43
+ ...family("vitest run", "jest", "playwright test"),
44
+ ...family("cargo check", "cargo test", "cargo build", "cargo clippy", "cargo fmt --check", "cargo --version"),
45
+ ...family("go build ./...", "go test ./...", "go vet ./...", "go version"),
46
+ ...family("pytest", "python3 -m pytest", "python -m pytest"),
47
+ ...family("ruff check", "mypy", "pyright"),
48
+ ...family("mvn test", "mvn verify", "gradle test", "gradle check", "./gradlew test", "./gradlew check"),
49
+ ...family("make test", "make check", "make build", "make --version"),
50
+ ...family("just --list")
51
+ ];
52
+ var GIT_READONLY_COMMANDS = [
53
+ ...family("git status", "git status --short", "git status --short --branch", "git status --porcelain", "git status --porcelain=v1 --branch"),
54
+ ...family("git diff", "git diff --stat", "git diff --cached", "git diff --check"),
55
+ ...family("git log", "git show"),
56
+ ...family("git rev-parse", "git rev-list", "git show-ref", "git cat-file"),
57
+ ...exactOnly("git symbolic-ref HEAD", "git symbolic-ref --short HEAD"),
58
+ ...family("git ls-files", "git ls-remote", "git grep"),
59
+ ...exactOnly("git branch", "git tag", "git remote", "git reflog"),
60
+ ...family("git branch --list", "git branch -l", "git branch -a", "git branch -r", "git branch -v", "git branch -vv", "git branch --all", "git branch --remotes", "git branch --show-current", "git branch --contains"),
61
+ ...family("git tag --list", "git tag -l", "git tag -n"),
62
+ ...family("git remote -v", "git remote --verbose", "git remote show", "git remote get-url"),
63
+ ...family("git reflog show"),
64
+ ...family("git stash list", "git describe", "git worktree list"),
65
+ ...family("git config --get", "git config --get-regexp")
66
+ ];
67
+ var GIT_MUTATING_COMMANDS = [
68
+ ...family("git add", "git rm --cached"),
69
+ ...family("git commit", "git merge --ff-only", "git merge --no-ff"),
70
+ ...family("git push", "git fetch", "git pull --ff-only"),
71
+ ...family("git stash", "git cherry-pick", "git revert"),
72
+ ...family("git switch", "git checkout -b", "git worktree add"),
73
+ ...exactOnly("git branch *", "git tag *", "git remote *", "git symbolic-ref *", "git reflog *")
74
+ ];
75
+
76
+ // src/permissions-rpc.ts
12
77
  var GvozdPermissions = Rpc.define({
13
78
  id: "gvozd-permissions",
14
79
  events: {},
@@ -358,9 +423,9 @@ function useSessionInsights(sessionID) {
358
423
  });
359
424
  const [resource] = createResource(() => {
360
425
  const id = sessionID();
361
- version();
362
- return id;
363
- }, async (id) => {
426
+ return { id, revision: version() };
427
+ }, async (key) => {
428
+ const id = key.id;
364
429
  if (!id)
365
430
  return EMPTY_INSIGHTS;
366
431
  const [messages, pending] = await Promise.allSettled([
@@ -429,38 +494,114 @@ async function evaluatePermissions(agent, checks) {
429
494
  function splitCommandPipeline(input) {
430
495
  const segments = [];
431
496
  let current = "";
432
- let quote;
433
497
  let escaped = false;
498
+ const contexts = [{ parenDepth: 0 }];
499
+ let heredoc;
434
500
  const flush = () => {
435
501
  const trimmed = current.trim();
436
502
  if (trimmed)
437
503
  segments.push(trimmed);
438
504
  current = "";
439
505
  };
506
+ const at = (index) => input[index] ?? "";
440
507
  for (let index = 0;index < input.length; index++) {
441
508
  const character = input[index];
509
+ if (heredoc) {
510
+ if (character === `
511
+ `) {
512
+ const line = current.slice(current.lastIndexOf(`
513
+ `) + 1).replace(/\r$/, "");
514
+ const candidate = heredoc.stripTabs ? line.replace(/^\t+/, "") : line;
515
+ if (candidate === heredoc.marker) {
516
+ heredoc = undefined;
517
+ current += character;
518
+ flush();
519
+ continue;
520
+ }
521
+ }
522
+ current += character;
523
+ continue;
524
+ }
525
+ const context = contexts[contexts.length - 1];
442
526
  if (escaped) {
443
527
  current += character;
444
528
  escaped = false;
445
529
  continue;
446
530
  }
447
- if (character === "\\" && quote !== "'") {
531
+ if (character === "\\" && context.quote !== "'") {
448
532
  current += character;
449
533
  escaped = true;
450
534
  continue;
451
535
  }
452
536
  if (character === '"' || character === "'") {
453
- if (quote === character)
454
- quote = undefined;
455
- else if (!quote)
456
- quote = character;
537
+ if (context.quote === character)
538
+ context.quote = undefined;
539
+ else if (!context.quote)
540
+ context.quote = character;
541
+ current += character;
542
+ continue;
543
+ }
544
+ if (context.quote === "'") {
545
+ current += character;
546
+ continue;
547
+ }
548
+ if (character === "$" && at(index + 1) === "(") {
549
+ contexts.push({ closer: ")", parenDepth: 0 });
550
+ current += "$(";
551
+ index++;
552
+ continue;
553
+ }
554
+ if (character === "`") {
555
+ if (context.closer === "`")
556
+ contexts.pop();
557
+ else
558
+ contexts.push({ closer: "`", parenDepth: 0 });
559
+ current += character;
560
+ continue;
561
+ }
562
+ if (context.closer === ")" && character === "(") {
563
+ context.parenDepth++;
564
+ current += character;
565
+ continue;
566
+ }
567
+ if (context.closer === ")" && character === ")") {
568
+ if (context.parenDepth > 0)
569
+ context.parenDepth--;
570
+ else
571
+ contexts.pop();
457
572
  current += character;
458
573
  continue;
459
574
  }
460
- if (quote) {
575
+ if (contexts.length > 1 || context.quote) {
461
576
  current += character;
462
577
  continue;
463
578
  }
579
+ if (character === "<" && at(index - 1) !== "<" && at(index + 1) === "<" && at(index + 2) !== "<") {
580
+ let cursor = index + 2;
581
+ const stripTabs = input[cursor] === "-";
582
+ if (stripTabs)
583
+ cursor++;
584
+ let marker = "";
585
+ while (cursor < input.length && /[ \t]/.test(input[cursor]))
586
+ cursor++;
587
+ if (input[cursor] === '"' || input[cursor] === "'") {
588
+ const closeQuote = input[cursor];
589
+ cursor++;
590
+ while (cursor < input.length && input[cursor] !== closeQuote)
591
+ marker += input[cursor++];
592
+ if (input[cursor] === closeQuote)
593
+ cursor++;
594
+ } else {
595
+ while (cursor < input.length && !/[\s;|&<>()]/.test(input[cursor]))
596
+ marker += input[cursor++];
597
+ }
598
+ if (marker) {
599
+ heredoc = { marker, stripTabs };
600
+ current += input.slice(index, cursor);
601
+ index = cursor - 1;
602
+ continue;
603
+ }
604
+ }
464
605
  if (character === ";" || character === `
465
606
  `) {
466
607
  flush();
@@ -624,7 +765,7 @@ function SessionInsightsSlot(props) {
624
765
  function DryRunPanel() {
625
766
  const context = usePlugin2();
626
767
  const [input, setInput] = createSignal2("");
627
- const [agent, setAgent] = createSignal2("master");
768
+ const [agent] = createSignal2("master");
628
769
  const [rows, setRows] = createSignal2([]);
629
770
  const [busy, setBusy] = createSignal2(false);
630
771
  const run = async () => {
@@ -861,6 +1002,29 @@ var MODE_HINTS = {
861
1002
  trusted: "allow all shell and edits; destructive git still denied",
862
1003
  strict: "ask for every shell command and edit"
863
1004
  };
1005
+ function KeymapCommands() {
1006
+ const context = usePlugin2();
1007
+ context.keymap.layer(() => ({
1008
+ mode: "global",
1009
+ commands: GVOZD_COMMANDS.map((command) => ({
1010
+ id: command.id,
1011
+ title: command.title,
1012
+ group: "Gvozd",
1013
+ palette: true,
1014
+ slash: { name: command.slash },
1015
+ run: () => {
1016
+ context.ui.panel.open(command.panel, { presentation: "fullscreen" });
1017
+ }
1018
+ }))
1019
+ }));
1020
+ return null;
1021
+ }
1022
+ var GVOZD_COMMANDS = [
1023
+ { id: "gvozd.insights", title: "Gvozd session insights", panel: "gvozd.insights", slash: "gvozd" },
1024
+ { id: "gvozd.dryrun", title: "Gvozd permission dry-run", panel: "gvozd.dryrun", slash: "gvozd-dryrun" },
1025
+ { id: "gvozd.leases", title: "Gvozd file leases", panel: "gvozd.leases", slash: "gvozd-leases" },
1026
+ { id: "gvozd.mode", title: "Gvozd permission mode", panel: "gvozd.mode", slash: "gvozd-mode" }
1027
+ ];
864
1028
  var tui_default = Plugin.define({
865
1029
  id: "agent-gvozd",
866
1030
  setup(context) {
@@ -876,6 +1040,10 @@ var tui_default = Plugin.define({
876
1040
  sessionID
877
1041
  }, undefined, false, undefined, this) : null
878
1042
  });
1043
+ const unregisterKeymapHost = context.ui.slot({
1044
+ append: "app",
1045
+ render: () => /* @__PURE__ */ jsxDEV(KeymapCommands, {}, undefined, false, undefined, this)
1046
+ });
879
1047
  const unregisterPanelSlot = context.ui.slot({
880
1048
  append: "session.panel",
881
1049
  render: (panel) => /* @__PURE__ */ jsxDEV(Fragment, {
@@ -899,55 +1067,11 @@ var tui_default = Plugin.define({
899
1067
  ]
900
1068
  }, undefined, true, undefined, this)
901
1069
  });
902
- const unregisterKeymap = context.keymap.layer(() => ({
903
- mode: "global",
904
- commands: [
905
- {
906
- id: "gvozd.insights",
907
- title: "Gvozd session insights",
908
- group: "Gvozd",
909
- palette: true,
910
- slash: { name: "gvozd" },
911
- run: () => {
912
- context.ui.panel.open("gvozd.insights", { presentation: "fullscreen" });
913
- }
914
- },
915
- {
916
- id: "gvozd.dryrun",
917
- title: "Gvozd permission dry-run",
918
- group: "Gvozd",
919
- palette: true,
920
- slash: { name: "gvozd-dryrun" },
921
- run: () => {
922
- context.ui.panel.open("gvozd.dryrun", { presentation: "fullscreen" });
923
- }
924
- },
925
- {
926
- id: "gvozd.leases",
927
- title: "Gvozd file leases",
928
- group: "Gvozd",
929
- palette: true,
930
- slash: { name: "gvozd-leases" },
931
- run: () => {
932
- context.ui.panel.open("gvozd.leases", { presentation: "fullscreen" });
933
- }
934
- },
935
- {
936
- id: "gvozd.mode",
937
- title: "Gvozd permission mode",
938
- group: "Gvozd",
939
- palette: true,
940
- slash: { name: "gvozd-mode" },
941
- run: () => {
942
- context.ui.panel.open("gvozd.mode", { presentation: "fullscreen" });
943
- }
944
- }
945
- ]
946
- }));
947
1070
  return () => {
948
1071
  unregisterSidebar();
949
1072
  unregisterFooter();
950
1073
  unregisterPanelSlot();
1074
+ unregisterKeymapHost();
951
1075
  };
952
1076
  }
953
1077
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nail00749/agent-gvozd",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A globally configured, permission-aware agent team for OpenCode V2",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -48,10 +48,10 @@
48
48
  "verify:sync": "bun run ./scripts/verify-sync.ts",
49
49
  "verify:package": "bun run ./scripts/verify-package.ts",
50
50
  "verify:live": "bun run ./scripts/live-opencode-compat.ts",
51
- "prepublishOnly": "bun run test:unit && bun run test:e2e && bun run typecheck && bun run build && bun run verify:sync && bun run verify:package",
51
+ "prepublishOnly": "bun run test:unit && bun run typecheck && bun run build && bun run verify:sync && bun run verify:package && bun run test:e2e",
52
52
  "test:unit": "bun test --path-ignore-patterns \"**/package-smoke.test.ts\"",
53
53
  "test:e2e": "bun test src/cli/package-smoke.test.ts",
54
- "publish:fast": "bun run typecheck && bun run build && bun run verify:package && npm publish --ignore-scripts"
54
+ "publish:fast": "bun run test:unit && bun run typecheck && bun run build && bun run verify:package && npm publish --ignore-scripts"
55
55
  },
56
56
  "dependencies": {
57
57
  "@clack/prompts": "1.8.0",