amicus 1.6.1 → 1.7.0

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.
@@ -0,0 +1,66 @@
1
+ // src/utils/project-root-sanity.js
2
+ 'use strict';
3
+
4
+ /**
5
+ * Project-root sanity heuristic (#43).
6
+ *
7
+ * `amicus doctor` resolves the project dir from process.cwd(); when a user
8
+ * launches amicus from a packaged-app/install directory (e.g. the Claude
9
+ * Desktop app dir) instead of their repo, sessions land in the wrong place.
10
+ * This pure helper inspects a path string + its directory markers and decides
11
+ * whether to WARN. It never touches the filesystem and never throws.
12
+ */
13
+
14
+ /**
15
+ * Path fragments that signal a packaged-app / install directory rather than a
16
+ * user project. Matched case-insensitively against the normalized path.
17
+ */
18
+ const INSTALL_PATTERNS = [
19
+ /anthropicclaude/i, // Claude Desktop app dir
20
+ /[\\/]app-\d/i, // versioned electron app dir, e.g. app-1.2.3
21
+ /appdata[\\/]local/i, // Windows per-user app data
22
+ /program files/i, // Windows install root
23
+ /[\\/]\.asar/i, // packaged electron resources
24
+ ];
25
+
26
+ /** @returns {boolean} true if the path looks like an app/install dir */
27
+ function looksLikeInstallDir(dir) {
28
+ if (!dir || typeof dir !== 'string') { return false; }
29
+ return INSTALL_PATTERNS.some((re) => re.test(dir));
30
+ }
31
+
32
+ /**
33
+ * Assess a resolved project dir.
34
+ *
35
+ * @param {string} dir Resolved project directory (e.g. process.cwd()).
36
+ * @param {{hasGit?:boolean, hasPackageJson?:boolean, hasClaude?:boolean}} markers
37
+ * Presence of .git / package.json / .claude in `dir`.
38
+ * @returns {{status:'ok'|'warn', message:string, hint:string|null}}
39
+ */
40
+ function assessProjectRoot(dir, markers) {
41
+ const m = markers || {};
42
+ const safeDir = (dir && typeof dir === 'string') ? dir : '';
43
+ const hint =
44
+ 'pass an explicit project (amicus … --project <path>) or cd into your repo before running amicus';
45
+
46
+ if (looksLikeInstallDir(safeDir)) {
47
+ return {
48
+ status: 'warn',
49
+ message: `${safeDir || '(empty)'} looks like an app/install dir, not a project`,
50
+ hint,
51
+ };
52
+ }
53
+
54
+ const hasMarker = !!(m.hasGit || m.hasPackageJson || m.hasClaude);
55
+ if (!hasMarker) {
56
+ return {
57
+ status: 'warn',
58
+ message: `${safeDir || '(empty)'} has no project markers (.git / package.json / .claude)`,
59
+ hint,
60
+ };
61
+ }
62
+
63
+ return { status: 'ok', message: safeDir, hint: null };
64
+ }
65
+
66
+ module.exports = { assessProjectRoot, looksLikeInstallDir, INSTALL_PATTERNS };
@@ -0,0 +1,51 @@
1
+ // src/utils/remediation-hints.js
2
+ 'use strict';
3
+
4
+ /**
5
+ * Shared, copy-paste remediation hint strings.
6
+ *
7
+ * One source of truth for the fix commands surfaced by `amicus doctor` and by
8
+ * failure messages across the CLI, so the guidance never drifts. Consumed by
9
+ * src/cli-handlers-doctor.js (per-check hints) and reused by sweep'd failure
10
+ * sites (#32) and the MCP recovery surface (#43).
11
+ *
12
+ * The object is frozen — these are a stable copy-paste contract; callers read
13
+ * fields, they do not mutate them.
14
+ */
15
+ const REMEDIATION_HINTS = Object.freeze({
16
+ /** Canonical global (re)install. */
17
+ reinstall: 'npm install -g amicus',
18
+
19
+ /** `npm cache clean --force` — clears a corrupt npm cache before reinstalling. */
20
+ cacheClean: 'npm cache clean --force',
21
+
22
+ /**
23
+ * Engine binaries missing/rolled back. A transient install error can roll
24
+ * back the platform engine packages; re-run, or clean the cache and reinstall.
25
+ */
26
+ reinstallEngine:
27
+ 'npm install -g amicus (a transient install error can roll back the engine binaries — re-run, or: npm cache clean --force && npm install -g amicus)',
28
+
29
+ /** Electron absent — reinstall to add the interactive GUI (headless still works). */
30
+ reinstallElectron: 'npm install -g amicus (reinstall to add Electron)',
31
+
32
+ /**
33
+ * Electron present but broken (ABI mismatch / partial unpack). Delete the
34
+ * vendored copy and reinstall to force a clean rebuild.
35
+ */
36
+ rebuildElectron:
37
+ 'rm -rf node_modules/electron && npm install -g amicus (rebuild Electron after an ABI mismatch or partial unpack)',
38
+
39
+ /** Point the user at the single recovery hub. */
40
+ runDoctor: 'run: amicus doctor (diagnoses config, keys, engine & MCP, with copy-paste fixes)',
41
+
42
+ /**
43
+ * Self-heal the optional Electron GUI in place (#56). This is the convergence
44
+ * target for the three "reinstall to fix Electron" hints — it provisions the
45
+ * binary from cache (or downloads on demand) WITHOUT a global reinstall, so it
46
+ * can't loop the way `npm install -g amicus` could when the rollback recurs.
47
+ */
48
+ doctorFix: 'amicus doctor --fix (self-heal the Electron GUI in place — provisions the binary; no reinstall, so it can\'t loop)',
49
+ });
50
+
51
+ module.exports = REMEDIATION_HINTS;
@@ -106,9 +106,19 @@ function waveExitCode(waveStatus) {
106
106
  * @param {object} opts
107
107
  * @param {string} opts.waveId
108
108
  * @param {Array<object>} opts.legs - run documents (in --models order)
109
- * Counts track the four primary terminal statuses (complete/error/timeout/aborted);
110
- * legs with other statuses (e.g. 'crashed', 'running' in a rebuilt wave) count toward
111
- * `total` only, so total may exceed the sum of the named buckets.
109
+ *
110
+ * COUNTS REMAINDER RULE (stable, no schemaVersion bump): `counts` exposes four
111
+ * NAMED terminal buckets complete, error, timeout, aborted plus `total`
112
+ * (= legs.length). The remaining TERMINAL_STATUSES ('crashed', 'idle-timeout')
113
+ * and any non-terminal status (e.g. 'running' in a live-rebuilt wave) are
114
+ * deliberately NOT given their own bucket; they are reflected ONLY in `total`.
115
+ * Therefore a consumer must treat the unnamed remainder as
116
+ * total − (complete + error + timeout + aborted)
117
+ * and must NOT assume the named buckets sum to `total`. Adding new buckets
118
+ * would change the document shape and REQUIRES bumping SCHEMA_VERSION.
119
+ * This agrees with the MCP wave path (mcp-server.js), which counts a leg as
120
+ * "done" iff its status is in TERMINAL_STATUSES — including 'crashed' — so a
121
+ * crashed leg is done/total there exactly as it is total-only here.
112
122
  * @param {{source: string, file: string|null, chars: number}|null} [opts.promptMeta]
113
123
  * @param {string|null} [opts.createdAt]
114
124
  * @param {string|null} [opts.completedAt]
@@ -117,6 +127,9 @@ function waveExitCode(waveStatus) {
117
127
  */
118
128
  function buildWaveResult({ waveId, legs = [], promptMeta = null, createdAt = null, completedAt = null, status = null }) {
119
129
  const { sumWaveUsage } = require('./pricing');
130
+ // Named buckets only (see "COUNTS REMAINDER RULE" above). 'crashed' and
131
+ // 'idle-timeout' legs are intentionally NOT bucketed — they land in `total`
132
+ // only, so total may exceed complete+error+timeout+aborted.
120
133
  const counts = {
121
134
  total: legs.length,
122
135
  complete: legs.filter(l => l.status === 'complete').length,
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @module utils/version-info — running vs. on-disk amicus version (#33)
3
+ *
4
+ * After an `npm i -g amicus` upgrade, a long-lived MCP server process keeps
5
+ * running the OLD code until the client restarts it. That staleness is
6
+ * invisible from inside an agent session. These helpers surface the running
7
+ * version in MCP responses and, via a CALL-TIME re-read of the on-disk
8
+ * package.json, flag when the two have diverged.
9
+ */
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ /** Absolute path to this install's package.json (repo root). */
14
+ const PKG_PATH = path.join(__dirname, '..', '..', 'package.json');
15
+
16
+ /**
17
+ * The version baked into the running process at load time. Free: package.json
18
+ * is already require()'d elsewhere, so this hits the module cache.
19
+ */
20
+ const RUNNING_VERSION = require('../../package.json').version;
21
+
22
+ /**
23
+ * Re-read the on-disk package.json version at call time. Wrapped in try/catch
24
+ * — the file may be mid-rewrite, missing, or unreadable during/after an
25
+ * upgrade — and returns null on any failure rather than throwing.
26
+ * @returns {string|null}
27
+ */
28
+ function readOnDiskVersion() {
29
+ try {
30
+ const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8'));
31
+ return (pkg && typeof pkg.version === 'string') ? pkg.version : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * One-line staleness warning, or null when there's nothing to warn about
39
+ * (on-disk version unreadable or identical to the running version).
40
+ * @returns {string|null}
41
+ */
42
+ function versionWarning() {
43
+ const onDisk = readOnDiskVersion();
44
+ if (!onDisk || onDisk === RUNNING_VERSION) { return null; }
45
+ return `Amicus was upgraded on disk (running v${RUNNING_VERSION}, on-disk v${onDisk}). `
46
+ + `Restart your MCP client to load v${onDisk}.`;
47
+ }
48
+
49
+ module.exports = { RUNNING_VERSION, readOnDiskVersion, versionWarning, PKG_PATH };