clario-agent 0.1.0 → 0.3.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.
- package/README.md +55 -51
- package/clario-agent-hook.mjs +120 -36
- package/cli.mjs +189 -93
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,70 +1,74 @@
|
|
|
1
1
|
# clario-agent: show your AI agent's activity in Clario 🧑🍳
|
|
2
2
|
|
|
3
|
-
A Claude Code
|
|
4
|
-
Clario space. Others see a pulsing badge above your avatar while
|
|
5
|
-
clicking your avatar shows your agent card: what
|
|
6
|
-
and commit (e.g. `graphite · feat/missions · a1b2c3d`). The badge
|
|
3
|
+
A hook for **Claude Code, Codex and Cursor** that shows what your coding agent is working on
|
|
4
|
+
to everyone in your current Clario space. Others see a pulsing badge above your avatar while
|
|
5
|
+
the agent works, and clicking your avatar shows your agent card: which agent, what it's doing,
|
|
6
|
+
and the project, branch and commit (e.g. `graphite · feat/missions · a1b2c3d`). The badge
|
|
7
|
+
clears when the agent finishes.
|
|
7
8
|
|
|
8
9
|
## Setup (one command)
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
1. Open your space on [clario.games](https://www.clario.games) and click **Agent** in the
|
|
12
|
+
left menu. Pick your agent; it shows your setup command, with your key in it:
|
|
13
|
+
```bash
|
|
14
|
+
npx clario-agent setup --key YOUR_KEY # Claude Code
|
|
15
|
+
npx clario-agent setup --key YOUR_KEY --agent codex # Codex
|
|
16
|
+
npx clario-agent setup --key YOUR_KEY --agent cursor # Cursor
|
|
17
|
+
```
|
|
18
|
+
2. Copy it and run it in the terminal, inside the project you want to share.
|
|
19
|
+
`--agent all` sets up all three; `--agent codex,cursor` works too.
|
|
20
|
+
|
|
21
|
+
That's it: the Agent button disappears once your agent has connected. Setup:
|
|
22
|
+
|
|
23
|
+
- **Checks the key** with the Clario server and saves it in `~/.clario/agent.json` (readable
|
|
24
|
+
only by you). It's your own key and it stays the same: the Agent button always shows the
|
|
25
|
+
same command for you. It can only post agent activity: it can't sign in, open a space or
|
|
26
|
+
change anything else. No password is ever typed in the terminal.
|
|
27
|
+
- **Installs the hook** at `~/.clario/hook.mjs`.
|
|
28
|
+
- **Registers it with your agent:**
|
|
29
|
+
|
|
30
|
+
| Agent | Hook file | Shared from |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| Claude Code | `.claude/settings.local.json` in this project (`~/.claude/settings.json` with `--global`) | this project |
|
|
33
|
+
| Codex | `~/.codex/hooks.json` (or `$CODEX_HOME/hooks.json`) | the projects you ran setup in |
|
|
34
|
+
| Cursor | `~/.cursor/hooks.json` | the projects you ran setup in |
|
|
35
|
+
|
|
36
|
+
Codex and Cursor read one hook file for every project, so the hook itself only reports
|
|
37
|
+
from the projects you ran setup in; add `--global` to share from all of them.
|
|
38
|
+
- **Codex** asks before it runs a new hook: open Codex, run `/hooks` and trust the Clario
|
|
39
|
+
hook. **Cursor**: restart it if it was open.
|
|
40
|
+
|
|
41
|
+
Then give your agent a task in that project and click your avatar. Teammates who do the same
|
|
42
|
+
and join your space show up on their avatars too.
|
|
43
|
+
|
|
44
|
+
To add another project later, run `npx clario-agent setup --agent …` there: it reuses your
|
|
45
|
+
saved key. Setup never registers the hook twice, leaves your other settings and hooks as they
|
|
46
|
+
are, and saves the previous file next to it as `*.clario-backup`.
|
|
12
47
|
|
|
13
48
|
```bash
|
|
14
|
-
npx clario-agent
|
|
49
|
+
npx clario-agent status # server, key, which agents are set up and where
|
|
50
|
+
npx clario-agent uninstall # stop sharing from this project, for every agent
|
|
51
|
+
npx clario-agent uninstall --global # remove every Clario hook and stop sharing everywhere
|
|
15
52
|
```
|
|
16
53
|
|
|
17
|
-
It
|
|
54
|
+
It uses the live server at `https://api.clario.games`. Running Clario yourself? The Agent
|
|
55
|
+
button on your own site adds `--api-url` to the command for you.
|
|
18
56
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
can only post agent activity: it can't sign in, open a space or change anything else.
|
|
22
|
-
2. **Installs the hook** at `~/.clario/hook.mjs`.
|
|
23
|
-
3. **Registers it with Claude Code** for this project only, in `.claude/settings.local.json`.
|
|
24
|
-
Add `--global` to use it in every project (`~/.claude/settings.json`) instead.
|
|
57
|
+
Used the older `viora-agent` package? Run `npx clario-agent setup` once: it reuses the key
|
|
58
|
+
from `~/.viora` and replaces the old hook. You can delete `~/.viora` afterwards.
|
|
25
59
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
It uses the live server at `https://api.clario.games`. Running Clario yourself? Add
|
|
30
|
-
`--api-url http://localhost:3001` (or your own server's address); setup remembers it.
|
|
31
|
-
|
|
32
|
-
Running `setup` again keeps a key that still has more than 14 days left, updates the hook
|
|
33
|
-
and never registers it twice. Your other Claude Code settings and hooks are left as they
|
|
34
|
-
are, and the previous file is saved next to it as `*.clario-backup`.
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npx clario-agent status # server, key expiry, where the hook is registered
|
|
38
|
-
npx clario-agent uninstall # remove the hook from this project (add --global for all)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Used the older `viora-agent` package? Run `setup` once: it reuses the key from `~/.viora` and
|
|
42
|
-
replaces the old hook, so it never runs twice. You can delete `~/.viora` afterwards.
|
|
43
|
-
|
|
44
|
-
From a checkout of this repo, `node tools/agent-hook/setup.mjs` does the same as `setup`.
|
|
45
|
-
For scripts, `CLARIO_EMAIL` and `CLARIO_PASSWORD` are read instead of prompting.
|
|
46
|
-
|
|
47
|
-
<details>
|
|
48
|
-
<summary>Manual alternative: a 7-day login token</summary>
|
|
49
|
-
|
|
50
|
-
Sign in, open DevTools → Console on your Clario site and run
|
|
51
|
-
`copy(localStorage.getItem("metaverse:token"))`, then save it as `"token"` in
|
|
52
|
-
`~/.clario/agent.json` next to `"apiUrl"`. It expires after 7 days. If you then run
|
|
53
|
-
`setup`, it uses that token to create the agent key without asking for your password.
|
|
54
|
-
</details>
|
|
55
|
-
|
|
56
|
-
> An agent key can't be cancelled on its own before it expires. If one leaks, delete it
|
|
57
|
-
> from `~/.clario/agent.json` and change the server's `JWT_SECRET` (this signs everyone out).
|
|
58
|
-
> The server needs the `/api/v1/agent-key` endpoint from this repo's `apps/server`.
|
|
60
|
+
> Keep the setup command private: anyone with your key can post activity as your agent (and
|
|
61
|
+
> nothing else). If it leaks, delete `~/.clario/agent.json`; the server's owner can cancel
|
|
62
|
+
> every key by changing `JWT_SECRET` (this signs everyone out).
|
|
59
63
|
|
|
60
64
|
## How it behaves
|
|
61
65
|
|
|
62
|
-
- While
|
|
63
|
-
|
|
64
|
-
`
|
|
66
|
+
- While your agent works your badge shows "cooking" with a short description of what it's
|
|
67
|
+
doing, e.g. `Claude is using Edit page.tsx`, `Codex is using Shell $ bun, git` or
|
|
68
|
+
`Cursor is editing page.tsx`, and the card says which agent it is
|
|
65
69
|
- Your agent card also shows the project folder, git branch and short commit the agent is
|
|
66
70
|
working in
|
|
67
|
-
- When
|
|
71
|
+
- When the agent finishes, the badge clears automatically
|
|
68
72
|
- If you're not inside a Clario space, updates are silently dropped
|
|
69
73
|
- Updates are throttled (2s) so fast tool bursts don't spam the room
|
|
70
74
|
- The hook never blocks or fails your agent session: all errors are swallowed
|
package/clario-agent-hook.mjs
CHANGED
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Clario agent hook for Claude Code
|
|
3
|
+
* Clario agent hook for Claude Code, Codex and Cursor
|
|
4
4
|
*
|
|
5
5
|
* Broadcasts what your AI agent is doing to everyone in your current Clario space.
|
|
6
6
|
* Setup guide: tools/agent-hook/README.md
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* node hook.mjs [--agent claude-code|codex|cursor-agent] (hook JSON on stdin)
|
|
9
|
+
*
|
|
10
|
+
* Reads the agent's hook events from stdin and POSTs activity to
|
|
9
11
|
* POST /api/v1/agent-activity on your Clario server. Exits 0 silently on any
|
|
10
|
-
* failure so it never blocks your agent session.
|
|
12
|
+
* failure so it never blocks your agent session. Without --agent it's Claude Code.
|
|
11
13
|
*
|
|
12
14
|
* Sent: a short status line (tool name, edited file name, or the programs a shell
|
|
13
|
-
* command runs, never its arguments)
|
|
14
|
-
* commit. `"shareGit": false` in ~/.clario/agent.json (or
|
|
15
|
-
* project, branch and commit on this machine.
|
|
15
|
+
* command runs, never its arguments), which agent it is, and the project folder,
|
|
16
|
+
* git branch and short commit. `"shareGit": false` in ~/.clario/agent.json (or
|
|
17
|
+
* CLARIO_SHARE_GIT=0) keeps project, branch and commit on this machine.
|
|
18
|
+
*
|
|
19
|
+
* Codex and Cursor read their hooks from one file for every project, so for them this
|
|
20
|
+
* only reports from the projects setup was run in (`projects` in the config), unless
|
|
21
|
+
* setup was run with --global (`everyProject`). Cursor reads each hook's stdout as its
|
|
22
|
+
* reply, so it always gets the neutral one: carry on, change nothing.
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
25
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
19
26
|
import { homedir } from "node:os";
|
|
20
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
27
|
+
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
21
28
|
|
|
22
29
|
const CONFIG_PATH = join(homedir(), ".clario", "agent.json");
|
|
23
30
|
const THROTTLE_MS = 2000;
|
|
24
31
|
const STATE_PATH = join(homedir(), ".clario", ".agent-hook-state");
|
|
25
32
|
|
|
33
|
+
/** Agents this hook understands, by the --agent value setup registers, with the name shown in Clario. */
|
|
34
|
+
const AGENTS = { "claude-code": "Claude", codex: "Codex", "cursor-agent": "Cursor" };
|
|
35
|
+
|
|
36
|
+
function agentFromArgs(argv) {
|
|
37
|
+
const i = argv.indexOf("--agent");
|
|
38
|
+
const value = i > -1 ? argv[i + 1] : argv.find((a) => a.startsWith("--agent="))?.slice("--agent=".length);
|
|
39
|
+
return value in AGENTS ? value : "claude-code";
|
|
40
|
+
}
|
|
41
|
+
|
|
26
42
|
function readStdin() {
|
|
27
43
|
return new Promise((resolve) => {
|
|
28
44
|
let raw = "";
|
|
@@ -36,7 +52,7 @@ function readStdin() {
|
|
|
36
52
|
function loadConfig() {
|
|
37
53
|
try {
|
|
38
54
|
const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
|
|
39
|
-
// agentKey (setup
|
|
55
|
+
// agentKey (from setup) wins over a pasted 7-day login token.
|
|
40
56
|
const credential = config?.agentKey || config?.token;
|
|
41
57
|
if (credential && config?.apiUrl) return { ...config, credential };
|
|
42
58
|
} catch {
|
|
@@ -52,6 +68,28 @@ function lastSentAt() {
|
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
/** Working directory of a hook event. Claude Code and Codex send `cwd`; Cursor sends `workspace_roots`. */
|
|
72
|
+
function eventCwd(event) {
|
|
73
|
+
const roots = Array.isArray(event.workspace_roots) ? event.workspace_roots.filter((r) => typeof r === "string" && r) : [];
|
|
74
|
+
return (typeof event.cwd === "string" && event.cwd) || roots[0] || process.env.CURSOR_PROJECT_DIR || process.cwd();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function realPath(path) {
|
|
78
|
+
try {
|
|
79
|
+
return realpathSync(path);
|
|
80
|
+
} catch {
|
|
81
|
+
return resolve(path);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Codex and Cursor: only from the projects setup was run in, or every project after setup --global. */
|
|
86
|
+
function sharedHere(config, agent, cwd) {
|
|
87
|
+
if (agent === "claude-code" || config.everyProject === true) return true; // Claude Code's hook is per project already
|
|
88
|
+
const here = realPath(cwd);
|
|
89
|
+
const projects = Array.isArray(config.projects) ? config.projects : [];
|
|
90
|
+
return projects.some((p) => typeof p === "string" && (here === p || here.startsWith(p.endsWith(sep) ? p : p + sep)));
|
|
91
|
+
}
|
|
92
|
+
|
|
55
93
|
// Git: project, branch and commit, read from .git directly (no git CLI).
|
|
56
94
|
|
|
57
95
|
/** Walk up from `start` to the first directory containing `.git` (a dir, or a file for worktrees). */
|
|
@@ -118,10 +156,9 @@ function readRef(gitDir, name) {
|
|
|
118
156
|
}
|
|
119
157
|
|
|
120
158
|
/** Project folder, branch and short commit for the agent card; nothing when sharing is off. */
|
|
121
|
-
function gitInfo(
|
|
159
|
+
function gitInfo(cwd, config) {
|
|
122
160
|
const env = process.env.CLARIO_SHARE_GIT;
|
|
123
161
|
if (env !== undefined ? env === "0" : config.shareGit === false) return {};
|
|
124
|
-
const cwd = typeof event.cwd === "string" && event.cwd ? event.cwd : process.cwd();
|
|
125
162
|
const root = findGitRoot(cwd);
|
|
126
163
|
if (!root) return { project: basename(resolve(cwd)) };
|
|
127
164
|
const { branch, commit } = readHead(root);
|
|
@@ -152,42 +189,89 @@ function programNames(command) {
|
|
|
152
189
|
return names.slice(0, 3);
|
|
153
190
|
}
|
|
154
191
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (
|
|
192
|
+
/** A shell tool's command line. Codex often sends it as ["bash", "-lc", "the command"]. */
|
|
193
|
+
function commandText(input) {
|
|
194
|
+
const command = input?.command ?? input?.cmd;
|
|
195
|
+
if (typeof command === "string") return command;
|
|
196
|
+
if (!Array.isArray(command) || !command.every((part) => typeof part === "string")) return "";
|
|
197
|
+
const flag = command.findIndex((part) => part === "-c" || part === "-lc");
|
|
198
|
+
return flag > -1 && command[flag + 1] ? command[flag + 1] : command.join(" ");
|
|
199
|
+
}
|
|
159
200
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
201
|
+
/** The first file a Codex apply_patch names ("*** Update File: path"), wherever the patch sits in the input. */
|
|
202
|
+
function patchFile(value, depth = 0) {
|
|
203
|
+
if (depth > 3 || value == null) return undefined;
|
|
204
|
+
if (typeof value === "string") return /^\*\*\* (?:Update|Add|Delete) File:\s*(.+?)\s*$/m.exec(value)?.[1];
|
|
205
|
+
const values = Array.isArray(value) ? value : typeof value === "object" ? Object.values(value) : [];
|
|
206
|
+
for (const v of values) {
|
|
207
|
+
const found = patchFile(v, depth + 1);
|
|
208
|
+
if (found) return found;
|
|
168
209
|
}
|
|
169
|
-
|
|
170
|
-
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const fileName = (path) => (typeof path === "string" && path.trim() ? basename(path.trim()) : "");
|
|
214
|
+
|
|
215
|
+
const DONE_EVENTS = new Set(["Stop", "SessionEnd", "stop", "sessionEnd"]);
|
|
216
|
+
const PROMPT_EVENTS = new Set(["UserPromptSubmit", "beforeSubmitPrompt"]);
|
|
217
|
+
const EDIT_TOOLS = new Set(["edit", "write", "multiedit", "notebookedit"]);
|
|
218
|
+
const PATCH_TOOLS = new Set(["apply_patch", "applypatch"]);
|
|
219
|
+
const SHELL_TOOLS = new Set(["bash", "shell", "local_shell", "exec_command"]);
|
|
220
|
+
|
|
221
|
+
/** What the agent is doing, as the one line others see; no text means there's nothing to send. */
|
|
222
|
+
function describe(event, agent) {
|
|
223
|
+
const name = AGENTS[agent];
|
|
224
|
+
const hookEvent = event.hook_event_name;
|
|
225
|
+
if (DONE_EVENTS.has(hookEvent)) return { state: "done", text: "" };
|
|
226
|
+
if (PROMPT_EVENTS.has(hookEvent)) return { state: "cooking", text: `${name} is thinking` };
|
|
227
|
+
// Cursor: one event per file its agent changes, with the file at the top level.
|
|
228
|
+
if (hookEvent === "afterFileEdit") {
|
|
229
|
+
const file = fileName(event.file_path);
|
|
230
|
+
return { state: "cooking", text: file ? `${name} is editing ${file}` : `${name} is editing` };
|
|
231
|
+
}
|
|
232
|
+
if (hookEvent !== "PreToolUse" && hookEvent !== "PostToolUse") return { state: "cooking", text: "" };
|
|
233
|
+
|
|
234
|
+
const tool = typeof event.tool_name === "string" ? event.tool_name : "";
|
|
235
|
+
const key = tool.toLowerCase();
|
|
236
|
+
let label = tool.replace(/^(mcp__|LambdaResponse)/, "").slice(0, 40) || "a tool";
|
|
237
|
+
let detail = "";
|
|
238
|
+
if (EDIT_TOOLS.has(key)) {
|
|
239
|
+
const file = fileName(event?.tool_input?.file_path ?? event?.tool_input?.notebook_path);
|
|
240
|
+
if (file) detail = ` ${file}`;
|
|
241
|
+
} else if (PATCH_TOOLS.has(key)) {
|
|
242
|
+
label = "Edit";
|
|
243
|
+
const file = fileName(patchFile(event?.tool_input));
|
|
244
|
+
if (file) detail = ` ${file}`;
|
|
245
|
+
} else if (SHELL_TOOLS.has(key)) {
|
|
246
|
+
if (key !== "bash") label = "Shell";
|
|
247
|
+
const names = programNames(commandText(event?.tool_input));
|
|
171
248
|
if (names.length) detail = ` $ ${names.join(", ")}`;
|
|
172
249
|
}
|
|
173
|
-
return { state: "cooking", text:
|
|
250
|
+
return { state: "cooking", text: `${name} is using ${label}${detail}` };
|
|
174
251
|
}
|
|
175
252
|
|
|
176
253
|
async function main() {
|
|
177
|
-
const
|
|
178
|
-
if (!config) process.exit(0);
|
|
179
|
-
|
|
254
|
+
const agent = agentFromArgs(process.argv.slice(2));
|
|
180
255
|
const raw = await readStdin();
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
let event;
|
|
256
|
+
let event = {};
|
|
184
257
|
try {
|
|
185
|
-
|
|
258
|
+
const parsed = JSON.parse(raw);
|
|
259
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) event = parsed;
|
|
186
260
|
} catch {
|
|
187
|
-
process.exit(0);
|
|
188
261
|
}
|
|
189
262
|
|
|
190
|
-
|
|
263
|
+
// Cursor waits for a reply on stdout before it goes on: give it first, whatever happens next.
|
|
264
|
+
if (agent === "cursor-agent") {
|
|
265
|
+
await new Promise((done) => process.stdout.write(event.hook_event_name === "beforeSubmitPrompt" ? '{"continue":true}' : "{}", done));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const config = loadConfig();
|
|
269
|
+
if (!config || !raw.trim()) process.exit(0);
|
|
270
|
+
|
|
271
|
+
const cwd = eventCwd(event);
|
|
272
|
+
if (!sharedHere(config, agent, cwd)) process.exit(0);
|
|
273
|
+
|
|
274
|
+
const { state, text } = describe(event, agent);
|
|
191
275
|
if (state === "cooking" && !text) process.exit(0);
|
|
192
276
|
|
|
193
277
|
// Client-side throttle: hook events fire per tool use and can be chatty.
|
|
@@ -204,7 +288,7 @@ async function main() {
|
|
|
204
288
|
"Content-Type": "application/json",
|
|
205
289
|
Authorization: `Bearer ${config.credential}`,
|
|
206
290
|
},
|
|
207
|
-
body: JSON.stringify(state === "cooking" ? { text, state, ...gitInfo(
|
|
291
|
+
body: JSON.stringify(state === "cooking" ? { text, state, agent, ...gitInfo(cwd, config) } : { text, state, agent }),
|
|
208
292
|
signal: controller.signal,
|
|
209
293
|
});
|
|
210
294
|
if (state === "cooking") {
|
package/cli.mjs
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* clario-agent: show what your Claude Code
|
|
3
|
+
* clario-agent: show what your coding agent (Claude Code, Codex or Cursor) is doing on
|
|
4
|
+
* your Clario avatar.
|
|
4
5
|
*
|
|
5
|
-
* npx clario-agent setup [--api-url URL] [--global]
|
|
6
|
-
* npx clario-agent status
|
|
7
|
-
* npx clario-agent uninstall [--global]
|
|
6
|
+
* npx clario-agent setup --key KEY [--agent claude|codex|cursor|all] [--api-url URL] [--global]
|
|
7
|
+
* npx clario-agent status what is set up
|
|
8
|
+
* npx clario-agent uninstall [--global] stop sharing from this project (or everywhere)
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The key comes from the website: the Agent button in a Clario space shows the whole
|
|
11
|
+
* setup command. setup checks the key with the server, saves it in ~/.clario/agent.json
|
|
12
|
+
* (one key per user that stays the same; it can only post agent activity), copies the hook
|
|
13
|
+
* to ~/.clario/hook.mjs and registers it with each agent:
|
|
14
|
+
* Claude Code this project's .claude/settings.local.json, or ~/.claude/settings.json with --global
|
|
15
|
+
* Codex ~/.codex/hooks.json (or $CODEX_HOME/hooks.json)
|
|
16
|
+
* Cursor ~/.cursor/hooks.json
|
|
17
|
+
* Codex and Cursor use one file for every project, so the hook only reports from the
|
|
18
|
+
* projects setup was run in (`projects` in the config), or from all with --global.
|
|
12
19
|
*/
|
|
13
20
|
|
|
14
21
|
import { execFileSync } from "node:child_process";
|
|
15
22
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
16
23
|
import { homedir } from "node:os";
|
|
17
24
|
import { dirname, join } from "node:path";
|
|
18
|
-
import { createInterface } from "node:readline";
|
|
19
25
|
import { fileURLToPath } from "node:url";
|
|
20
26
|
|
|
21
27
|
const DIR = join(homedir(), ".clario");
|
|
@@ -26,14 +32,43 @@ const HOOK_SOURCE = join(dirname(fileURLToPath(import.meta.url)), "clario-agent-
|
|
|
26
32
|
const OLD_CONFIG_PATH = join(homedir(), ".viora", "agent.json");
|
|
27
33
|
// The live Clario server; pass --api-url http://localhost:3001 for a local one.
|
|
28
34
|
const DEFAULT_API_URL = "https://api.clario.games";
|
|
29
|
-
const
|
|
30
|
-
const
|
|
35
|
+
const WARN_DAYS = 14; // setup reminds you to get a new key when the saved one has less time left than this
|
|
36
|
+
const GET_A_KEY = "Open your Clario space at https://www.clario.games, click Agent in the left menu, and run the setup command it shows.";
|
|
31
37
|
|
|
32
38
|
class CliError extends Error {}
|
|
33
39
|
const fail = (message) => {
|
|
34
40
|
throw new CliError(message);
|
|
35
41
|
};
|
|
36
42
|
|
|
43
|
+
/** A path for a hook command line, quoted for the shell. */
|
|
44
|
+
const quote = (path) => `"${path.replace(/(["\\$`])/g, "\\$1")}"`;
|
|
45
|
+
|
|
46
|
+
/** Where each agent reads its hooks, the events the hook listens to, and the file's layout. */
|
|
47
|
+
const AGENT_TARGETS = {
|
|
48
|
+
claude: {
|
|
49
|
+
name: "Claude Code",
|
|
50
|
+
file: (global) => (global ? join(homedir(), ".claude", "settings.json") : join(process.cwd(), ".claude", "settings.local.json")),
|
|
51
|
+
events: ["PreToolUse", "PostToolUse", "Stop", "SessionEnd"],
|
|
52
|
+
command: () => `node ${quote(HOOK_PATH)}`,
|
|
53
|
+
},
|
|
54
|
+
codex: {
|
|
55
|
+
name: "Codex",
|
|
56
|
+
shared: true, // one file for every project
|
|
57
|
+
file: () => join(process.env.CODEX_HOME || join(homedir(), ".codex"), "hooks.json"),
|
|
58
|
+
events: ["UserPromptSubmit", "PostToolUse", "Stop", "SessionEnd"],
|
|
59
|
+
// The full path to node: Codex and Cursor can start hooks without your shell's PATH.
|
|
60
|
+
command: () => `${quote(process.execPath)} ${quote(HOOK_PATH)} --agent codex`,
|
|
61
|
+
},
|
|
62
|
+
cursor: {
|
|
63
|
+
name: "Cursor",
|
|
64
|
+
shared: true,
|
|
65
|
+
flat: true, // { version: 1, hooks: { event: [{ command }] } }
|
|
66
|
+
file: () => join(homedir(), ".cursor", "hooks.json"),
|
|
67
|
+
events: ["beforeSubmitPrompt", "afterFileEdit", "stop"],
|
|
68
|
+
command: () => `${quote(process.execPath)} ${quote(HOOK_PATH)} --agent cursor-agent`,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
37
72
|
/** { value } for a JSON file, {} when it doesn't exist; fails on invalid JSON instead of overwriting it. */
|
|
38
73
|
function readJson(file) {
|
|
39
74
|
let text;
|
|
@@ -57,16 +92,26 @@ function writeJson(file, value, { mode } = {}) {
|
|
|
57
92
|
if (mode) chmodSync(file, mode);
|
|
58
93
|
}
|
|
59
94
|
|
|
60
|
-
/**
|
|
61
|
-
function
|
|
95
|
+
/** The readable part of a key (a JWT), or null. The server checks the signature. */
|
|
96
|
+
function keyPayload(token) {
|
|
62
97
|
try {
|
|
63
|
-
const
|
|
64
|
-
return typeof
|
|
98
|
+
const payload = JSON.parse(Buffer.from(String(token).split(".")[1], "base64url").toString());
|
|
99
|
+
return payload && typeof payload === "object" ? payload : null;
|
|
65
100
|
} catch {
|
|
66
101
|
return null;
|
|
67
102
|
}
|
|
68
103
|
}
|
|
69
104
|
|
|
105
|
+
/** Days until a key expires: Infinity for one that doesn't (today's keys), null when it can't be read. */
|
|
106
|
+
function daysLeft(token) {
|
|
107
|
+
const payload = keyPayload(token);
|
|
108
|
+
if (!payload) return null;
|
|
109
|
+
return typeof payload.exp === "number" ? (payload.exp * 1000 - Date.now()) / 86_400_000 : Infinity;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const expiryText = (days) =>
|
|
113
|
+
Number.isFinite(days) ? `works until ${new Date(Date.now() + days * 86_400_000).toDateString()}` : "it doesn't expire";
|
|
114
|
+
|
|
70
115
|
async function post(apiUrl, path, body, token) {
|
|
71
116
|
let res;
|
|
72
117
|
try {
|
|
@@ -81,50 +126,28 @@ async function post(apiUrl, path, body, token) {
|
|
|
81
126
|
return { status: res.status, json: await res.json().catch(() => null) };
|
|
82
127
|
}
|
|
83
128
|
|
|
84
|
-
/**
|
|
85
|
-
async function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const password = await answer;
|
|
97
|
-
rl._writeToOutput = write;
|
|
98
|
-
rl.close();
|
|
99
|
-
process.stdout.write("\n");
|
|
100
|
-
return { email, password };
|
|
129
|
+
/** A key from the website, checked with the server before it's saved. */
|
|
130
|
+
async function checkKey(key, apiUrl) {
|
|
131
|
+
const payload = keyPayload(key);
|
|
132
|
+
if (!payload || typeof payload.userId !== "string") fail(`That isn't a Clario agent key. ${GET_A_KEY}`);
|
|
133
|
+
if (payload.scope !== "agent") fail(`That's a login token, not an agent key. ${GET_A_KEY}`);
|
|
134
|
+
const days = daysLeft(key);
|
|
135
|
+
if (days === null || days <= 0) fail(`That key has expired. ${GET_A_KEY}`);
|
|
136
|
+
// A "done" update changes nothing anyone sees, and tells the server the hook is set up.
|
|
137
|
+
const res = await post(apiUrl, "/agent-activity", { text: "", state: "done" }, key);
|
|
138
|
+
if (res.status === 403) fail(`The server at ${apiUrl} doesn't accept this key. ${GET_A_KEY}`);
|
|
139
|
+
if (res.status !== 200) fail(`Could not check the key with ${apiUrl} (HTTP ${res.status}).`);
|
|
140
|
+
return { key, days, reused: false };
|
|
101
141
|
}
|
|
102
142
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
if (config.agentKey && config.apiUrl === apiUrl &&
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
// A login token from the manual setup (README) is enough to create the key.
|
|
110
|
-
let res = config.token ? await post(apiUrl, "/agent-key", {}, config.token) : null;
|
|
111
|
-
if (!res || res.status !== 200) {
|
|
112
|
-
if (res?.status !== 404) {
|
|
113
|
-
const { email, password } = await askCredentials();
|
|
114
|
-
const signin = await post(apiUrl, "/signin", { email, password });
|
|
115
|
-
if (!signin.json?.token) fail("Sign-in failed. Check your email and password.");
|
|
116
|
-
res = await post(apiUrl, "/agent-key", {}, signin.json.token);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (res.status === 404) fail(`The server at ${apiUrl} has no /api/v1/agent-key yet. Restart it so it runs the latest code.`);
|
|
120
|
-
if (res.status !== 200 || !res.json?.key) fail(`Could not create an agent key (HTTP ${res.status}).`);
|
|
121
|
-
return { key: res.json.key, days: daysLeft(res.json.key), reused: false };
|
|
143
|
+
/** The key from an earlier setup, when it still works for this server (e.g. to add another project). */
|
|
144
|
+
function savedKey(config, apiUrl) {
|
|
145
|
+
const days = daysLeft(config.agentKey);
|
|
146
|
+
if (config.agentKey && config.apiUrl === apiUrl && days !== null && days > 0) return { key: config.agentKey, days, reused: true };
|
|
147
|
+
fail(`No agent key yet. ${GET_A_KEY}`);
|
|
122
148
|
}
|
|
123
149
|
|
|
124
|
-
// ----
|
|
125
|
-
|
|
126
|
-
const settingsFile = (global) =>
|
|
127
|
-
global ? join(homedir(), ".claude", "settings.json") : join(process.cwd(), ".claude", "settings.local.json");
|
|
150
|
+
// ---- Hook files ---------------------------------------------------------------------
|
|
128
151
|
|
|
129
152
|
/** Our hooks from any version (also from before the rename): this package's copy, or the repo script. */
|
|
130
153
|
const isOurHook = (hook) =>
|
|
@@ -140,6 +163,7 @@ function withoutOurHooks(settings) {
|
|
|
140
163
|
continue;
|
|
141
164
|
}
|
|
142
165
|
const kept = groups
|
|
166
|
+
.filter((group) => !isOurHook(group)) // Cursor lists commands directly
|
|
143
167
|
.map((group) => (Array.isArray(group?.hooks) ? { ...group, hooks: group.hooks.filter((h) => !isOurHook(h)) } : group))
|
|
144
168
|
.filter((group) => !Array.isArray(group?.hooks) || group.hooks.length > 0);
|
|
145
169
|
if (kept.length) hooks[event] = kept;
|
|
@@ -149,17 +173,41 @@ function withoutOurHooks(settings) {
|
|
|
149
173
|
}
|
|
150
174
|
|
|
151
175
|
const countOurHooks = (settings) =>
|
|
152
|
-
Object.values(settings?.hooks ?? {})
|
|
176
|
+
Object.values(settings?.hooks ?? {})
|
|
177
|
+
.flat()
|
|
178
|
+
.flatMap((group) => (Array.isArray(group?.hooks) ? group.hooks : [group]))
|
|
179
|
+
.filter(isOurHook).length;
|
|
153
180
|
|
|
154
181
|
/** Add the hook for every event, replacing older entries of ours so it never fires twice. */
|
|
155
|
-
function registerHooks(
|
|
182
|
+
function registerHooks(target, global) {
|
|
183
|
+
const file = target.file(global);
|
|
156
184
|
const { value = {} } = readJson(file);
|
|
157
185
|
if (existsSync(file)) copyFileSync(file, `${file}.clario-backup`);
|
|
158
186
|
const settings = withoutOurHooks(value);
|
|
159
187
|
const hooks = { ...(settings.hooks ?? {}) };
|
|
160
|
-
const command =
|
|
161
|
-
|
|
162
|
-
|
|
188
|
+
const command = target.command();
|
|
189
|
+
const entry = target.flat ? { command, timeout: 10 } : { hooks: [{ type: "command", command, timeout: 10 }] };
|
|
190
|
+
for (const event of target.events) hooks[event] = [...(hooks[event] ?? []), entry];
|
|
191
|
+
writeJson(file, { ...(target.flat ? { version: 1 } : {}), ...settings, hooks });
|
|
192
|
+
return file;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Remove our hook from a file; true when there was one. */
|
|
196
|
+
function unregisterHooks(file) {
|
|
197
|
+
const { value } = readJson(file);
|
|
198
|
+
if (!value || !countOurHooks(value)) return false;
|
|
199
|
+
copyFileSync(file, `${file}.clario-backup`);
|
|
200
|
+
writeJson(file, withoutOurHooks(value));
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The project setup runs in: the git root around the current folder, or the folder itself. */
|
|
205
|
+
function projectRoot() {
|
|
206
|
+
const here = realpathSync(process.cwd());
|
|
207
|
+
for (let dir = here; ; dir = dirname(dir)) {
|
|
208
|
+
if (existsSync(join(dir, ".git"))) return dir;
|
|
209
|
+
if (dirname(dir) === dir) return here;
|
|
210
|
+
}
|
|
163
211
|
}
|
|
164
212
|
|
|
165
213
|
function gitIgnored(file) {
|
|
@@ -171,29 +219,54 @@ function gitIgnored(file) {
|
|
|
171
219
|
}
|
|
172
220
|
}
|
|
173
221
|
|
|
222
|
+
/** --agent claude|codex|cursor|all, repeatable or comma separated; Claude Code when left out. */
|
|
223
|
+
function parseAgents(args) {
|
|
224
|
+
const values = args
|
|
225
|
+
.flatMap((arg, i) => (arg === "--agent" ? [args[i + 1] ?? ""] : arg.startsWith("--agent=") ? [arg.slice("--agent=".length)] : []))
|
|
226
|
+
.flatMap((value) => value.split(","))
|
|
227
|
+
.map((value) => value.trim().toLowerCase())
|
|
228
|
+
.filter(Boolean);
|
|
229
|
+
const aliases = { "claude-code": "claude", "cursor-agent": "cursor" };
|
|
230
|
+
const names = values.includes("all") ? Object.keys(AGENT_TARGETS) : values.map((value) => aliases[value] ?? value);
|
|
231
|
+
const unknown = names.find((name) => !(name in AGENT_TARGETS));
|
|
232
|
+
if (unknown !== undefined) fail(`Unknown agent "${unknown}". Use claude, codex, cursor or all.`);
|
|
233
|
+
return names.length ? [...new Set(names)] : ["claude"];
|
|
234
|
+
}
|
|
235
|
+
|
|
174
236
|
// ---- Commands -----------------------------------------------------------------------
|
|
175
237
|
|
|
176
238
|
async function setup(opts) {
|
|
177
239
|
const { value: config = readJson(OLD_CONFIG_PATH).value ?? {} } = readJson(CONFIG_PATH);
|
|
178
|
-
|
|
179
|
-
const
|
|
240
|
+
// A new key comes from the live site unless --api-url says otherwise; re-running setup keeps the saved server.
|
|
241
|
+
const apiUrl = String(opts.apiUrl || (opts.key ? DEFAULT_API_URL : config.apiUrl) || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
242
|
+
const { key, days, reused } = opts.key ? await checkKey(opts.key, apiUrl) : savedKey(config, apiUrl);
|
|
180
243
|
|
|
181
|
-
//
|
|
244
|
+
// An old 7-day login token isn't needed any more; everything else (shareGit, projects…) stays.
|
|
182
245
|
const { token: _loginToken, ...rest } = config;
|
|
183
|
-
|
|
246
|
+
const next = { ...rest, apiUrl, agentKey: key };
|
|
247
|
+
const root = projectRoot();
|
|
248
|
+
if (opts.agents.some((agent) => AGENT_TARGETS[agent].shared)) {
|
|
249
|
+
if (opts.global) next.everyProject = true;
|
|
250
|
+
else next.projects = [...new Set([...(Array.isArray(rest.projects) ? rest.projects : []), root])];
|
|
251
|
+
}
|
|
252
|
+
writeJson(CONFIG_PATH, next, { mode: 0o600 });
|
|
184
253
|
mkdirSync(DIR, { recursive: true, mode: 0o700 });
|
|
185
254
|
copyFileSync(HOOK_SOURCE, HOOK_PATH);
|
|
186
255
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
256
|
+
console.log(`${reused ? "Kept your agent key" : "Saved your agent key"} in ${CONFIG_PATH} (${expiryText(days)}).`);
|
|
257
|
+
if (reused && days < WARN_DAYS) console.log(`It runs out in ${Math.ceil(days)} days. ${GET_A_KEY}`);
|
|
258
|
+
for (const agent of opts.agents) {
|
|
259
|
+
const target = AGENT_TARGETS[agent];
|
|
260
|
+
const file = registerHooks(target, opts.global);
|
|
261
|
+
const scope = opts.global ? "every project" : target.shared ? `${root} (and any other project you ran setup in)` : "this project only";
|
|
262
|
+
console.log(`${target.name}: hook registered in ${file}, sharing from ${scope}.`);
|
|
263
|
+
if (agent === "claude" && !opts.global && gitIgnored(file) === false) {
|
|
264
|
+
console.log(" Note: .claude/settings.local.json isn't git-ignored here. It holds no secrets, but you may want to ignore it.");
|
|
265
|
+
}
|
|
266
|
+
if (agent === "codex") console.log(" Codex asks before it runs a new hook: open Codex, run /hooks and trust the Clario hook.");
|
|
267
|
+
if (agent === "cursor") console.log(" If Cursor is open, restart it so it loads the hook.");
|
|
195
268
|
}
|
|
196
|
-
console.log("Open your Clario space and give
|
|
269
|
+
console.log("Open your Clario space and give your agent a task: your avatar's card shows what it's doing.");
|
|
197
270
|
}
|
|
198
271
|
|
|
199
272
|
function status() {
|
|
@@ -202,43 +275,66 @@ function status() {
|
|
|
202
275
|
console.log(`Server: ${config.apiUrl ?? "not set"}`);
|
|
203
276
|
console.log(
|
|
204
277
|
config.agentKey || config.token
|
|
205
|
-
? `Key: ${config.agentKey ? "agent key" : "7-day login token"}, ${left === null ? "unreadable" : left > 0 ? `${Math.floor(left)} days left` : "EXPIRED (run setup)"}`
|
|
278
|
+
? `Key: ${config.agentKey ? "agent key" : "7-day login token"}, ${left === null ? "unreadable" : left === Infinity ? "doesn't expire" : left > 0 ? `${Math.floor(left)} days left` : "EXPIRED (run setup)"}`
|
|
206
279
|
: "Key: none (run setup)",
|
|
207
280
|
);
|
|
208
281
|
console.log(`Hook: ${existsSync(HOOK_PATH) ? HOOK_PATH : "not installed (run setup)"}`);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
282
|
+
const registered = (file) => (countOurHooks(readJson(file).value) ? `registered (${file})` : "not registered");
|
|
283
|
+
console.log(`Claude Code, this project: ${registered(AGENT_TARGETS.claude.file(false))}`);
|
|
284
|
+
console.log(`Claude Code, all projects: ${registered(AGENT_TARGETS.claude.file(true))}`);
|
|
285
|
+
console.log(`Codex: ${registered(AGENT_TARGETS.codex.file())}`);
|
|
286
|
+
console.log(`Cursor: ${registered(AGENT_TARGETS.cursor.file())}`);
|
|
287
|
+
const projects = Array.isArray(config.projects) ? config.projects : [];
|
|
288
|
+
console.log(`Codex and Cursor share from: ${config.everyProject ? "every project" : projects.length ? projects.join(", ") : "no project yet"}`);
|
|
214
289
|
}
|
|
215
290
|
|
|
216
291
|
function uninstall(opts) {
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
292
|
+
const { value: config } = readJson(CONFIG_PATH);
|
|
293
|
+
if (opts.global) {
|
|
294
|
+
// Stop everywhere: every agent's hook, and the list of shared projects.
|
|
295
|
+
for (const target of Object.values(AGENT_TARGETS)) {
|
|
296
|
+
const file = target.file(true);
|
|
297
|
+
if (unregisterHooks(file)) console.log(`Removed the ${target.name} hook from ${file}.`);
|
|
298
|
+
}
|
|
299
|
+
if (config) {
|
|
300
|
+
const { projects: _projects, everyProject: _every, ...rest } = config;
|
|
301
|
+
writeJson(CONFIG_PATH, rest, { mode: 0o600 });
|
|
302
|
+
}
|
|
303
|
+
console.log(`Nothing is shared any more. Your key stays in ${CONFIG_PATH}; delete that file to remove it too.`);
|
|
221
304
|
return;
|
|
222
305
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
console.log(`Removed the
|
|
306
|
+
// Stop in this project: its Claude Code hook, and Codex and Cursor reporting from it.
|
|
307
|
+
const file = AGENT_TARGETS.claude.file(false);
|
|
308
|
+
if (unregisterHooks(file)) console.log(`Removed the Claude Code hook from ${file}.`);
|
|
309
|
+
const root = projectRoot();
|
|
310
|
+
if (config && Array.isArray(config.projects) && config.projects.includes(root)) {
|
|
311
|
+
writeJson(CONFIG_PATH, { ...config, projects: config.projects.filter((p) => p !== root) }, { mode: 0o600 });
|
|
312
|
+
console.log(`Codex and Cursor no longer share from ${root}.`);
|
|
313
|
+
}
|
|
314
|
+
if (config?.everyProject) console.log("Codex and Cursor still share from every project (setup --global); run uninstall --global to stop that.");
|
|
315
|
+
console.log("This project isn't shared any more.");
|
|
226
316
|
}
|
|
227
317
|
|
|
228
|
-
const HELP = `clario-agent: show what your
|
|
318
|
+
const HELP = `clario-agent: show what your coding agent is doing on your Clario avatar
|
|
229
319
|
|
|
230
|
-
clario-agent setup [--
|
|
231
|
-
|
|
232
|
-
clario-agent
|
|
320
|
+
clario-agent setup --key KEY [--agent claude|codex|cursor|all] [--global]
|
|
321
|
+
save your key and register the hook
|
|
322
|
+
clario-agent status show what is set up
|
|
323
|
+
clario-agent uninstall [--global] stop sharing from this project (or everywhere)
|
|
233
324
|
|
|
234
|
-
|
|
235
|
-
--
|
|
325
|
+
Get the setup command (with your key) from the Agent button in your Clario space.
|
|
326
|
+
--agent picks which agents to set up (default claude; comma separated or repeated).
|
|
327
|
+
Without --global only the project you run it in is shared. Without --key, setup reuses
|
|
328
|
+
the key you saved before. The server is ${DEFAULT_API_URL} unless you pass --api-url.`;
|
|
236
329
|
|
|
237
330
|
export async function run(argv) {
|
|
238
331
|
const [command = "help", ...rest] = argv;
|
|
239
|
-
const
|
|
240
|
-
|
|
332
|
+
const value = (flag) => {
|
|
333
|
+
const i = rest.indexOf(flag);
|
|
334
|
+
return i > -1 ? rest[i + 1] : undefined;
|
|
335
|
+
};
|
|
241
336
|
try {
|
|
337
|
+
const opts = { global: rest.includes("--global"), apiUrl: value("--api-url"), key: value("--key"), agents: parseAgents(rest) };
|
|
242
338
|
if (command === "setup") await setup(opts);
|
|
243
339
|
else if (command === "status") status();
|
|
244
340
|
else if (command === "uninstall") uninstall(opts);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clario-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Show what your Claude Code agent is doing (project, branch, commit) on your Clario avatar.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Show what your Claude Code, Codex or Cursor agent is doing (project, branch, commit) on your Clario avatar.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clario-agent": "cli.mjs"
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"keywords": [
|
|
19
19
|
"clario",
|
|
20
20
|
"claude-code",
|
|
21
|
+
"codex",
|
|
22
|
+
"cursor",
|
|
21
23
|
"hooks",
|
|
22
24
|
"agent",
|
|
23
25
|
"metaverse"
|