clario-agent 0.0.0-stage → 0.2.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 CHANGED
@@ -1,3 +1,72 @@
1
- # Temporary Holding Version
1
+ # clario-agent: show your AI agent's activity in Clario 🧑‍🍳
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A Claude Code hook that shows what your agent is working on to everyone in your current
4
+ Clario space. Others see a pulsing badge above your avatar while the agent works, and
5
+ clicking your avatar shows your agent card: what Claude is doing plus the project, branch
6
+ and commit (e.g. `graphite · feat/missions · a1b2c3d`). The badge clears when Claude finishes.
7
+
8
+ ## Setup (one command)
9
+
10
+ 1. Open your space on [clario.games](https://www.clario.games) and click **Agent** in the
11
+ left menu. It shows your setup command, with a key made for you:
12
+ ```bash
13
+ npx clario-agent setup --key YOUR_KEY
14
+ ```
15
+ 2. Copy it and run it in the terminal, inside the project you want to share.
16
+
17
+ That's it: the Agent button disappears once your agent has connected. Setup:
18
+
19
+ - **Checks the key** with the Clario server and saves it in `~/.clario/agent.json` (readable
20
+ only by you). It's your own key and it stays the same: the Agent button always shows the
21
+ same command for you. It can only post agent activity: it can't sign in, open a space or
22
+ change anything else. No password is ever typed in the terminal.
23
+ - **Installs the hook** at `~/.clario/hook.mjs`.
24
+ - **Registers it with Claude Code** for this project only, in `.claude/settings.local.json`.
25
+ Add `--global` to use it in every project (`~/.claude/settings.json`) instead.
26
+
27
+ Then give Claude a task in that project and click your avatar. Teammates who do the same and
28
+ join your space show up on their avatars too.
29
+
30
+ To add another project later, run `npx clario-agent setup` there: it reuses your saved key.
31
+ Setup never registers the hook twice, leaves your other Claude Code settings and hooks as
32
+ they are, and saves the previous file next to it as `*.clario-backup`.
33
+
34
+ ```bash
35
+ npx clario-agent status # server, key expiry, where the hook is registered
36
+ npx clario-agent uninstall # remove the hook from this project (add --global for all)
37
+ ```
38
+
39
+ It uses the live server at `https://api.clario.games`. Running Clario yourself? The Agent
40
+ button on your own site adds `--api-url` to the command for you.
41
+
42
+ Used the older `viora-agent` package? Run `npx clario-agent setup` once: it reuses the key
43
+ from `~/.viora` and replaces the old hook. You can delete `~/.viora` afterwards.
44
+
45
+ > Keep the setup command private: anyone with your key can post activity as your agent (and
46
+ > nothing else). If it leaks, delete `~/.clario/agent.json`; the server's owner can cancel
47
+ > every key by changing `JWT_SECRET` (this signs everyone out).
48
+
49
+ ## How it behaves
50
+
51
+ - While Claude runs tools (edits, bash commands…) your badge shows "cooking" with a short
52
+ description of the current tool (e.g. `Claude is using Edit page.tsx` or
53
+ `Claude is using Bash $ bun, git`)
54
+ - Your agent card also shows the project folder, git branch and short commit the agent is
55
+ working in
56
+ - When Claude finishes, the badge clears automatically
57
+ - If you're not inside a Clario space, updates are silently dropped
58
+ - Updates are throttled (2s) so fast tool bursts don't spam the room
59
+ - The hook never blocks or fails your agent session: all errors are swallowed
60
+
61
+ ## Privacy notes
62
+
63
+ Everyone in your space sees what is sent. That is the tool name, the edited file's name,
64
+ the names of the programs a shell command runs (`bun`, `git`; never its arguments, which
65
+ can hold tokens or paths), and the project folder, branch and short commit. Never file
66
+ contents, prompts or code.
67
+
68
+ To keep project, branch and commit on your machine, add `"shareGit": false` to
69
+ `~/.clario/agent.json` (or set `CLARIO_SHARE_GIT=0`). Run `npx clario-agent uninstall` to stop
70
+ broadcasting from a project.
71
+
72
+ Requires Node.js 18 or newer. MIT licensed.
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Clario agent hook for Claude Code
4
+ *
5
+ * Broadcasts what your AI agent is doing to everyone in your current Clario space.
6
+ * Setup guide: tools/agent-hook/README.md
7
+ *
8
+ * Reads Claude Code hook events from stdin and POSTs activity to
9
+ * POST /api/v1/agent-activity on your Clario server. Exits 0 silently on any
10
+ * failure so it never blocks your agent session.
11
+ *
12
+ * Sent: a short status line (tool name, edited file name, or the programs a shell
13
+ * command runs, never its arguments) plus the project folder, git branch and short
14
+ * commit. `"shareGit": false` in ~/.clario/agent.json (or CLARIO_SHARE_GIT=0) keeps
15
+ * project, branch and commit on this machine.
16
+ */
17
+
18
+ import { existsSync, readFileSync, statSync } from "node:fs";
19
+ import { homedir } from "node:os";
20
+ import { basename, dirname, join, resolve } from "node:path";
21
+
22
+ const CONFIG_PATH = join(homedir(), ".clario", "agent.json");
23
+ const THROTTLE_MS = 2000;
24
+ const STATE_PATH = join(homedir(), ".clario", ".agent-hook-state");
25
+
26
+ function readStdin() {
27
+ return new Promise((resolve) => {
28
+ let raw = "";
29
+ process.stdin.setEncoding("utf8");
30
+ process.stdin.on("data", (chunk) => (raw += chunk));
31
+ process.stdin.on("end", () => resolve(raw));
32
+ setTimeout(() => resolve(raw), 1500);
33
+ });
34
+ }
35
+
36
+ function loadConfig() {
37
+ try {
38
+ const config = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
39
+ // agentKey (setup.mjs, lasts 180 days) wins over a pasted 7-day login token.
40
+ const credential = config?.agentKey || config?.token;
41
+ if (credential && config?.apiUrl) return { ...config, credential };
42
+ } catch {
43
+ }
44
+ return null;
45
+ }
46
+
47
+ function lastSentAt() {
48
+ try {
49
+ return Number(readFileSync(STATE_PATH, "utf8")) || 0;
50
+ } catch {
51
+ return 0;
52
+ }
53
+ }
54
+
55
+ // Git: project, branch and commit, read from .git directly (no git CLI).
56
+
57
+ /** Walk up from `start` to the first directory containing `.git` (a dir, or a file for worktrees). */
58
+ function findGitRoot(start) {
59
+ let dir = resolve(start);
60
+ for (;;) {
61
+ if (existsSync(join(dir, ".git"))) return dir;
62
+ const parent = dirname(dir);
63
+ if (parent === dir) return null;
64
+ dir = parent;
65
+ }
66
+ }
67
+
68
+ /** The repository's git dir. Throws if `.git` is missing or unreadable. */
69
+ function gitDirOf(gitRoot) {
70
+ const gitDir = join(gitRoot, ".git");
71
+ if (!statSync(gitDir).isFile()) return gitDir;
72
+ // Worktrees and submodules: ".git" is a file containing "gitdir: <path>".
73
+ const m = /^gitdir:\s*(.+)$/m.exec(readFileSync(gitDir, "utf8"));
74
+ return m ? resolve(gitRoot, m[1].trim()) : undefined;
75
+ }
76
+
77
+ /** A full commit id: 40 hex characters, or 64 in SHA-256 repositories. */
78
+ const COMMIT_RE = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/;
79
+
80
+ /** Branch and commit from HEAD. A detached HEAD has no branch; a repo with no commits has no commit. */
81
+ function readHead(gitRoot) {
82
+ let gitDir;
83
+ let head;
84
+ try {
85
+ gitDir = gitDirOf(gitRoot);
86
+ head = gitDir && readFileSync(join(gitDir, "HEAD"), "utf8").trim();
87
+ } catch {
88
+ return {};
89
+ }
90
+ if (!head) return {};
91
+ if (COMMIT_RE.test(head)) return { commit: head };
92
+ const ref = /^ref:\s*(refs\/heads\/(.+))$/.exec(head);
93
+ return ref ? { branch: ref[2], commit: readRef(gitDir, ref[1]) } : {};
94
+ }
95
+
96
+ /** A branch's commit: loose ref first, then packed-refs. Linked worktrees keep both in the main git dir. */
97
+ function readRef(gitDir, name) {
98
+ let commonDir = gitDir;
99
+ try {
100
+ commonDir = resolve(gitDir, readFileSync(join(gitDir, "commondir"), "utf8").trim());
101
+ } catch {
102
+ // Not a linked worktree.
103
+ }
104
+ let id;
105
+ try {
106
+ id = readFileSync(join(commonDir, name), "utf8").trim();
107
+ } catch {
108
+ try {
109
+ for (const line of readFileSync(join(commonDir, "packed-refs"), "utf8").split("\n")) {
110
+ const [sha, ref] = line.trim().split(" ");
111
+ if (ref === name) id = sha;
112
+ }
113
+ } catch {
114
+ // No packed-refs either: a repo with no commits yet.
115
+ }
116
+ }
117
+ return id && COMMIT_RE.test(id) ? id : undefined;
118
+ }
119
+
120
+ /** Project folder, branch and short commit for the agent card; nothing when sharing is off. */
121
+ function gitInfo(event, config) {
122
+ const env = process.env.CLARIO_SHARE_GIT;
123
+ if (env !== undefined ? env === "0" : config.shareGit === false) return {};
124
+ const cwd = typeof event.cwd === "string" && event.cwd ? event.cwd : process.cwd();
125
+ const root = findGitRoot(cwd);
126
+ if (!root) return { project: basename(resolve(cwd)) };
127
+ const { branch, commit } = readHead(root);
128
+ return { project: basename(root), branch, commit: commit?.slice(0, 7) };
129
+ }
130
+
131
+ const WRAPPERS = new Set(["sudo", "env", "time", "nohup", "exec", "command"]);
132
+ const NOT_PROGRAMS = new Set(["cd", "export", "source", ".", "set", "unset"]);
133
+
134
+ /**
135
+ * The programs a shell command runs ("bun", "git"), never its arguments: those can
136
+ * hold tokens, passwords or private paths, and the status is shown to the whole space.
137
+ */
138
+ function programNames(command) {
139
+ const names = [];
140
+ // First line only (skips heredoc bodies), and quoted strings removed, so a `;` or `|`
141
+ // inside quotes can't turn quoted text into a "program".
142
+ const line = command.split("\n")[0].replace(/'[^']*'|"(?:\\.|[^"\\])*"/g, "''");
143
+ for (const segment of line.split(/&&|\|\||[;|]/)) {
144
+ const words = segment.trim().split(/\s+/);
145
+ let i = 0;
146
+ // Skip leading VAR=value, wrappers like sudo/env and their flags.
147
+ while (i < words.length && (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i]) || WRAPPERS.has(words[i]) || words[i].startsWith("-"))) i++;
148
+ if (!words[i] || NOT_PROGRAMS.has(words[i])) continue;
149
+ const name = basename(words[i]);
150
+ if (/^[A-Za-z0-9_.][A-Za-z0-9._+-]{0,23}$/.test(name) && !names.includes(name)) names.push(name);
151
+ }
152
+ return names.slice(0, 3);
153
+ }
154
+
155
+ function describe(event) {
156
+ const tool = typeof event.tool_name === "string" ? event.tool_name : "";
157
+ if (event.hook_event_name === "Stop") return { state: "done", text: "" };
158
+ if (event.hook_event_name === "SessionEnd") return { state: "done", text: "" };
159
+
160
+ const toolLabel = tool.replace(/^(mcp__|LambdaResponse)/, "").slice(0, 40);
161
+ let detail = "";
162
+ if (tool === "Edit" || tool === "Write" || tool === "MultiEdit") {
163
+ const filePath = event?.tool_input?.file_path;
164
+ if (typeof filePath === "string") {
165
+ const parts = filePath.split("/");
166
+ detail = ` ${parts[parts.length - 1]}`;
167
+ }
168
+ }
169
+ if (tool === "Bash" && typeof event?.tool_input?.command === "string") {
170
+ const names = programNames(event.tool_input.command);
171
+ if (names.length) detail = ` $ ${names.join(", ")}`;
172
+ }
173
+ return { state: "cooking", text: `Claude is using ${toolLabel || "a tool"}${detail}` };
174
+ }
175
+
176
+ async function main() {
177
+ const config = loadConfig();
178
+ if (!config) process.exit(0);
179
+
180
+ const raw = await readStdin();
181
+ if (!raw.trim()) process.exit(0);
182
+
183
+ let event;
184
+ try {
185
+ event = JSON.parse(raw);
186
+ } catch {
187
+ process.exit(0);
188
+ }
189
+
190
+ const { state, text } = describe(event);
191
+ if (state === "cooking" && !text) process.exit(0);
192
+
193
+ // Client-side throttle: hook events fire per tool use and can be chatty.
194
+ const now = Date.now();
195
+ if (state === "cooking" && now - lastSentAt() < THROTTLE_MS) process.exit(0);
196
+
197
+ const controller = new AbortController();
198
+ const timer = setTimeout(() => controller.abort(), 2500);
199
+
200
+ try {
201
+ await fetch(`${config.apiUrl.replace(/\/$/, "")}/api/v1/agent-activity`, {
202
+ method: "POST",
203
+ headers: {
204
+ "Content-Type": "application/json",
205
+ Authorization: `Bearer ${config.credential}`,
206
+ },
207
+ body: JSON.stringify(state === "cooking" ? { text, state, ...gitInfo(event, config) } : { text, state }),
208
+ signal: controller.signal,
209
+ });
210
+ if (state === "cooking") {
211
+ const { writeFileSync, mkdirSync } = await import("node:fs");
212
+ try {
213
+ mkdirSync(join(homedir(), ".clario"), { recursive: true });
214
+ writeFileSync(STATE_PATH, String(now));
215
+ } catch {
216
+ }
217
+ }
218
+ } catch {
219
+ // Never let analytics failures disturb the agent session.
220
+ } finally {
221
+ clearTimeout(timer);
222
+ }
223
+
224
+ process.exit(0);
225
+ }
226
+
227
+ main();
package/cli.mjs ADDED
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * clario-agent: show what your Claude Code agent is doing on your Clario avatar.
4
+ *
5
+ * npx clario-agent setup --key KEY [--api-url URL] [--global] key + hook, once
6
+ * npx clario-agent status what is set up
7
+ * npx clario-agent uninstall [--global] remove the hook
8
+ *
9
+ * The key comes from the website: the Agent button in a Clario space shows the whole
10
+ * setup command. setup checks the key with the server, saves it in ~/.clario/agent.json
11
+ * (one key per user that stays the same; it can only post agent activity), copies the hook to
12
+ * ~/.clario/hook.mjs and registers it in this project's .claude/settings.local.json,
13
+ * or in ~/.claude/settings.json with --global.
14
+ */
15
+
16
+ import { execFileSync } from "node:child_process";
17
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
18
+ import { homedir } from "node:os";
19
+ import { dirname, join } from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ const DIR = join(homedir(), ".clario");
23
+ const CONFIG_PATH = join(DIR, "agent.json");
24
+ const HOOK_PATH = join(DIR, "hook.mjs");
25
+ const HOOK_SOURCE = join(dirname(fileURLToPath(import.meta.url)), "clario-agent-hook.mjs");
26
+ // Where this package kept its key before it was renamed: setup reuses that key instead of asking again.
27
+ const OLD_CONFIG_PATH = join(homedir(), ".viora", "agent.json");
28
+ // The live Clario server; pass --api-url http://localhost:3001 for a local one.
29
+ const DEFAULT_API_URL = "https://api.clario.games";
30
+ const HOOK_EVENTS = ["PreToolUse", "PostToolUse", "Stop", "SessionEnd"];
31
+ const WARN_DAYS = 14; // setup reminds you to get a new key when the saved one has less time left than this
32
+ 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.";
33
+
34
+ class CliError extends Error {}
35
+ const fail = (message) => {
36
+ throw new CliError(message);
37
+ };
38
+
39
+ /** { value } for a JSON file, {} when it doesn't exist; fails on invalid JSON instead of overwriting it. */
40
+ function readJson(file) {
41
+ let text;
42
+ try {
43
+ text = readFileSync(file, "utf8");
44
+ } catch {
45
+ return {};
46
+ }
47
+ try {
48
+ return { value: JSON.parse(text) };
49
+ } catch {
50
+ fail(`${file} is not valid JSON. Fix or remove it, then run this again.`);
51
+ }
52
+ }
53
+
54
+ function writeJson(file, value, { mode } = {}) {
55
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
56
+ const tmp = `${file}.${process.pid}.tmp`;
57
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, mode ? { mode } : {});
58
+ renameSync(tmp, file);
59
+ if (mode) chmodSync(file, mode);
60
+ }
61
+
62
+ /** The readable part of a key (a JWT), or null. The server checks the signature. */
63
+ function keyPayload(token) {
64
+ try {
65
+ const payload = JSON.parse(Buffer.from(String(token).split(".")[1], "base64url").toString());
66
+ return payload && typeof payload === "object" ? payload : null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /** Days until a key expires: Infinity for one that doesn't (today's keys), null when it can't be read. */
73
+ function daysLeft(token) {
74
+ const payload = keyPayload(token);
75
+ if (!payload) return null;
76
+ return typeof payload.exp === "number" ? (payload.exp * 1000 - Date.now()) / 86_400_000 : Infinity;
77
+ }
78
+
79
+ const expiryText = (days) =>
80
+ Number.isFinite(days) ? `works until ${new Date(Date.now() + days * 86_400_000).toDateString()}` : "it doesn't expire";
81
+
82
+ async function post(apiUrl, path, body, token) {
83
+ let res;
84
+ try {
85
+ res = await fetch(`${apiUrl}/api/v1${path}`, {
86
+ method: "POST",
87
+ headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
88
+ body: JSON.stringify(body),
89
+ });
90
+ } catch {
91
+ fail(`Can't reach the Clario server at ${apiUrl}. Start it, or pass --api-url, then run this again.`);
92
+ }
93
+ return { status: res.status, json: await res.json().catch(() => null) };
94
+ }
95
+
96
+ /** A key from the website, checked with the server before it's saved. */
97
+ async function checkKey(key, apiUrl) {
98
+ const payload = keyPayload(key);
99
+ if (!payload || typeof payload.userId !== "string") fail(`That isn't a Clario agent key. ${GET_A_KEY}`);
100
+ if (payload.scope !== "agent") fail(`That's a login token, not an agent key. ${GET_A_KEY}`);
101
+ const days = daysLeft(key);
102
+ if (days === null || days <= 0) fail(`That key has expired. ${GET_A_KEY}`);
103
+ // A "done" update changes nothing anyone sees, and tells the server the hook is set up.
104
+ const res = await post(apiUrl, "/agent-activity", { text: "", state: "done" }, key);
105
+ if (res.status === 403) fail(`The server at ${apiUrl} doesn't accept this key. ${GET_A_KEY}`);
106
+ if (res.status !== 200) fail(`Could not check the key with ${apiUrl} (HTTP ${res.status}).`);
107
+ return { key, days, reused: false };
108
+ }
109
+
110
+ /** The key from an earlier setup, when it still works for this server (e.g. to add another project). */
111
+ function savedKey(config, apiUrl) {
112
+ const days = daysLeft(config.agentKey);
113
+ if (config.agentKey && config.apiUrl === apiUrl && days !== null && days > 0) return { key: config.agentKey, days, reused: true };
114
+ fail(`No agent key yet. ${GET_A_KEY}`);
115
+ }
116
+
117
+ // ---- Claude Code settings -------------------------------------------------------
118
+
119
+ const settingsFile = (global) =>
120
+ global ? join(homedir(), ".claude", "settings.json") : join(process.cwd(), ".claude", "settings.local.json");
121
+
122
+ /** Our hooks from any version (also from before the rename): this package's copy, or the repo script. */
123
+ const isOurHook = (hook) =>
124
+ typeof hook?.command === "string" && /(clario|viora)-agent-hook\.mjs|[\\/]\.(clario|viora)[\\/]hook\.mjs/.test(hook.command);
125
+
126
+ /** Settings with every one of our hooks removed; other hooks and settings are left exactly as they were. */
127
+ function withoutOurHooks(settings) {
128
+ if (!settings.hooks || typeof settings.hooks !== "object") return settings;
129
+ const hooks = {};
130
+ for (const [event, groups] of Object.entries(settings.hooks)) {
131
+ if (!Array.isArray(groups)) {
132
+ hooks[event] = groups;
133
+ continue;
134
+ }
135
+ const kept = groups
136
+ .map((group) => (Array.isArray(group?.hooks) ? { ...group, hooks: group.hooks.filter((h) => !isOurHook(h)) } : group))
137
+ .filter((group) => !Array.isArray(group?.hooks) || group.hooks.length > 0);
138
+ if (kept.length) hooks[event] = kept;
139
+ }
140
+ const { hooks: _old, ...rest } = settings;
141
+ return Object.keys(hooks).length ? { ...rest, hooks } : rest;
142
+ }
143
+
144
+ const countOurHooks = (settings) =>
145
+ Object.values(settings?.hooks ?? {}).flat().flatMap((group) => group?.hooks ?? []).filter(isOurHook).length;
146
+
147
+ /** Add the hook for every event, replacing older entries of ours so it never fires twice. */
148
+ function registerHooks(file) {
149
+ const { value = {} } = readJson(file);
150
+ if (existsSync(file)) copyFileSync(file, `${file}.clario-backup`);
151
+ const settings = withoutOurHooks(value);
152
+ const hooks = { ...(settings.hooks ?? {}) };
153
+ const command = `node "${HOOK_PATH}"`;
154
+ for (const event of HOOK_EVENTS) hooks[event] = [...(hooks[event] ?? []), { hooks: [{ type: "command", command, timeout: 10 }] }];
155
+ writeJson(file, { ...settings, hooks });
156
+ }
157
+
158
+ function gitIgnored(file) {
159
+ try {
160
+ execFileSync("git", ["check-ignore", "-q", file], { cwd: dirname(file), stdio: "ignore" });
161
+ return true;
162
+ } catch (e) {
163
+ return e.status === 1 ? false : null; // 1: tracked-able; anything else: not a git repo
164
+ }
165
+ }
166
+
167
+ // ---- Commands -----------------------------------------------------------------------
168
+
169
+ async function setup(opts) {
170
+ const { value: config = readJson(OLD_CONFIG_PATH).value ?? {} } = readJson(CONFIG_PATH);
171
+ // A new key comes from the live site unless --api-url says otherwise; re-running setup keeps the saved server.
172
+ const apiUrl = String(opts.apiUrl || (opts.key ? DEFAULT_API_URL : config.apiUrl) || DEFAULT_API_URL).replace(/\/+$/, "");
173
+ const { key, days, reused } = opts.key ? await checkKey(opts.key, apiUrl) : savedKey(config, apiUrl);
174
+
175
+ // An old 7-day login token isn't needed any more; everything else (shareGit…) stays.
176
+ const { token: _loginToken, ...rest } = config;
177
+ writeJson(CONFIG_PATH, { ...rest, apiUrl, agentKey: key }, { mode: 0o600 });
178
+ mkdirSync(DIR, { recursive: true, mode: 0o700 });
179
+ copyFileSync(HOOK_SOURCE, HOOK_PATH);
180
+
181
+ const file = settingsFile(opts.global);
182
+ registerHooks(file);
183
+
184
+ console.log(`${reused ? "Kept your agent key" : "Saved your agent key"} in ${CONFIG_PATH} (${expiryText(days)}).`);
185
+ if (reused && days < WARN_DAYS) console.log(`It runs out in ${Math.ceil(days)} days. ${GET_A_KEY}`);
186
+ console.log(`Hook registered in ${file}${opts.global ? " for every project" : " for this project only"}.`);
187
+ if (!opts.global && gitIgnored(file) === false) {
188
+ console.log("Note: .claude/settings.local.json isn't git-ignored here. It holds no secrets, but you may want to ignore it.");
189
+ }
190
+ console.log("Open your Clario space and give Claude a task: your avatar's card shows what it's doing.");
191
+ }
192
+
193
+ function status() {
194
+ const { value: config = {} } = readJson(CONFIG_PATH);
195
+ const left = daysLeft(config.agentKey ?? config.token);
196
+ console.log(`Server: ${config.apiUrl ?? "not set"}`);
197
+ console.log(
198
+ config.agentKey || config.token
199
+ ? `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)"}`
200
+ : "Key: none (run setup)",
201
+ );
202
+ console.log(`Hook: ${existsSync(HOOK_PATH) ? HOOK_PATH : "not installed (run setup)"}`);
203
+ for (const global of [false, true]) {
204
+ const file = settingsFile(global);
205
+ const n = countOurHooks(readJson(file).value);
206
+ console.log(`${global ? "All projects" : "This project"}: ${n ? `registered (${file})` : "not registered"}`);
207
+ }
208
+ }
209
+
210
+ function uninstall(opts) {
211
+ const file = settingsFile(opts.global);
212
+ const { value } = readJson(file);
213
+ if (!value || !countOurHooks(value)) {
214
+ console.log(`No Clario hook in ${file}.`);
215
+ return;
216
+ }
217
+ copyFileSync(file, `${file}.clario-backup`);
218
+ writeJson(file, withoutOurHooks(value));
219
+ console.log(`Removed the Clario hook from ${file}. Your key stays in ${CONFIG_PATH}; delete that file to remove it too.`);
220
+ }
221
+
222
+ const HELP = `clario-agent: show what your Claude Code agent is doing on your Clario avatar
223
+
224
+ clario-agent setup --key KEY [--api-url URL] [--global] save your key and register the hook
225
+ clario-agent status show what is set up
226
+ clario-agent uninstall [--global] remove the hook
227
+
228
+ Get the setup command (with your key) from the Agent button in your Clario space.
229
+ Without --global the hook is registered only for the project you run it in.
230
+ Without --key, setup reuses the key you saved before. The server is ${DEFAULT_API_URL}
231
+ unless you pass --api-url.`;
232
+
233
+ export async function run(argv) {
234
+ const [command = "help", ...rest] = argv;
235
+ const value = (flag) => {
236
+ const i = rest.indexOf(flag);
237
+ return i > -1 ? rest[i + 1] : undefined;
238
+ };
239
+ const opts = { global: rest.includes("--global"), apiUrl: value("--api-url"), key: value("--key") };
240
+ try {
241
+ if (command === "setup") await setup(opts);
242
+ else if (command === "status") status();
243
+ else if (command === "uninstall") uninstall(opts);
244
+ else console.log(HELP);
245
+ } catch (e) {
246
+ if (!(e instanceof CliError)) throw e;
247
+ console.error(e.message);
248
+ process.exitCode = 1;
249
+ }
250
+ }
251
+
252
+ // Run when called as a command (npm links the bin, so compare real paths).
253
+ if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
254
+ run(process.argv.slice(2));
255
+ }
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "clario-agent",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Show what your Claude Code agent is doing (project, branch, commit) on your Clario avatar.",
5
+ "type": "module",
6
+ "bin": {
7
+ "clario-agent": "cli.mjs"
8
+ },
9
+ "files": [
10
+ "cli.mjs",
11
+ "setup.mjs",
12
+ "clario-agent-hook.mjs",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "keywords": [
19
+ "clario",
20
+ "claude-code",
21
+ "hooks",
22
+ "agent",
23
+ "metaverse"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Deepak-negi11/graphite-hacks.git",
28
+ "directory": "tools/agent-hook"
29
+ },
30
+ "license": "MIT"
31
+ }
package/setup.mjs ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // Same as `clario-agent setup` (see cli.mjs), for running straight from this repo:
3
+ // node tools/agent-hook/setup.mjs [--api-url http://localhost:3001] [--global]
4
+ import { run } from "./cli.mjs";
5
+
6
+ run(["setup", ...process.argv.slice(2)]);