@actions-json/bridge 0.1.186 → 0.1.187

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/bin/cli.js CHANGED
@@ -10,6 +10,7 @@ const path = require('node:path');
10
10
  const { spawn } = require('node:child_process');
11
11
  const { ensureBinary } = require('../lib/install');
12
12
  const { ensureStorageRoot } = require('../lib/storage');
13
+ const { install } = require('../lib/register');
13
14
 
14
15
  // The primitive dictionary (browser-control tool catalog) ships with this
15
16
  // package — it's a fixed runtime file, not user config. When the caller runs a
@@ -40,6 +41,13 @@ function withDefaults(args) {
40
41
  }
41
42
 
42
43
  async function main() {
44
+ // `install` is a wrapper-side subcommand (register the MCP server with the
45
+ // local coding agents); it does not touch the bridge binary or launch it.
46
+ if (process.argv[2] === 'install') {
47
+ process.exit(install());
48
+ return;
49
+ }
50
+
43
51
  let bin;
44
52
  try {
45
53
  bin = await ensureBinary();
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ // `npx @actions-json/bridge install` — register the actions-json MCP server with
4
+ // whatever coding agents are present, by calling each agent's own `mcp add`
5
+ // command. This removes the manual "copy the right claude/codex command" step
6
+ // from the install: we detect the agent, run its registrar, and report.
7
+ //
8
+ // Design: we shell out to each agent's OWN CLI (`claude mcp add …`,
9
+ // `codex mcp add …`) rather than hand-editing their config files, so the agent
10
+ // stays the source of truth for its own config format. Idempotent-ish: agents
11
+ // treat re-adding an existing server as an error, which we catch and report as
12
+ // "already registered" rather than failing the whole run.
13
+
14
+ const { execFileSync, execSync } = require('node:child_process');
15
+
16
+ // The canonical launch command every agent should register.
17
+ const SERVER_NAME = 'actions-json';
18
+ const LAUNCH = ['npx', '-y', '@actions-json/bridge', 'mcp'];
19
+
20
+ // Each supported agent: how to detect its CLI, and how to register a stdio MCP
21
+ // server with it. `addArgs` produces the argv for a spawn of `bin`.
22
+ const AGENTS = [
23
+ {
24
+ key: 'claude',
25
+ label: 'Claude Code',
26
+ bin: 'claude',
27
+ listArgs: () => ['mcp', 'list'],
28
+ addArgs: () => ['mcp', 'add', SERVER_NAME, '--', ...LAUNCH],
29
+ manual: `claude mcp add ${SERVER_NAME} -- ${LAUNCH.join(' ')}`,
30
+ },
31
+ {
32
+ key: 'codex',
33
+ label: 'Codex',
34
+ bin: 'codex',
35
+ listArgs: () => ['mcp', 'list'],
36
+ addArgs: () => ['mcp', 'add', SERVER_NAME, '--', ...LAUNCH],
37
+ manual: `codex mcp add ${SERVER_NAME} -- ${LAUNCH.join(' ')}`,
38
+ },
39
+ ];
40
+
41
+ // Is a CLI on PATH? Cross-platform: `command -v` on posix, `where` on win32.
42
+ function onPath(bin) {
43
+ try {
44
+ if (process.platform === 'win32') {
45
+ execSync(`where ${bin}`, { stdio: 'ignore' });
46
+ } else {
47
+ execSync(`command -v ${bin}`, { stdio: 'ignore', shell: '/bin/sh' });
48
+ }
49
+ return true;
50
+ } catch (_e) {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ // Is the actions-json server ALREADY registered with this agent? We check first
56
+ // and never overwrite an existing config — a re-run must be a no-op, not a
57
+ // clobber (an install should only touch config when there's a reason to).
58
+ function alreadyRegistered(agent) {
59
+ if (!agent.listArgs) return false;
60
+ try {
61
+ const out = execFileSync(agent.bin, agent.listArgs(), { stdio: 'pipe' }).toString();
62
+ return out.includes(SERVER_NAME);
63
+ } catch (_e) {
64
+ // If listing fails we can't confirm; fall through to the add path, which
65
+ // still catches an "already exists" error rather than overwriting.
66
+ return false;
67
+ }
68
+ }
69
+
70
+ // Register with one agent. Returns { agent, status, detail }.
71
+ function registerOne(agent) {
72
+ if (!onPath(agent.bin)) {
73
+ return { agent: agent.label, status: 'absent' };
74
+ }
75
+ // Never replace an existing registration — skip if it's already there.
76
+ if (alreadyRegistered(agent)) {
77
+ return { agent: agent.label, status: 'already' };
78
+ }
79
+ try {
80
+ execFileSync(agent.bin, agent.addArgs(), { stdio: 'pipe' });
81
+ return { agent: agent.label, status: 'registered' };
82
+ } catch (e) {
83
+ // Belt-and-suspenders: if the add still reports the name exists (race, or an
84
+ // agent we couldn't list), treat as already — do NOT clobber. Anything else
85
+ // is a real failure we surface with the manual command.
86
+ const out = `${e.stdout || ''}${e.stderr || ''}`.toLowerCase();
87
+ if (out.includes('already') || out.includes('exist')) {
88
+ return { agent: agent.label, status: 'already' };
89
+ }
90
+ return { agent: agent.label, status: 'failed', detail: agent.manual };
91
+ }
92
+ }
93
+
94
+ // Detect + register across all known agents; print a human summary. Returns an
95
+ // exit code (0 if at least one agent got the server, 1 if none did).
96
+ function install() {
97
+ const results = AGENTS.map(registerOne);
98
+ const present = results.filter((r) => r.status !== 'absent');
99
+
100
+ process.stdout.write('\nactions.json — MCP registration\n');
101
+ for (const r of results) {
102
+ if (r.status === 'absent') continue;
103
+ const mark =
104
+ r.status === 'registered' ? '✓ registered' :
105
+ r.status === 'already' ? '✓ already registered' :
106
+ '✗ failed';
107
+ process.stdout.write(` ${mark}: ${r.agent}\n`);
108
+ if (r.status === 'failed') {
109
+ process.stdout.write(` run manually: ${r.detail}\n`);
110
+ }
111
+ }
112
+
113
+ if (present.length === 0) {
114
+ process.stdout.write(
115
+ ' No supported coding agent (Claude Code or Codex) found on PATH.\n' +
116
+ ' Install one, then re-run `npx @actions-json/bridge install`, or add it manually:\n'
117
+ );
118
+ for (const a of AGENTS) {
119
+ process.stdout.write(` ${a.manual}\n`);
120
+ }
121
+ } else {
122
+ const ok = present.some((r) => r.status === 'registered' || r.status === 'already');
123
+ if (ok) {
124
+ process.stdout.write(
125
+ '\nDone. Restart (or reconnect) your agent so it launches the server, then\n' +
126
+ 'install the browser extension and connect it. See:\n' +
127
+ ' https://yaniv256.github.io/actions.json/getting-started.html\n'
128
+ );
129
+ }
130
+ }
131
+ process.stdout.write('\n');
132
+
133
+ return present.length > 0 &&
134
+ present.some((r) => r.status === 'registered' || r.status === 'already')
135
+ ? 0
136
+ : 1;
137
+ }
138
+
139
+ module.exports = { install, AGENTS, SERVER_NAME, onPath, registerOne };
package/lib/storage.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const fs = require('node:fs');
4
4
  const os = require('node:os');
5
5
  const path = require('node:path');
6
+ const { execFileSync } = require('node:child_process');
6
7
 
7
8
  // Default storage location when the caller doesn't pass --storage-root.
8
9
  // Lives in the user's home dir (not cwd) so it resolves no matter where the
@@ -14,39 +15,58 @@ function defaultStorageRoot() {
14
15
  );
15
16
  }
16
17
 
17
- const TEMPLATE_DIR = path.join(__dirname, '..', 'template');
18
+ // The bundled seed skeleton (storage.json + scope READMEs + private placeholder),
19
+ // mirroring examples/actions.json.storage. The PUBLIC site maps are NOT bundled;
20
+ // they are cloned from the public maps repo on first run (see clonePublicMaps)
21
+ // so a fresh install arrives stocked with real, working maps to try.
22
+ const SEED_DIR = path.join(__dirname, '..', 'seed');
23
+ const PUBLIC_MAPS_REPO = 'https://github.com/yaniv256/actions.json.storage.public.git';
18
24
 
19
- // Recursively copy the template, skipping the .gitkeep placeholders (they only
20
- // exist to ship empty dirs inside the package/repo).
21
- function copyTemplate(src, dst) {
25
+ // Recursively copy the seed skeleton into the target.
26
+ function copySeed(src, dst) {
22
27
  fs.mkdirSync(dst, { recursive: true });
23
28
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
24
- if (entry.name === '.gitkeep') continue;
25
29
  const s = path.join(src, entry.name);
26
30
  const d = path.join(dst, entry.name);
27
31
  if (entry.isDirectory()) {
28
- copyTemplate(s, d);
32
+ copySeed(s, d);
29
33
  } else {
30
34
  fs.copyFileSync(s, d);
31
35
  }
32
36
  }
33
37
  }
34
38
 
35
- // Ensure the storage root exists; create it from the bundled template on
36
- // first run. Returns the absolute path. Never overwrites existing storage.
39
+ // Clone the public site maps into scopes/public so the workspace ships with
40
+ // something to try. Best-effort: if git is missing or the clone fails (offline,
41
+ // etc.), seed an empty scopes/public and tell the user how to get the maps —
42
+ // never fail the whole seed over the optional maps.
43
+ function clonePublicMaps(target) {
44
+ const publicDir = path.join(target, 'scopes', 'public');
45
+ if (fs.existsSync(publicDir) && fs.readdirSync(publicDir).length > 0) return;
46
+ try {
47
+ execFileSync('git', ['clone', '--depth', '1', PUBLIC_MAPS_REPO, publicDir], {
48
+ stdio: 'ignore',
49
+ });
50
+ process.stderr.write(`Cloned public site maps into ${publicDir}\n`);
51
+ } catch (_e) {
52
+ fs.mkdirSync(publicDir, { recursive: true });
53
+ process.stderr.write(
54
+ `Could not clone public site maps (need git + network). ` +
55
+ `Get them later with:\n git clone ${PUBLIC_MAPS_REPO} "${publicDir}"\n`
56
+ );
57
+ }
58
+ }
59
+
60
+ // Ensure the storage root exists; create it from the bundled seed on first run,
61
+ // then clone the public maps. Returns the absolute path. Never overwrites
62
+ // existing storage.
37
63
  function ensureStorageRoot(root) {
38
64
  const target = root || defaultStorageRoot();
39
65
  if (!fs.existsSync(target)) {
40
66
  fs.mkdirSync(path.dirname(target), { recursive: true });
41
- copyTemplate(TEMPLATE_DIR, target);
42
- // Recreate the empty scope dirs that .gitkeep stood in for.
43
- for (const p of [
44
- ['scopes', 'private', 'sites'],
45
- ['scopes', 'public', 'sites'],
46
- ['scopes', 'shared'],
47
- ]) {
48
- fs.mkdirSync(path.join(target, ...p), { recursive: true });
49
- }
67
+ copySeed(SEED_DIR, target);
68
+ fs.mkdirSync(path.join(target, 'scopes', 'private'), { recursive: true });
69
+ clonePublicMaps(target);
50
70
  process.stderr.write(`Created actions.json storage at ${target}\n`);
51
71
  }
52
72
  return path.resolve(target);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actions-json/bridge",
3
- "version": "0.1.186",
3
+ "version": "0.1.187",
4
4
  "bridgeBinaryVersion": "0.1.186",
5
5
  "description": "Run the actions.json MCP bridge with npx — no Rust toolchain required. Downloads the prebuilt actions-json-mcp binary, bundles the primitive dictionary, and creates your storage workspace on first run.",
6
6
  "license": "MIT",
@@ -17,7 +17,7 @@
17
17
  "bin/",
18
18
  "lib/",
19
19
  "dictionary/",
20
- "template/",
20
+ "seed/",
21
21
  "README.md"
22
22
  ],
23
23
  "engines": {
package/seed/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # actions.json.storage
2
+
3
+ Your `actions.json.storage` workspace — the root/index that records storage configuration, mounted
4
+ scopes, and layout. It was created here on first run of `@actions-json/bridge`.
5
+
6
+ ## Layout
7
+
8
+ ```text
9
+ storage.json Root manifest (mounts, visibility, sync policy)
10
+ scopes/
11
+ public/ Reviewed, reusable public site maps (cloned from the public maps repo)
12
+ private/ Your OWN owner-only storage — local by default (see its README)
13
+ ```
14
+
15
+ ## ⚠️ Privacy
16
+
17
+ `scopes/private` is where your browsing memory, observations, and unreviewed maps live. It is **local
18
+ and private by default** here in your home directory. If you later put this workspace under version
19
+ control, putting files in `scopes/private` does NOT make them private on its own — keep the repo
20
+ private, or mount `scopes/private` from your own **private** repo. See `scopes/private/README.md`.
21
+
22
+ ## The public maps
23
+
24
+ `scopes/public` is cloned from
25
+ [actions.json.storage.public](https://github.com/yaniv256/actions.json.storage.public) so you have
26
+ working site maps to try immediately. Update them with `git -C scopes/public pull`, or repoint the
27
+ `public` mount in `storage.json` at your own public repo.
@@ -0,0 +1,10 @@
1
+ # scopes/
2
+
3
+ Each subfolder is a **mounted scope** — a separate storage area with its own visibility. Scopes are
4
+ usually git submodules pointing at their own repositories, so they can have different access than this
5
+ root repo.
6
+
7
+ - **public/** — reviewed, reusable public site-action maps (a git submodule to a public repo).
8
+ - **private/** — your OWN owner-only storage (see its README). Not included in this public example.
9
+
10
+ Add further scopes (e.g. `shared/<audience>/`) as separate repositories when you need them.
@@ -0,0 +1,23 @@
1
+ # scopes/private/ — placeholder
2
+
3
+ This is a **placeholder folder**, intentionally empty in the public example. Your private scope holds
4
+ owner-only browsing memory, observations, run logs, item indexes, and unreviewed action maps — content
5
+ that should never be public.
6
+
7
+ ## ⚠️ Enforce privacy — two options
8
+
9
+ Putting files in this folder does NOT make them private on its own. Choose one:
10
+
11
+ 1. **Keep the whole `actions.json.storage` repo private** (set the forked repo to Private). Then this
12
+ folder and its contents stay private along with everything else.
13
+ 2. **Mount this folder from your OWN private repo** as a submodule, so private content lives in a
14
+ separate private repository even if the root repo is public:
15
+
16
+ ```bash
17
+ git submodule add https://github.com/YOUR-ORG/your-actions.json.storage.private.git scopes/private
18
+ ```
19
+
20
+ and add a matching `private` mount to `storage.json`.
21
+
22
+ If you do neither and commit private files into a public repo, they will be exposed. The public example
23
+ links to no private repo, so nothing here is exposed by default.
@@ -13,13 +13,14 @@
13
13
  "mount_type": "local",
14
14
  "visibility": "private",
15
15
  "default": true,
16
- "description": "Your owner-only browsing memory, observations, overlays, run logs, item indexes, and unreviewed action maps. Local by default; point it at your own private repo to sync the same memory across your machines and agents."
16
+ "description": "Your owner-only browsing memory, observations, overlays, run logs, item indexes, and unreviewed action maps. Local by default; point it at your OWN private repo to sync the same memory across your machines and agents (see scopes/private/README.md)."
17
17
  },
18
18
  "public": {
19
19
  "path": "scopes/public",
20
- "mount_type": "local",
20
+ "mount_type": "git_clone",
21
+ "repo": "https://github.com/yaniv256/actions.json.storage.public.git",
21
22
  "visibility": "public",
22
- "description": "Reviewed, redacted maps you publish openly. Point this at your own public repo when you want to share."
23
+ "description": "Reviewed, reusable public site-action maps (Trello, Gmail, Docs, GitHub, LinkedIn, Calendar, …). Cloned from the public maps repo on first run; pull to update, or point this at your own public repo."
23
24
  }
24
25
  },
25
26
  "default_policy": {
@@ -1,74 +0,0 @@
1
- # actions.json storage
2
-
3
- This **is** your `actions.json.storage` workspace — operational, not an example.
4
- The bridge created it on first run (default: `~/.actions-json/storage`) and reads
5
- and writes here as your agent browses. Your browsing memory, observations, and
6
- action maps live in these folders.
7
-
8
- ## Scopes
9
-
10
- Storage is organized by visibility. Each scope is a folder under `scopes/`; the
11
- runtime discovers site maps under `scopes/<scope>/sites/<host>/`.
12
-
13
- | Scope | What goes here | Git |
14
- |---|---|---|
15
- | `private/` | Your owner-only browsing memory, observations, run logs, and unreviewed maps. | Local by default. Opt in: back it with your own **private** repo to sync across your machines and agents. |
16
- | `public/` | Reviewed, redacted maps **you publish** openly. | Opt in: point it at your own public repo. |
17
- | `shared/<name>/` | Maps shared with a specific collaborator, **and** maps you pull **from** a source. | Opt in: one git remote per source. |
18
-
19
- `private` and `public` are set up. Add `shared/<name>/` when you need it.
20
-
21
- ## Versioning a scope
22
-
23
- Each scope is an ordinary folder, local until you choose to version it. Turn one
24
- into a git repo when you want to sync or share it.
25
-
26
- **Sync your private memory across machines** — back `scopes/private/` with your
27
- own **private** repo so the same browsing memory follows your agent everywhere:
28
-
29
- ```bash
30
- cd ~/.actions-json/storage/scopes/private
31
- git init && git add . && git commit -m "my private actions.json memory"
32
- git remote add origin https://github.com/<you>/<your-private-storage>.git # private!
33
- git push -u origin main
34
- # on another machine: clone it into scopes/private instead of git init
35
- ```
36
-
37
- This stays private — it is your repo, set to private. It only ever leaves your
38
- machine if you point it at a remote, and `public`/`shared` promotion still
39
- requires explicit review.
40
-
41
- **Publish your public maps** — make `scopes/public/` your own public repo:
42
-
43
- ```bash
44
- cd ~/.actions-json/storage/scopes/public
45
- git init && git add . && git commit -m "my public actions.json maps"
46
- git remote add origin https://github.com/<you>/<your-public-storage>.git
47
- git push -u origin main
48
- ```
49
-
50
- **Pull the official map library** — vendor maps *from* a source into a shared
51
- scope (the runtime discovers maps under `scopes/shared/<name>/`):
52
-
53
- ```bash
54
- cd ~/.actions-json/storage/scopes/shared
55
- git clone https://github.com/yaniv256/actions.json.storage.public.git actions-json
56
- # maps now resolve from scopes/shared/actions-json/sites/<host>/
57
- ```
58
-
59
- Pulling from someone else's library uses the **shared** scope (inbound), not
60
- `public` (which is for what *you* publish). Keep that boundary clear.
61
-
62
- ## Layout
63
-
64
- ```text
65
- storage.json Root manifest — which scopes are mounted
66
- scopes/
67
- private/
68
- scope.json Private scope manifest
69
- sites/<host>/ Per-site maps, observations, overlays (local; sync via a private repo)
70
- public/
71
- scope.json Public scope manifest
72
- sites/<host>/ Reviewed, redacted maps you publish
73
- shared/<name>/ Add per-collaborator or per-source as needed
74
- ```
@@ -1,17 +0,0 @@
1
- {
2
- "protocol": "actions.json.storage.scope",
3
- "version": "0.1.0",
4
- "scope": "private",
5
- "parent": "actions.json.storage",
6
- "owner": {
7
- "type": "person",
8
- "id": "you"
9
- },
10
- "visibility": "private",
11
- "default_policy": {
12
- "observations": "store_freely",
13
- "runs": "store_freely",
14
- "overlays": "store_freely",
15
- "action_maps": "draft_until_reviewed"
16
- }
17
- }
File without changes
@@ -1,17 +0,0 @@
1
- {
2
- "protocol": "actions.json.storage.scope",
3
- "version": "0.1.0",
4
- "scope": "public",
5
- "parent": "actions.json.storage",
6
- "owner": {
7
- "type": "person",
8
- "id": "you"
9
- },
10
- "visibility": "public",
11
- "default_policy": {
12
- "observations": "do_not_store_raw_private_observations",
13
- "reports": "reviewed_public_artifact",
14
- "action_maps": "reviewed_and_redacted",
15
- "promotion": "review_required"
16
- }
17
- }
File without changes
File without changes