fellow-agents 0.0.22 → 0.0.28

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.
@@ -1,4 +1,4 @@
1
- import { readPreferences, writePreferences, lookupCli, preferencesFile, KNOWN_KEYS, } from "../lib/preferences.js";
1
+ import { readPreferences, writePreferences, lookupCli, preferencesFile, stripMatchedQuotes, KNOWN_KEYS, } from "../lib/preferences.js";
2
2
  function printHelp() {
3
3
  console.log(`fellow-agents config — read or write user preferences
4
4
 
@@ -75,7 +75,7 @@ function handleSet(args) {
75
75
  process.exit(1);
76
76
  }
77
77
  const key = args[0];
78
- const value = args.slice(1).join(" ");
78
+ const value = stripMatchedQuotes(args.slice(1).join(" ").trim()).trim();
79
79
  if (!isKnownKey(key)) {
80
80
  console.error(`Unknown key: ${key}`);
81
81
  console.error(`Known keys: ${KNOWN_KEYS.join(", ")}`);
@@ -10,7 +10,7 @@ import { startEmcomServer, startPtyWin, stopAll, logPath } from "../lib/services
10
10
  import { scaffoldWorkspaces, registerAgents, writeHooks } from "../lib/workspaces.js";
11
11
  import { installSkills } from "../lib/skills.js";
12
12
  import { binarySuffix } from "../lib/platform.js";
13
- import { readPreferences, writePreferences, autoDetectClis, lookupCli, } from "../lib/preferences.js";
13
+ import { readPreferences, writePreferences, autoDetectClis, lookupCli, stripMatchedQuotes, } from "../lib/preferences.js";
14
14
  // Minimal engines.node range check — handles ">=N", "<N", and combinations.
15
15
  function nodeInRange(version, range) {
16
16
  const major = parseInt(version.split(".")[0], 10);
@@ -63,7 +63,8 @@ async function promptForCliPreference() {
63
63
  return detected[num - 1];
64
64
  }
65
65
  if (!isNaN(num) && num === customIdx) {
66
- const value = (await rl.question(" Enter command or full path: ")).trim();
66
+ const raw = (await rl.question(" Enter command or full path: ")).trim();
67
+ const value = stripMatchedQuotes(raw).trim();
67
68
  if (!value) {
68
69
  console.log(" Empty input — skipped.");
69
70
  return null;
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, unlinkSync } from "fs";
2
2
  import { join } from "path";
3
- import { execSync } from "child_process";
3
+ import { execFileSync } from "child_process";
4
4
  import { dataDir } from "./paths.js";
5
5
  /** ~/.fellow-agents/preferences.json */
6
6
  export const preferencesFile = join(dataDir, "preferences.json");
@@ -42,16 +42,39 @@ export function readPreferences() {
42
42
  return null;
43
43
  }
44
44
  }
45
+ /**
46
+ * Strip a single pair of matched leading/trailing single or double quotes.
47
+ * Defensive helper for user inputs where a shell-style quoted value
48
+ * (`'agency cp'` or `"agency cp"`) gets typed literally — e.g. into a
49
+ * readline prompt or a config-set value after the outer shell has already
50
+ * consumed its own quote layer.
51
+ *
52
+ * Unmatched, mixed, or absent quotes pass through unchanged.
53
+ */
54
+ export function stripMatchedQuotes(s) {
55
+ if (s.length < 2)
56
+ return s;
57
+ const first = s[0];
58
+ const last = s[s.length - 1];
59
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
60
+ return s.slice(1, -1);
61
+ }
62
+ return s;
63
+ }
45
64
  /**
46
65
  * Write preferences atomically (temp + renameSync — atomic on Windows from Node 10+).
47
66
  * Always stamps `updatedAt` to now and `updatedBy` to the supplied value.
48
67
  * Ensures dataDir exists. Cleans up the temp file on failure.
68
+ *
69
+ * Preserves forward-compat keys from `prefs` that aren't in the current
70
+ * Preferences type — callers spreading an existing file with future schema:2
71
+ * fields can rely on those fields surviving a schema:1-aware writer.
49
72
  */
50
73
  export function writePreferences(prefs) {
51
74
  mkdirSync(dataDir, { recursive: true });
52
75
  const next = {
76
+ ...prefs,
53
77
  schema: prefs.schema ?? CURRENT_SCHEMA,
54
- cliPreference: prefs.cliPreference,
55
78
  updatedAt: new Date().toISOString(),
56
79
  updatedBy: prefs.updatedBy,
57
80
  };
@@ -72,12 +95,18 @@ export function writePreferences(prefs) {
72
95
  }
73
96
  /**
74
97
  * Look up a CLI on PATH. Returns the resolved full path (first hit), or null if not found.
75
- * Uses `where.exe` on Windows and `which -a` on Unix. Never throws.
98
+ * Uses `where.exe` on Windows and `which` on Unix. Never throws.
99
+ *
100
+ * Uses execFileSync with shell: false so user-controlled `name` values
101
+ * (e.g. a cliPreference set by config-set) cannot inject shell metacharacters.
102
+ * Values containing spaces or shell-special chars (`;`, `&`, `|`, `$`, etc.) are
103
+ * passed verbatim as a single argv to where.exe/which — which will simply not
104
+ * find a match and return null, rather than execute arbitrary commands.
76
105
  */
77
106
  export function lookupCli(name) {
78
- const cmd = process.platform === "win32" ? `where.exe ${name}` : `which ${name}`;
107
+ const bin = process.platform === "win32" ? "where.exe" : "which";
79
108
  try {
80
- const out = execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] })
109
+ const out = execFileSync(bin, [name], { stdio: ["ignore", "pipe", "ignore"] })
81
110
  .toString()
82
111
  .split(/\r?\n/)
83
112
  .map((s) => s.trim())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fellow-agents",
3
- "version": "0.0.22",
3
+ "version": "0.0.28",
4
4
  "description": "Multi-agent system — multiple Claude Code instances collaborating via messaging",
5
5
  "type": "module",
6
6
  "bin": {