@ijfw/install 1.5.3 → 1.5.5

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/install.js CHANGED
@@ -73,20 +73,44 @@ function writeAtomic(path3, contents, opts = {}) {
73
73
  }
74
74
  }
75
75
  function backup(path3, ts) {
76
+ const res = backupDetailed(path3, ts);
77
+ return res.ok && res.path ? res.path : null;
78
+ }
79
+ function backupDetailed(path3, ts) {
80
+ let st;
76
81
  try {
77
- const st = statSync(path3);
78
- if (!st.isFile()) return null;
79
- } catch {
80
- return null;
82
+ st = statSync(path3);
83
+ } catch (err) {
84
+ if (err && err.code === "ENOENT") return { ok: true, path: null, reason: "absent" };
85
+ return { ok: false, reason: "stat-failed", error: err };
86
+ }
87
+ if (!st.isFile()) {
88
+ return { ok: true, path: null, reason: "not-a-file" };
81
89
  }
82
90
  const dst = `${path3}.bak.${ts}`;
83
91
  try {
84
92
  copyFileSync(path3, dst);
85
93
  printInfo(`backup: ${basename(path3)}.bak.${ts}`);
86
- return dst;
87
- } catch {
94
+ return { ok: true, path: dst };
95
+ } catch (err) {
96
+ return { ok: false, reason: "copy-failed", error: err };
97
+ }
98
+ }
99
+ function requireBackup(path3, ts) {
100
+ if (!ts) return null;
101
+ const res = backupDetailed(path3, ts);
102
+ if (res.ok) return res.path;
103
+ if (process.env.IJFW_FORCE_NO_BACKUP === "1") {
104
+ const why2 = res.error && res.error.message ? res.error.message : res.reason;
105
+ printInfo(`backup: forced past failure for ${basename(path3)} (${why2})`);
88
106
  return null;
89
107
  }
108
+ const why = res.error && res.error.message ? res.error.message : res.reason;
109
+ const err = new Error(
110
+ `Refusing to merge into existing config without backup: ${path3} (${why}). Re-run with IJFW_FORCE_NO_BACKUP=1 to proceed.`
111
+ );
112
+ err.code = "BACKUP_REQUIRED";
113
+ throw err;
90
114
  }
91
115
  function safeChecksum(path3) {
92
116
  try {
@@ -244,7 +268,7 @@ function readJsonOrEmpty(path3) {
244
268
  }
245
269
  function mergeJson(dst, serverJs, ts) {
246
270
  mkdirSync2(dirname3(dst), { recursive: true });
247
- if (ts) backup(dst, ts);
271
+ requireBackup(dst, ts);
248
272
  const doc = readJsonOrEmpty(dst);
249
273
  if (!doc.mcpServers || typeof doc.mcpServers !== "object") doc.mcpServers = {};
250
274
  const isWin = IS_WIN;
@@ -272,7 +296,7 @@ function mergeJson(dst, serverJs, ts) {
272
296
  }
273
297
  function mergeToml(dst, serverJs, ts) {
274
298
  mkdirSync2(dirname3(dst), { recursive: true });
275
- if (ts) backup(dst, ts);
299
+ requireBackup(dst, ts);
276
300
  let text = "";
277
301
  try {
278
302
  text = existsSync3(dst) ? readFileSync2(dst, "utf8") : "";
@@ -560,7 +584,8 @@ import {
560
584
  statSync as statSync2,
561
585
  copyFileSync as copyFileSync2,
562
586
  cpSync,
563
- chmodSync as chmodSync2
587
+ chmodSync as chmodSync2,
588
+ rmSync
564
589
  } from "node:fs";
565
590
  import { join as join4, dirname as dirname4, isAbsolute } from "node:path";
566
591
  import { platform } from "node:os";
@@ -935,10 +960,31 @@ async function installWayland(ctx) {
935
960
  const pluginDst = join4(ctx.home, ".wayland", "plugins", "ijfw");
936
961
  ensureDir(pluginDst);
937
962
  let entries;
963
+ let readdirErr = null;
938
964
  try {
939
965
  entries = readdirSync(pluginSrc);
940
- } catch {
966
+ } catch (err) {
941
967
  entries = [];
968
+ readdirErr = err;
969
+ }
970
+ if (readdirErr) {
971
+ ctx.log.warn(`Wayland plugin tree readdir failed: ${readdirErr.message || readdirErr}`);
972
+ }
973
+ const srcNames = new Set(entries.filter((n) => n !== "__pycache__"));
974
+ let dstEntries = [];
975
+ try {
976
+ dstEntries = readdirSync(pluginDst);
977
+ } catch {
978
+ }
979
+ for (const name of dstEntries) {
980
+ if (name === "__pycache__") continue;
981
+ if (!srcNames.has(name)) {
982
+ try {
983
+ rmSync(join4(pluginDst, name), { recursive: true, force: true });
984
+ } catch (err) {
985
+ ctx.log.warn(`Wayland plugin: could not remove stale ${name}: ${err.message || err}`);
986
+ }
987
+ }
942
988
  }
943
989
  for (const name of entries) {
944
990
  if (name === "__pycache__") continue;
@@ -986,10 +1032,31 @@ async function installHermes(ctx) {
986
1032
  const pluginDst = join4(ctx.home, ".hermes", "plugins", "ijfw");
987
1033
  ensureDir(pluginDst);
988
1034
  let entries;
1035
+ let readdirErr = null;
989
1036
  try {
990
1037
  entries = readdirSync(pluginSrc);
991
- } catch {
1038
+ } catch (err) {
992
1039
  entries = [];
1040
+ readdirErr = err;
1041
+ }
1042
+ if (readdirErr) {
1043
+ ctx.log.warn(`Hermes plugin tree readdir failed: ${readdirErr.message || readdirErr}`);
1044
+ }
1045
+ const srcNames = new Set(entries.filter((n) => n !== "__pycache__"));
1046
+ let dstEntries = [];
1047
+ try {
1048
+ dstEntries = readdirSync(pluginDst);
1049
+ } catch {
1050
+ }
1051
+ for (const name of dstEntries) {
1052
+ if (name === "__pycache__") continue;
1053
+ if (!srcNames.has(name)) {
1054
+ try {
1055
+ rmSync(join4(pluginDst, name), { recursive: true, force: true });
1056
+ } catch (err) {
1057
+ ctx.log.warn(`Hermes plugin: could not remove stale ${name}: ${err.message || err}`);
1058
+ }
1059
+ }
993
1060
  }
994
1061
  for (const name of entries) {
995
1062
  if (name === "__pycache__") continue;
@@ -1181,18 +1248,27 @@ function installOpenclaw(ctx) {
1181
1248
  }
1182
1249
  const dst = path.join(ctx.home, ".openclaw", "openclaw.json");
1183
1250
  const serverJs = ctx.serverJsNative || ctx.serverJs;
1251
+ let cliRegistered = false;
1184
1252
  if (commandExists("openclaw")) {
1185
1253
  try {
1186
1254
  const payload = JSON.stringify({ command: "node", args: [serverJs] });
1187
- execFileSync("openclaw", ["mcp", "set", "ijfw-memory", payload], { stdio: "ignore" });
1188
- printOk(`Registered ijfw-memory via 'openclaw mcp set' (${dst})`);
1189
- return { status: "ok" };
1190
- } catch {
1255
+ execFileSync("openclaw", ["mcp", "set", "ijfw-memory", payload], {
1256
+ stdio: ["ignore", "pipe", "pipe"],
1257
+ encoding: "utf8"
1258
+ });
1259
+ cliRegistered = true;
1260
+ } catch (err) {
1261
+ const msg = err && err.stderr ? String(err.stderr).trim() : err && err.message || String(err);
1262
+ printInfo(`openclaw CLI registration failed (${msg}); falling back to file-write merge.`);
1191
1263
  }
1192
1264
  }
1193
1265
  ensureDir2(path.dirname(dst));
1194
1266
  openclawMerge(dst, serverJs);
1195
- printOk(`Merged MCP into ${dst} (openclaw mcp.servers schema)`);
1267
+ if (cliRegistered) {
1268
+ printOk(`Registered ijfw-memory via 'openclaw mcp set' AND file-write merge (${dst})`);
1269
+ } else {
1270
+ printOk(`Merged MCP into ${dst} (openclaw mcp.servers schema)`);
1271
+ }
1196
1272
  return { status: "ok" };
1197
1273
  }
1198
1274
  function installAider(ctx) {
@@ -1294,6 +1370,14 @@ function linkPlugin({ repoRoot, ijfwHome, ts }) {
1294
1370
  } catch {
1295
1371
  }
1296
1372
  } else if (st.isDirectory()) {
1373
+ try {
1374
+ fs2.rmSync(pluginDst, { recursive: true, force: true });
1375
+ } catch (err) {
1376
+ process.stderr.write(
1377
+ `[ijfw] linkPlugin: could not clear ${pluginDst} for mirror (${err && err.message ? err.message : err}); falling back to merge.
1378
+ `
1379
+ );
1380
+ }
1297
1381
  fs2.cpSync(pluginSrc, pluginDst, { recursive: true });
1298
1382
  printOk(`Plugin tree mirrored to ${pluginDst}`);
1299
1383
  return;
@@ -1369,12 +1453,20 @@ function seedState({ ijfwHome, repoRoot, nodeBin: _nodeBin }) {
1369
1453
  }
1370
1454
  } catch {
1371
1455
  }
1372
- let installedVer = "0.0.0";
1456
+ const pkgPath = path2.join(repoRoot, "installer", "package.json");
1457
+ let installedVer;
1373
1458
  try {
1374
- const pkgPath = path2.join(repoRoot, "installer", "package.json");
1375
1459
  const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
1376
- installedVer = pkg.version || "0.0.0";
1377
- } catch {
1460
+ installedVer = pkg.version;
1461
+ } catch (err) {
1462
+ throw new Error(
1463
+ `Preflight: installer/package.json missing or unreadable at ${pkgPath} (${err && err.message ? err.message : err}). Repo tree incomplete; rerun bootstrap from a clean clone.`
1464
+ );
1465
+ }
1466
+ if (!installedVer || typeof installedVer !== "string") {
1467
+ throw new Error(
1468
+ `Preflight: installer/package.json at ${pkgPath} has no usable "version" field. Rerun bootstrap from a clean clone.`
1469
+ );
1378
1470
  }
1379
1471
  const nowTs = Math.floor(Date.now() / 1e3);
1380
1472
  const state = {
@@ -1484,7 +1576,14 @@ function patchPluginMcpJson({ ijfwHome, repoRoot, nodeBin, serverJs }) {
1484
1576
  d.mcpServers["ijfw-memory"].command = nodeBin;
1485
1577
  d.mcpServers["ijfw-memory"].args = [serverJs];
1486
1578
  const envSep = process.platform === "win32" ? ";" : ":";
1487
- const commonPaths = process.platform === "win32" ? [nodeDir, "C:\\Windows\\System32"] : [nodeDir, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
1579
+ let commonPaths;
1580
+ if (process.platform === "win32") {
1581
+ commonPaths = [nodeDir, "C:\\Windows\\System32"];
1582
+ } else if (process.platform === "darwin") {
1583
+ commonPaths = [nodeDir, "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"];
1584
+ } else {
1585
+ commonPaths = [nodeDir, "/usr/local/bin", "/usr/bin", "/bin"];
1586
+ }
1488
1587
  const dedup = [...new Set(commonPaths.filter((x) => x && fs2.existsSync(x)))];
1489
1588
  d.mcpServers["ijfw-memory"].env = { PATH: dedup.join(envSep) };
1490
1589
  try {
@@ -1857,7 +1956,7 @@ var init_install_flow = __esm({
1857
1956
 
1858
1957
  // src/install.js
1859
1958
  import { spawnSync as spawnSync2 } from "node:child_process";
1860
- import { existsSync as existsSync5, rmSync, mkdirSync as mkdirSync4, realpathSync as realpathSync2, renameSync as renameSync3 } from "node:fs";
1959
+ import { existsSync as existsSync5, rmSync as rmSync2, mkdirSync as mkdirSync4, realpathSync as realpathSync2, renameSync as renameSync3, readdirSync as readdirSync2, cpSync as cpSync2 } from "node:fs";
1861
1960
  import { resolve as resolve4, join as join5, dirname as dirname5 } from "node:path";
1862
1961
  import { homedir as homedir3, platform as platform2 } from "node:os";
1863
1962
  import { fileURLToPath as fileURLToPath2 } from "node:url";
@@ -2017,7 +2116,7 @@ function triggerColdScan(projectRoot, options = {}) {
2017
2116
  }
2018
2117
 
2019
2118
  // src/install.js
2020
- var DEFAULT_REPO = "https://gitlab.com/therealseandonahoe/ijfw.git";
2119
+ var DEFAULT_REPO = "https://github.com/FerroxLabs/ijfw.git";
2021
2120
  var DEFAULT_BRANCH = "main";
2022
2121
  function parseArgs(argv) {
2023
2122
  const out = { yes: false, dir: null, noMarketplace: false, branch: DEFAULT_BRANCH, branchExplicit: false, purge: false };
@@ -2037,7 +2136,11 @@ function parseArgs(argv) {
2037
2136
  }
2038
2137
  return out;
2039
2138
  }
2139
+ function skipNetwork() {
2140
+ return process.env.IJFW_SKIP_NETWORK === "1";
2141
+ }
2040
2142
  function latestTagFromGithub() {
2143
+ if (skipNetwork()) return null;
2041
2144
  try {
2042
2145
  const res = spawnSync2("git", ["ls-remote", "--tags", "--refs", "--sort=-v:refname", DEFAULT_REPO], {
2043
2146
  encoding: "utf8",
@@ -2051,7 +2154,7 @@ function latestTagFromGithub() {
2051
2154
  return null;
2052
2155
  }
2053
2156
  }
2054
- function resolveBranchOrTag({ branch, branchExplicit, _tagLookup } = {}) {
2157
+ function resolveBranchOrTag({ branch, branchExplicit, _tagLookup, _logger } = {}) {
2055
2158
  if (branchExplicit) return branch;
2056
2159
  const lookup = _tagLookup || latestTagFromGithub;
2057
2160
  let tag = null;
@@ -2060,7 +2163,15 @@ function resolveBranchOrTag({ branch, branchExplicit, _tagLookup } = {}) {
2060
2163
  } catch {
2061
2164
  tag = null;
2062
2165
  }
2063
- return tag || branch || DEFAULT_BRANCH;
2166
+ if (!tag) {
2167
+ const log = _logger || console.warn;
2168
+ const eff = branch || DEFAULT_BRANCH;
2169
+ log(
2170
+ ` note: could not resolve latest tag from upstream (network or rate-limit?). Using branch "${eff}" instead. Pin a specific version with --branch vX.Y.Z if needed.`
2171
+ );
2172
+ return eff;
2173
+ }
2174
+ return tag;
2064
2175
  }
2065
2176
  function printHelp() {
2066
2177
  console.log(`ijfw-install -- IJFW installer
@@ -2128,6 +2239,14 @@ function runCheck(cmd, args, opts) {
2128
2239
  return { status: r.status, stdout: r.stdout || "", stderr: r.stderr || "", spawnError: r.error?.code, signal: r.signal };
2129
2240
  }
2130
2241
  function cloneOrPull(dir, branch) {
2242
+ if (skipNetwork()) {
2243
+ if (existsSync5(dir)) {
2244
+ return "skipped-network";
2245
+ }
2246
+ throw new Error(
2247
+ `IJFW_SKIP_NETWORK=1 set but cloneOrPull needs network: target directory ${dir} does not exist. Pre-seed the directory before setting IJFW_SKIP_NETWORK, or unset the env var.`
2248
+ );
2249
+ }
2131
2250
  if (!existsSync5(dir)) {
2132
2251
  mkdirSync4(dir, { recursive: true });
2133
2252
  const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
@@ -2143,7 +2262,11 @@ function cloneOrPull(dir, branch) {
2143
2262
  if (remoteStatus === 0) {
2144
2263
  const STALE_PATTERNS = [
2145
2264
  /^https:\/\/github\.com\/seandonahoe\/ijfw(\.git)?\/?$/i,
2146
- /^https:\/\/github\.com\/therealseandonahoe\/ijfw(\.git)?\/?$/i
2265
+ /^https:\/\/github\.com\/therealseandonahoe\/ijfw(\.git)?\/?$/i,
2266
+ // V155 rebrand: GitLab was the canonical source through v1.5.4;
2267
+ // users who installed from gitlab.com need their origin migrated
2268
+ // forward to FerroxLabs/ijfw on GitHub on next `ijfw-install`.
2269
+ /^https:\/\/gitlab\.com\/therealseandonahoe\/ijfw(\.git)?\/?$/i
2147
2270
  ];
2148
2271
  const currentOrigin = (stdout || "").trim();
2149
2272
  if (STALE_PATTERNS.some((re) => re.test(currentOrigin))) {
@@ -2161,23 +2284,63 @@ function cloneOrPull(dir, branch) {
2161
2284
  return "updated";
2162
2285
  }
2163
2286
  }
2287
+ const RESTORE_ALLOWLIST = [
2288
+ "memory",
2289
+ "sessions",
2290
+ "install.log",
2291
+ ".session-counter",
2292
+ // v1.5.x additions:
2293
+ "ijfw",
2294
+ // visible brain layer (wiki + facts)
2295
+ "state",
2296
+ // state.json, deploy-failures.jsonl, .dream-state-v2.json
2297
+ "cache",
2298
+ // npm-view-cache and friends
2299
+ "logs",
2300
+ // post-tool-use logs, jsonl observations
2301
+ "run",
2302
+ // runtime lock files / pid markers
2303
+ ".ijfw"
2304
+ // internal — recall counter, indexes, layout version
2305
+ ];
2164
2306
  const backupDir = dir + ".bak." + Date.now();
2165
2307
  renameSync3(dir, backupDir);
2166
2308
  try {
2167
2309
  const r = spawnSync2("git", ["clone", "--depth", "1", "--branch", branch, DEFAULT_REPO, dir], { stdio: "inherit" });
2168
2310
  if (r.status !== 0) throw new Error(`IJFW repo fetch did not complete (exit ${r.status}) -- check network access and retry.`);
2169
- for (const item of ["memory", "sessions", "install.log", ".session-counter"]) {
2311
+ let restoredCount = 0;
2312
+ for (const item of RESTORE_ALLOWLIST) {
2170
2313
  const src = join5(backupDir, item);
2171
2314
  if (existsSync5(src)) {
2172
2315
  const dst = join5(dir, item);
2173
- if (existsSync5(dst)) rmSync(dst, { recursive: true, force: true });
2174
- renameSync3(src, dst);
2316
+ if (existsSync5(dst)) rmSync2(dst, { recursive: true, force: true });
2317
+ try {
2318
+ cpSync2(src, dst, { recursive: true, dereference: false });
2319
+ rmSync2(src, { recursive: true, force: true });
2320
+ restoredCount++;
2321
+ } catch (cpErr) {
2322
+ const msg = cpErr && cpErr.message ? cpErr.message : String(cpErr);
2323
+ throw new Error(
2324
+ `IJFW restore: cpSync failed for "${item}" (${msg}). Your data is still intact under: ${backupDir}. Move it back into ${dir} manually after diagnosing the copy failure.`
2325
+ );
2326
+ }
2175
2327
  }
2176
2328
  }
2177
- rmSync(backupDir, { recursive: true, force: true });
2329
+ let backupResidual = [];
2330
+ try {
2331
+ backupResidual = readdirSync2(backupDir);
2332
+ } catch {
2333
+ }
2334
+ if (backupResidual.length === 0) {
2335
+ rmSync2(backupDir, { recursive: true, force: true });
2336
+ } else {
2337
+ console.warn(
2338
+ ` [!] restored ${restoredCount} known dirs; backup retained at ${backupDir} (contains: ${backupResidual.slice(0, 8).join(", ")}${backupResidual.length > 8 ? ", ..." : ""}). Remove manually after verifying.`
2339
+ );
2340
+ }
2178
2341
  return "updated";
2179
2342
  } catch (err) {
2180
- if (existsSync5(dir)) rmSync(dir, { recursive: true, force: true });
2343
+ if (existsSync5(dir)) rmSync2(dir, { recursive: true, force: true });
2181
2344
  renameSync3(backupDir, dir);
2182
2345
  throw err;
2183
2346
  }
@@ -2208,8 +2371,13 @@ async function main() {
2208
2371
  const sigint = () => {
2209
2372
  if (createdThisRun && existsSync5(target)) {
2210
2373
  try {
2211
- rmSync(target, { recursive: true, force: true });
2212
- } catch {
2374
+ rmSync2(target, { recursive: true, force: true });
2375
+ } catch (err) {
2376
+ const msg = err && err.message ? err.message : String(err);
2377
+ console.warn(
2378
+ `
2379
+ [!] partial install at ${target} could not be cleaned (${msg}) \u2014 run \`rm -rf "${target}"\` (or Remove-Item -Recurse -Force on Windows) before retrying.`
2380
+ );
2213
2381
  }
2214
2382
  }
2215
2383
  process.exit(130);
package/docs/GUIDE.md CHANGED
@@ -502,7 +502,7 @@ cat ~/.ijfw/dashboard.port
502
502
 
503
503
  ### Still stuck
504
504
 
505
- Every install writes a log to `~/.ijfw/install.log`. Every session writes observations to `~/.ijfw/observations.jsonl`. Open an issue at [gitlab.com/therealseandonahoe/ijfw/-/issues](https://gitlab.com/therealseandonahoe/ijfw/-/issues) with both files redacted and attached.
505
+ Every install writes a log to `~/.ijfw/install.log`. Every session writes observations to `~/.ijfw/observations.jsonl`. Open an issue at [github.com/FerroxLabs/ijfw/issues](https://github.com/FerroxLabs/ijfw/issues) with both files redacted and attached.
506
506
 
507
507
  ---
508
508
 
@@ -545,7 +545,7 @@ Yes. `.ijfw/team/` is git-committed by default. Decisions, patterns, and stack c
545
545
  </p>
546
546
 
547
547
  <p align="center">
548
- <a href="https://gitlab.com/therealseandonahoe/ijfw">gitlab.com/therealseandonahoe/ijfw</a>
548
+ <a href="https://github.com/FerroxLabs/ijfw">github.com/FerroxLabs/ijfw</a>
549
549
  &nbsp;|&nbsp;
550
550
  <a href="https://www.npmjs.com/package/@ijfw/install">npm</a>
551
551
  &nbsp;|&nbsp;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ijfw/install",
3
- "version": "1.5.3",
3
+ "version": "1.5.5",
4
4
  "description": "One-command installer for IJFW -- the AI efficiency layer. One install, every AI coding agent, zero config.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,8 @@
13
13
  "src/install.ps1",
14
14
  "docs/GUIDE.md",
15
15
  "docs/guide/assets",
16
+ "scripts/pack-hub-extension.js",
17
+ "scripts/hub-extension/**",
16
18
  "README.md",
17
19
  "CHANGELOG.md",
18
20
  "LICENSE"
@@ -20,9 +22,12 @@
20
22
  "scripts": {
21
23
  "build": "node scripts/build.js",
22
24
  "test": "node --test test.js",
25
+ "test:hub-extension": "node --test test/test-pack-hub-extension.js",
23
26
  "preflight": "node dist/ijfw.js preflight",
24
27
  "pack:check": "npm pack --dry-run",
25
- "prepublishOnly": "npm run build && npm run preflight"
28
+ "pack:hub-extension": "node scripts/pack-hub-extension.js",
29
+ "prepack": "node -e \"const fs=require('fs'),path=require('path');const d='dist';if(fs.existsSync(d)){for(const f of fs.readdirSync(d)){if(/^ijfw-\\d+\\.\\d+\\.\\d+\\.(zip|sha512)$/.test(f)){fs.rmSync(path.join(d,f))}}}\"",
30
+ "prepublishOnly": "npm run build && npm run preflight && npm run pack:check"
26
31
  },
27
32
  "devDependencies": {
28
33
  "esbuild": "^0.28.0",
@@ -46,13 +51,19 @@
46
51
  ],
47
52
  "license": "MIT",
48
53
  "author": "Sean Donahoe",
49
- "homepage": "https://gitlab.com/therealseandonahoe/ijfw",
54
+ "contributors": [
55
+ {
56
+ "name": "Ferrox Labs",
57
+ "url": "https://ferroxlabs.com"
58
+ }
59
+ ],
60
+ "homepage": "https://github.com/FerroxLabs/ijfw",
50
61
  "bugs": {
51
- "url": "https://gitlab.com/therealseandonahoe/ijfw/-/issues"
62
+ "url": "https://github.com/FerroxLabs/ijfw/issues"
52
63
  },
53
64
  "repository": {
54
65
  "type": "git",
55
- "url": "git+https://gitlab.com/therealseandonahoe/ijfw.git"
66
+ "url": "git+https://github.com/FerroxLabs/ijfw.git"
56
67
  },
57
68
  "publishConfig": {
58
69
  "access": "public"
@@ -0,0 +1,156 @@
1
+ {
2
+ "name": "ijfw",
3
+ "displayName": "IJFW — AI Efficiency Layer",
4
+ "version": "{{VERSION}}",
5
+ "description": "One install, every AI coding agent, zero config. Unifies 15 CLIs under a shared MCP memory layer so context follows you across Claude, Codex, Gemini, Cursor, Windsurf, and 10 more.",
6
+ "author": "Sean Donahoe",
7
+ "icon": "assets/ijfw-logo.svg",
8
+ "engines": {
9
+ "wayland": ">=0.6.0"
10
+ },
11
+ "hubs": ["acpAdapters", "mcpServers"],
12
+ "lifecycle": {
13
+ "onInstall": "scripts/install.js",
14
+ "onUninstall": "scripts/uninstall.js"
15
+ },
16
+ "contributes": {
17
+ "acpAdapters": [
18
+ {
19
+ "id": "claude",
20
+ "name": "Claude Code",
21
+ "description": "Anthropic's Claude Code agentic coding CLI with IJFW memory + plugin layer",
22
+ "connectionType": "cli",
23
+ "cliCommand": "claude",
24
+ "defaultCliPath": "npx @anthropic-ai/claude-code",
25
+ "supportsStreaming": true
26
+ },
27
+ {
28
+ "id": "codex",
29
+ "name": "OpenAI Codex CLI",
30
+ "description": "OpenAI Codex CLI with IJFW MCP memory, hooks, skills, and command aliases",
31
+ "connectionType": "cli",
32
+ "cliCommand": "codex",
33
+ "defaultCliPath": "npx @openai/codex",
34
+ "supportsStreaming": true
35
+ },
36
+ {
37
+ "id": "gemini",
38
+ "name": "Gemini CLI",
39
+ "description": "Google Gemini CLI with IJFW MCP memory, extension bundle, and hooks",
40
+ "connectionType": "cli",
41
+ "cliCommand": "gemini",
42
+ "defaultCliPath": "npx @google/gemini-cli",
43
+ "supportsStreaming": true
44
+ },
45
+ {
46
+ "id": "cursor",
47
+ "name": "Cursor",
48
+ "description": "Cursor AI editor with IJFW MCP memory and project rules",
49
+ "connectionType": "cli",
50
+ "cliCommand": "cursor",
51
+ "defaultCliPath": "cursor",
52
+ "supportsStreaming": false
53
+ },
54
+ {
55
+ "id": "windsurf",
56
+ "name": "Windsurf",
57
+ "description": "Windsurf AI editor with IJFW MCP memory and project rules",
58
+ "connectionType": "cli",
59
+ "cliCommand": "windsurf",
60
+ "defaultCliPath": "windsurf",
61
+ "supportsStreaming": false
62
+ },
63
+ {
64
+ "id": "copilot",
65
+ "name": "GitHub Copilot",
66
+ "description": "GitHub Copilot (VS Code) with IJFW MCP memory and copilot-instructions",
67
+ "connectionType": "cli",
68
+ "cliCommand": "code",
69
+ "defaultCliPath": "code",
70
+ "supportsStreaming": false
71
+ },
72
+ {
73
+ "id": "hermes",
74
+ "name": "Hermes CLI",
75
+ "description": "Hermes CLI with IJFW MCP memory, skills, plugin, and tier-2 hook",
76
+ "connectionType": "cli",
77
+ "cliCommand": "hermes",
78
+ "defaultCliPath": "npx @hermes-ai/cli",
79
+ "supportsStreaming": true
80
+ },
81
+ {
82
+ "id": "wayland",
83
+ "name": "Wayland CLI",
84
+ "description": "Wayland CLI with IJFW MCP memory, skills, plugin, and tier-2 hook",
85
+ "connectionType": "cli",
86
+ "cliCommand": "wayland",
87
+ "defaultCliPath": "npx @wayland-ai/cli",
88
+ "supportsStreaming": true
89
+ },
90
+ {
91
+ "id": "aider",
92
+ "name": "Aider",
93
+ "description": "Aider AI pair programmer with IJFW rules (aider.conf.yml + CONVENTIONS.md)",
94
+ "connectionType": "cli",
95
+ "cliCommand": "aider",
96
+ "defaultCliPath": "npx aider-chat",
97
+ "supportsStreaming": false
98
+ },
99
+ {
100
+ "id": "opencode",
101
+ "name": "OpenCode",
102
+ "description": "OpenCode CLI with IJFW MCP memory (opencode mcp.local schema)",
103
+ "connectionType": "cli",
104
+ "cliCommand": "opencode",
105
+ "defaultCliPath": "npx opencode-ai",
106
+ "supportsStreaming": true
107
+ },
108
+ {
109
+ "id": "qwencode",
110
+ "name": "Qwen Code",
111
+ "description": "Alibaba Qwen Code CLI with IJFW MCP memory",
112
+ "connectionType": "cli",
113
+ "cliCommand": "qwen",
114
+ "defaultCliPath": "npx @qwen-ai/qwen-code",
115
+ "supportsStreaming": true
116
+ },
117
+ {
118
+ "id": "cline",
119
+ "name": "Cline",
120
+ "description": "Cline VS Code extension with IJFW MCP memory (globalStorage schema)",
121
+ "connectionType": "cli",
122
+ "cliCommand": "cline",
123
+ "defaultCliPath": "cline",
124
+ "supportsStreaming": false
125
+ },
126
+ {
127
+ "id": "kimicode",
128
+ "name": "Kimi Code",
129
+ "description": "Moonshot Kimi Code CLI with IJFW MCP memory",
130
+ "connectionType": "cli",
131
+ "cliCommand": "kimi",
132
+ "defaultCliPath": "npx @moonshot-ai/kimi-code",
133
+ "supportsStreaming": true
134
+ },
135
+ {
136
+ "id": "openclaw",
137
+ "name": "OpenClaw",
138
+ "description": "OpenClaw CLI with IJFW MCP memory (mcp.servers schema)",
139
+ "connectionType": "cli",
140
+ "cliCommand": "openclaw",
141
+ "defaultCliPath": "npx @openclaw-ai/cli",
142
+ "supportsStreaming": true
143
+ },
144
+ {
145
+ "id": "antigravity",
146
+ "name": "Antigravity",
147
+ "description": "Antigravity AI IDE (ex-Windsurf team) with IJFW MCP memory (IDE + CLI paths)",
148
+ "connectionType": "cli",
149
+ "cliCommand": "agy",
150
+ "defaultCliPath": "npx @antigravity-ai/cli",
151
+ "supportsStreaming": true
152
+ }
153
+ ],
154
+ "mcpServers": ["ijfw-memory"]
155
+ }
156
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2
+ <circle cx="16" cy="16" r="16" fill="#ff6b35"/>
3
+ <text x="16" y="21" font-family="sans-serif" font-size="10" font-weight="bold" fill="white" text-anchor="middle">IJFW</text>
4
+ </svg>