@technomoron/agent-run 0.99.27 → 0.99.29

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/project.js CHANGED
@@ -164,6 +164,11 @@ function profileFromPackage(projectRoot) {
164
164
  if (fs.existsSync(packagePath)) {
165
165
  try {
166
166
  const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
167
+ const repositoryProfile = parsePackageRepositoryProfile(pkg.repository);
168
+ if (repositoryProfile !== null) {
169
+ (0, utils_1.verbose)(`using package.json repository profile=${repositoryProfile}`);
170
+ return parseProfile(repositoryProfile, `${packagePath} repository`);
171
+ }
167
172
  if (typeof pkg.name === 'string' && pkg.name.length > 0) {
168
173
  (0, utils_1.verbose)(`using package.json name=${pkg.name}`);
169
174
  return parseProfile(pkg.name.startsWith('@') ? pkg.name.slice(1) : pkg.name, `${packagePath} name`);
@@ -183,6 +188,28 @@ function profileFromPackage(projectRoot) {
183
188
  }
184
189
  return { profile: null, reason: `cannot resolve agent profile from project path: ${projectRoot}` };
185
190
  }
191
+ function parsePackageRepositoryProfile(repository) {
192
+ let repositoryPath = null;
193
+ if (typeof repository === 'string') {
194
+ repositoryPath = repository;
195
+ }
196
+ else if (repository !== null && typeof repository === 'object' && 'url' in repository) {
197
+ const url = repository.url;
198
+ if (typeof url === 'string') {
199
+ repositoryPath = url;
200
+ }
201
+ }
202
+ if (repositoryPath === null) {
203
+ return null;
204
+ }
205
+ const value = repositoryPath.trim();
206
+ const githubProfile = parseGitHubRemoteProfile(value.replace(/^git\+/, ''));
207
+ if (githubProfile !== null) {
208
+ return githubProfile;
209
+ }
210
+ const shorthand = /^(?:github:)?([^/:\s]+)\/([^/\s]+?)(?:\.git)?$/.exec(value);
211
+ return shorthand?.[1] && shorthand[2] ? `${shorthand[1]}/${shorthand[2]}` : null;
212
+ }
186
213
  function resolveGitHubProfile(projectRoot) {
187
214
  const result = childProcess.spawnSync('git', ['-C', projectRoot, 'config', '--get', 'remote.origin.url'], {
188
215
  encoding: 'utf8',
@@ -271,17 +298,11 @@ function defaultConfigRoot(projectRoot, options = {}) {
271
298
  if (explicit !== null) {
272
299
  return explicit;
273
300
  }
274
- if (projectRoot) {
275
- const projectConfigRoot = defaultCodeConfigRoot(projectRoot);
276
- if (options.preferProjectRoot || fs.existsSync(projectConfigRoot)) {
277
- return projectConfigRoot;
278
- }
279
- return findExistingConfigRoot(defaultConfigRootSearchCandidates()) ?? projectConfigRoot;
280
- }
281
- return findExistingConfigRoot(defaultConfigRootSearchCandidates()) ?? path.join(os.homedir(), '.agent-config');
301
+ void options;
302
+ return path.join(os.homedir(), '.agent-run');
282
303
  }
283
304
  function explicitConfigRoot(projectRoot) {
284
- for (const value of [process.env[constants_1.CONFIG_ROOT_OVERRIDE_ENV], process.env[constants_1.CONFIG_DIR_ENV], process.env.AGENT_CONFIG_ROOT]) {
305
+ for (const value of [process.env[constants_1.CONFIG_ROOT_OVERRIDE_ENV], process.env[constants_1.CONFIG_DIR_ENV]]) {
285
306
  if (value) {
286
307
  return path.resolve(value);
287
308
  }
@@ -290,21 +311,9 @@ function explicitConfigRoot(projectRoot) {
290
311
  return null;
291
312
  }
292
313
  const env = readAgentRunEnv(projectRoot);
293
- const localValue = env[constants_1.CONFIG_DIR_ENV]?.trim() || env.AGENT_CONFIG_ROOT?.trim();
314
+ const localValue = env[constants_1.CONFIG_DIR_ENV]?.trim();
294
315
  return localValue ? path.resolve(projectRoot, localValue) : null;
295
316
  }
296
- function defaultCodeConfigRoot(projectRoot) {
297
- const resolvedProjectRoot = path.resolve(projectRoot);
298
- const ownerDir = path.dirname(resolvedProjectRoot);
299
- const codeRoot = path.dirname(ownerDir);
300
- if (ownerDir === resolvedProjectRoot || codeRoot === ownerDir) {
301
- return path.join(os.homedir(), '.agent-config');
302
- }
303
- return path.join(codeRoot, 'agent-config');
304
- }
305
- function findExistingConfigRoot(candidates) {
306
- return candidates.find((candidate) => fs.existsSync(candidate)) ?? null;
307
- }
308
317
  function defaultConfigRootSearchCandidates(_projectRoot, options = {}) {
309
318
  const platform = options.platform ?? process.platform;
310
319
  const pathApi = platform === 'win32' ? path.win32 : path.posix;
package/dist/renderer.js CHANGED
@@ -51,11 +51,23 @@ function renderAgentSections(env, configRoot, manifest, context, trace) {
51
51
  }
52
52
  sections.push((0, templates_1.renderTemplateFile)(env, configRoot, include, context, trace));
53
53
  }
54
- return sections.map((section, index) => {
54
+ const renderedSections = sections.map((section, index) => {
55
55
  const label = index === 0 ? manifest.agent.base : manifest.agent.includes[index - 1] ?? `section ${index}`;
56
56
  (0, templates_1.assertNoUnexpandedTemplateVars)(label, section);
57
57
  return section.trimEnd();
58
58
  });
59
+ renderedSections.push(projectMemoryInstructions(context));
60
+ return renderedSections;
61
+ }
62
+ function projectMemoryInstructions(context) {
63
+ return [
64
+ '## Project Memory',
65
+ '',
66
+ `Project memory is stored in ${context.paths.projectMemoryDir}.`,
67
+ 'Read README.md there before starting work when prior project context may matter, then read only the linked files relevant to the task.',
68
+ 'Do not store secrets, raw chat transcripts, or temporary task state there.',
69
+ 'Update project memory only when the user explicitly asks you to remember or update something for this project.'
70
+ ].join('\n');
59
71
  }
60
72
  function renderProfileFiles(env, configRoot, context, targetTool, trace) {
61
73
  const files = [];
@@ -101,11 +113,18 @@ function syncAgentProfile(projectRoot, agentDir, options) {
101
113
  }
102
114
  function writeGeneratedFile(filePath, content, executable) {
103
115
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
104
- fs.writeFileSync(filePath, content, 'utf8');
116
+ const unchanged = fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === content;
117
+ if (!unchanged) {
118
+ fs.writeFileSync(filePath, content, 'utf8');
119
+ (0, utils_1.verbose)(`write ${filePath}`);
120
+ }
105
121
  if (executable && !constants_1.IS_WINDOWS) {
106
- fs.chmodSync(filePath, 0o755);
122
+ const currentMode = fs.statSync(filePath).mode & 0o777;
123
+ if (currentMode !== 0o755) {
124
+ fs.chmodSync(filePath, 0o755);
125
+ (0, utils_1.verbose)(`chmod 755 ${filePath}`);
126
+ }
107
127
  }
108
- (0, utils_1.verbose)(`write ${filePath}`);
109
128
  }
110
129
  function removeStaleGeneratedEntries(rendered) {
111
130
  const expectedFiles = new Set(rendered.files.map((file) => path.resolve(file.path)));
@@ -203,6 +222,7 @@ function checkRenderedFiles(rendered, findings) {
203
222
  function checkRuntimeDirectories(context, findings) {
204
223
  for (const dir of [
205
224
  context.paths.reviewDir,
225
+ context.paths.projectMemoryDir,
206
226
  context.paths.memoriesDir,
207
227
  context.paths.codexHomeDir,
208
228
  context.paths.liveDir,
@@ -296,6 +316,7 @@ function resolveProfileOverrideTemplate(configRoot, context, overrideFileName, g
296
316
  function syncRuntimeDirs(context) {
297
317
  for (const dir of [
298
318
  context.paths.reviewDir,
319
+ context.paths.projectMemoryDir,
299
320
  context.paths.memoriesDir,
300
321
  context.paths.codexHomeDir,
301
322
  context.paths.codexSkillsDir,
@@ -305,6 +326,11 @@ function syncRuntimeDirs(context) {
305
326
  ]) {
306
327
  fs.mkdirSync(dir, { recursive: true });
307
328
  }
329
+ const projectMemoryIndex = path.join(context.paths.projectMemoryDir, 'README.md');
330
+ if (!fs.existsSync(projectMemoryIndex)) {
331
+ fs.writeFileSync(projectMemoryIndex, (0, defaults_1.defaultProjectMemoryIndex)(), 'utf8');
332
+ (0, utils_1.verbose)(`created ${projectMemoryIndex}`);
333
+ }
308
334
  migrateLiveReviewFiles(context);
309
335
  removeLegacyCodexSkillDirs(context);
310
336
  removeLegacyCodeReviewSkillDirs(context);
package/dist/templates.js CHANGED
@@ -35,6 +35,7 @@ function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env)
35
35
  };
36
36
  const reviewDir = resolveRuntimePath(configRoot, manifest.paths.reviewDir, env, baseContext);
37
37
  const memoriesDir = resolveRuntimePath(configRoot, manifest.paths.memoriesDir, env, baseContext);
38
+ const projectMemoryDir = resolveRuntimePath(configRoot, manifest.paths.projectMemoryDir, env, baseContext);
38
39
  const codexHomeDir = path.join(memoriesDir, 'codex-home');
39
40
  const paths = {
40
41
  profileDir,
@@ -47,6 +48,8 @@ function buildRenderContext(projectRoot, profileDir, configRoot, manifest, env)
47
48
  : path.join(reviewDir, 'REVIEW.md'),
48
49
  memoriesDir,
49
50
  globalMemoryDir: path.join(configRoot, 'notes', 'memory'),
51
+ projectMemoryDir,
52
+ nativeMemoryDir: path.join(codexHomeDir, 'memories'),
50
53
  codexHomeDir,
51
54
  overridesDir: path.join(profileDir, 'overrides'),
52
55
  codexSkillsDir: path.join(codexHomeDir, 'skills'),
package/dist/tools.js CHANGED
@@ -23,6 +23,7 @@ function runCodex(realBinary, permissionArgs, context, args, wrapperArgs) {
23
23
  AGENT_DIR: liveDir,
24
24
  AGENT_PROFILE_DIR: context.profileDir,
25
25
  AGENT_RUN_PROJECT_ROOT: projectRoot,
26
+ AGENT_PROJECT_MEMORY_DIR: context.paths.projectMemoryDir,
26
27
  AGENT_RUN_REAL_PATH: realPath,
27
28
  PATH: `${path.join(liveDir, 'bin')}${path.delimiter}${realPath}`
28
29
  };
@@ -30,6 +31,8 @@ function runCodex(realBinary, permissionArgs, context, args, wrapperArgs) {
30
31
  (0, process_1.execCommand)(realBinary, [
31
32
  ...permissionArgs,
32
33
  ...runtimeArgs,
34
+ '--add-dir',
35
+ context.paths.projectMemoryDir,
33
36
  '-C',
34
37
  projectRoot,
35
38
  ...(wrapperArgs.sandboxMode !== 'danger' && wrapperArgs.codexNetwork
@@ -51,12 +54,15 @@ function runClaude(realBinary, permissionArgs, context, args, wrapperArgs) {
51
54
  AGENT_DIR: liveDir,
52
55
  AGENT_PROFILE_DIR: context.profileDir,
53
56
  AGENT_RUN_PROJECT_ROOT: context.projectRoot,
57
+ AGENT_PROJECT_MEMORY_DIR: context.paths.projectMemoryDir,
54
58
  AGENT_RUN_REAL_PATH: realPath,
55
59
  PATH: `${path.join(liveDir, 'bin')}${path.delimiter}${realPath}`
56
60
  };
57
61
  const runtimeArgs = wrapperArgs.sandboxMode === 'danger' ? getDangerArgs('claude') : permissionArgs;
58
62
  (0, process_1.execCommand)(realBinary, [
59
63
  ...runtimeArgs,
64
+ '--add-dir',
65
+ context.paths.projectMemoryDir,
60
66
  '--append-system-prompt-file',
61
67
  claudePath,
62
68
  '--settings',
@@ -9,7 +9,8 @@ It contains two pieces:
9
9
  config root.
10
10
  - `agent-config/`: a complete config root with shared manifest defaults, global
11
11
  templates, snippets, skill templates, a personal memory skill, a sparse
12
- profile manifest, profile-local instructions, and profile overrides.
12
+ profile manifest, durable project memory, profile-local instructions, and
13
+ profile overrides.
13
14
 
14
15
  Try it from this repository after building:
15
16
 
@@ -33,6 +33,7 @@
33
33
  "changesFile": "{{ projectRoot }}/CHANGES",
34
34
  "reviewDir": "{{ profileDir }}/reviews",
35
35
  "reviewFile": "{{ profileDir }}/reviews/REVIEW-{{ date }}.md",
36
- "memoriesDir": "{{ agentDir }}/memories"
36
+ "memoriesDir": "{{ agentDir }}/memories",
37
+ "projectMemoryDir": "{{ profileDir }}/notes/memory"
37
38
  }
38
39
  }
@@ -14,6 +14,8 @@ Do not edit this generated file. Edit root defaults or profile-specific source f
14
14
  - reviewFile: `{{ paths.reviewFile }}`
15
15
  - reviewConsolidatedFile: `{{ paths.reviewConsolidatedFile }}`
16
16
  - memoriesDir: `{{ paths.memoriesDir }}`
17
+ - nativeMemoryDir: `{{ paths.nativeMemoryDir }}`
18
+ - projectMemoryDir: `{{ paths.projectMemoryDir }}`
17
19
  - globalMemoryDir: `{{ paths.globalMemoryDir }}`
18
20
 
19
21
  ## Skills
@@ -6,4 +6,5 @@
6
6
  {% endfor %}
7
7
  Claude reads this file from the private agent directory.
8
8
 
9
+ projectMemoryDir: `{{ paths.projectMemoryDir }}`
9
10
  globalMemoryDir: `{{ paths.globalMemoryDir }}`
@@ -3,6 +3,7 @@
3
3
  "AGENT_DIR": {{ agentDir | dump }},
4
4
  "AGENT_PROFILE_DIR": {{ profileDir | dump }},
5
5
  "AGENT_RUN_PROJECT_ROOT": {{ projectRoot | dump }},
6
+ "AGENT_PROJECT_MEMORY_DIR": {{ paths.projectMemoryDir | dump }},
6
7
  "AGENT_GLOBAL_MEMORY_DIR": {{ paths.globalMemoryDir | dump }},
7
8
  "STARTER_PROFILE": {{ profile | dump }}
8
9
  },
@@ -15,6 +16,7 @@
15
16
  "AGENT_DIR": {{ agentDir | dump }},
16
17
  "AGENT_PROFILE_DIR": {{ profileDir | dump }},
17
18
  "AGENT_RUN_PROJECT_ROOT": {{ projectRoot | dump }},
19
+ "AGENT_PROJECT_MEMORY_DIR": {{ paths.projectMemoryDir | dump }},
18
20
  "AGENT_GLOBAL_MEMORY_DIR": {{ paths.globalMemoryDir | dump }},
19
21
  "STARTER_PROFILE": {{ profile | dump }}
20
22
  }
@@ -7,6 +7,7 @@ sandbox_mode = "workspace-write"
7
7
  writable_roots = [
8
8
  {{ projectRoot | dump }},
9
9
  {{ paths.reviewDir | dump }},
10
- {{ paths.memoriesDir | dump }}
10
+ {{ paths.memoriesDir | dump }},
11
+ {{ paths.projectMemoryDir | dump }}
11
12
  ]
12
13
  network_access = false
@@ -0,0 +1,8 @@
1
+ # Project memory
2
+
3
+ This directory contains durable notes for this project across agent sessions and machines.
4
+ Keep the notes short, factual, and free of secrets. Link additional topic files from this index.
5
+
6
+ ## Files
7
+
8
+ - Add project memory files here as they are needed.
@@ -3,6 +3,7 @@
3
3
  "AGENT_DIR": {{ agentDir | dump }},
4
4
  "AGENT_PROFILE_DIR": {{ profileDir | dump }},
5
5
  "AGENT_RUN_PROJECT_ROOT": {{ projectRoot | dump }},
6
+ "AGENT_PROJECT_MEMORY_DIR": {{ paths.projectMemoryDir | dump }},
6
7
  "AGENT_GLOBAL_MEMORY_DIR": {{ paths.globalMemoryDir | dump }},
7
8
  "STARTER_PROFILE": {{ profile | dump }},
8
9
  "STARTER_OVERRIDE": "true"
@@ -16,6 +17,7 @@
16
17
  "AGENT_DIR": {{ agentDir | dump }},
17
18
  "AGENT_PROFILE_DIR": {{ profileDir | dump }},
18
19
  "AGENT_RUN_PROJECT_ROOT": {{ projectRoot | dump }},
20
+ "AGENT_PROJECT_MEMORY_DIR": {{ paths.projectMemoryDir | dump }},
19
21
  "AGENT_GLOBAL_MEMORY_DIR": {{ paths.globalMemoryDir | dump }},
20
22
  "STARTER_PROFILE": {{ profile | dump }},
21
23
  "STARTER_OVERRIDE": "true"
@@ -7,6 +7,7 @@ sandbox_mode = "workspace-write"
7
7
  writable_roots = [
8
8
  {{ projectRoot | dump }},
9
9
  {{ paths.reviewDir | dump }},
10
- {{ paths.memoriesDir | dump }}
10
+ {{ paths.memoriesDir | dump }},
11
+ {{ paths.projectMemoryDir | dump }}
11
12
  ]
12
13
  network_access = false
@@ -13,4 +13,4 @@ Environment=XDG_CONFIG_HOME=/root/.config
13
13
  Environment=NPM_CONFIG_PREFIX=/usr/local
14
14
  Environment=PNPM_HOME=/usr/local
15
15
  Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
16
- ExecStart=/usr/local/lib/agent-run/update-ai-tools.sh
16
+ ExecStart=/usr/local/libexec/agent-run/update-ai-tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technomoron/agent-run",
3
- "version": "0.99.27",
3
+ "version": "0.99.29",
4
4
  "packageManager": "pnpm@10.28.1",
5
5
  "bin": {
6
6
  "agent-run": "dist/agent-run.js"
@@ -19,10 +19,20 @@ USAGE
19
19
  }
20
20
 
21
21
  ROOT="$(cd "$(dirname "$0")/.." && pwd)"
22
- INSTALL_LIB_DIR="${INSTALL_LIB_DIR:-/usr/local/lib/agent-run}"
22
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
23
+ INSTALL_LIBEXEC_DIR="${INSTALL_LIBEXEC_DIR:-${INSTALL_LIB_DIR:-/usr/local/libexec/agent-run}}"
23
24
  SYSTEMD_DIR="${SYSTEMD_DIR:-/etc/systemd/system}"
24
25
  ORIGINAL_ARGS=("$@")
25
26
 
27
+ UPDATE_SCRIPT="$ROOT/scripts/update-ai-tools.sh"
28
+ SERVICE_FILE="$ROOT/ops/systemd/ai-tools-update.service"
29
+ TIMER_FILE="$ROOT/ops/systemd/ai-tools-update.timer"
30
+ if [ ! -f "$UPDATE_SCRIPT" ]; then
31
+ UPDATE_SCRIPT="$SCRIPT_DIR/update-ai-tools.sh"
32
+ SERVICE_FILE="$SCRIPT_DIR/ai-tools-update.service"
33
+ TIMER_FILE="$SCRIPT_DIR/ai-tools-update.timer"
34
+ fi
35
+
26
36
  INSTALL_AI_TOOLS=0
27
37
  DRY_RUN=0
28
38
 
@@ -71,10 +81,10 @@ run() {
71
81
  "$@"
72
82
  }
73
83
 
74
- run install -d -m 0755 "$INSTALL_LIB_DIR" "$SYSTEMD_DIR"
75
- run install -m 0755 "$ROOT/scripts/update-ai-tools.sh" "$INSTALL_LIB_DIR/update-ai-tools.sh"
76
- run install -m 0644 "$ROOT/ops/systemd/ai-tools-update.service" "$SYSTEMD_DIR/ai-tools-update.service"
77
- run install -m 0644 "$ROOT/ops/systemd/ai-tools-update.timer" "$SYSTEMD_DIR/ai-tools-update.timer"
84
+ run install -d -m 0755 "$INSTALL_LIBEXEC_DIR" "$SYSTEMD_DIR"
85
+ run install -m 0755 "$UPDATE_SCRIPT" "$INSTALL_LIBEXEC_DIR/update-ai-tools"
86
+ run install -m 0644 "$SERVICE_FILE" "$SYSTEMD_DIR/ai-tools-update.service"
87
+ run install -m 0644 "$TIMER_FILE" "$SYSTEMD_DIR/ai-tools-update.timer"
78
88
 
79
89
  if [ "$INSTALL_AI_TOOLS" -eq 1 ]; then
80
90
  run systemctl daemon-reload
@@ -6,18 +6,44 @@ export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-/usr/local}"
6
6
  export PNPM_HOME="${PNPM_HOME:-/usr/local}"
7
7
  export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
8
8
 
9
- NPM_BIN="${AI_TOOLS_NPM_BIN:-/usr/local/bin/npm}"
9
+ NPM_BIN="${AI_TOOLS_NPM_BIN:-/usr/bin/npm}"
10
+ NODE_BIN="${AI_TOOLS_NODE_BIN:-/usr/bin/node}"
10
11
  APT_GET_BIN="${AI_TOOLS_APT_GET_BIN:-/usr/bin/apt-get}"
11
12
 
12
13
  AI_TOOLS_NPM_PACKAGES="${AI_TOOLS_NPM_PACKAGES:-npm@latest pnpm@latest corepack@latest fallow@latest ripgrep@latest pm2@latest tsx@latest typescript@latest @openai/codex@latest @anthropic-ai/claude-code@latest @technomoron/agent-run@latest}"
13
14
  AI_TOOLS_PNPM_PACKAGE="${AI_TOOLS_PNPM_PACKAGE:-pnpm@latest}"
14
15
  AI_TOOLS_APT_PACKAGES="${AI_TOOLS_APT_PACKAGES:-gh}"
16
+ AI_TOOLS_CLEAN_BINS="${AI_TOOLS_CLEAN_BINS:-agent-run claude codex}"
17
+ AI_TOOLS_SYSTEM_BIN_DIRS="${AI_TOOLS_SYSTEM_BIN_DIRS:-/usr/local/bin /usr/bin}"
15
18
 
16
19
  if [ "$(id -u)" -ne 0 ]; then
17
20
  echo "update-ai-tools.sh must run as root" >&2
18
21
  exit 1
19
22
  fi
20
23
 
24
+ clean_tool_bins() {
25
+ local bin_dir="$1"
26
+ local bin_name
27
+ local -a bin_names
28
+
29
+ [ -d "$bin_dir" ] || return 0
30
+ read -r -a bin_names <<<"$AI_TOOLS_CLEAN_BINS"
31
+ for bin_name in "${bin_names[@]}"; do
32
+ case "$bin_name" in
33
+ agent-run|claude|codex)
34
+ rm -f -- "$bin_dir/$bin_name"
35
+ ;;
36
+ *)
37
+ echo "Refusing to clean unexpected AI tool bin: $bin_name" >&2
38
+ exit 1
39
+ ;;
40
+ esac
41
+ done
42
+ }
43
+
44
+ target_bin_dir="${NPM_CONFIG_PREFIX%/}/bin"
45
+ clean_tool_bins "$target_bin_dir"
46
+
21
47
  if [ -n "$AI_TOOLS_NPM_PACKAGES" ]; then
22
48
  read -r -a npm_packages <<<"$AI_TOOLS_NPM_PACKAGES"
23
49
  "$NPM_BIN" install -g --force "${npm_packages[@]}"
@@ -30,9 +56,18 @@ if [ -n "$AI_TOOLS_PNPM_PACKAGE" ]; then
30
56
  "$NPM_BIN" install -g --force "${pnpm_packages[@]}"
31
57
  fi
32
58
 
59
+ # Once the new launchers are installed, remove exact-name duplicates from the
60
+ # other conventional system prefix so PATH ordering cannot select a stale copy.
61
+ read -r -a system_bin_dirs <<<"$AI_TOOLS_SYSTEM_BIN_DIRS"
62
+ for system_bin_dir in "${system_bin_dirs[@]}"; do
63
+ if [ "${system_bin_dir%/}" != "${target_bin_dir%/}" ]; then
64
+ clean_tool_bins "$system_bin_dir"
65
+ fi
66
+ done
67
+
33
68
  CLAUDE_INSTALL="$("$NPM_BIN" root -g)/@anthropic-ai/claude-code/install.cjs"
34
69
  if [ -f "$CLAUDE_INSTALL" ]; then
35
- /usr/local/bin/node "$CLAUDE_INSTALL"
70
+ "$NODE_BIN" "$CLAUDE_INSTALL"
36
71
  fi
37
72
 
38
73
  if [ -n "$AI_TOOLS_APT_PACKAGES" ] && command -v "$APT_GET_BIN" >/dev/null 2>&1; then