clario-agent 0.0.0-stage → 0.1.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,83 @@
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
+ Make an account at [clario.games](https://www.clario.games) first, then run this inside the
11
+ project you want to share:
12
+
13
+ ```bash
14
+ npx clario-agent setup
15
+ ```
16
+
17
+ It does three things:
18
+
19
+ 1. **Saves an agent key** in `~/.clario/agent.json` (readable only by you). It asks for your
20
+ Clario email and password once (sent only to the Clario server). The key lasts 180 days and
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.
25
+
26
+ Then open your Clario space, give Claude a task in that project, and click your avatar.
27
+ Teammates who do the same and join your space show up on their avatars too.
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`.
59
+
60
+ ## How it behaves
61
+
62
+ - While Claude runs tools (edits, bash commands…) your badge shows "cooking" with a short
63
+ description of the current tool (e.g. `Claude is using Edit page.tsx` or
64
+ `Claude is using Bash $ bun, git`)
65
+ - Your agent card also shows the project folder, git branch and short commit the agent is
66
+ working in
67
+ - When Claude finishes, the badge clears automatically
68
+ - If you're not inside a Clario space, updates are silently dropped
69
+ - Updates are throttled (2s) so fast tool bursts don't spam the room
70
+ - The hook never blocks or fails your agent session: all errors are swallowed
71
+
72
+ ## Privacy notes
73
+
74
+ Everyone in your space sees what is sent. That is the tool name, the edited file's name,
75
+ the names of the programs a shell command runs (`bun`, `git`; never its arguments, which
76
+ can hold tokens or paths), and the project folder, branch and short commit. Never file
77
+ contents, prompts or code.
78
+
79
+ To keep project, branch and commit on your machine, add `"shareGit": false` to
80
+ `~/.clario/agent.json` (or set `CLARIO_SHARE_GIT=0`). Run `npx clario-agent uninstall` to stop
81
+ broadcasting from a project.
82
+
83
+ 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,256 @@
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 [--api-url URL] [--global] agent key + hook, once
6
+ * npx clario-agent status what is set up
7
+ * npx clario-agent uninstall [--global] remove the hook
8
+ *
9
+ * setup saves a 180-day agent key in ~/.clario/agent.json (it can only post agent
10
+ * activity), copies the hook to ~/.clario/hook.mjs and registers it in this project's
11
+ * .claude/settings.local.json, or in ~/.claude/settings.json with --global.
12
+ */
13
+
14
+ import { execFileSync } from "node:child_process";
15
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
16
+ import { homedir } from "node:os";
17
+ import { dirname, join } from "node:path";
18
+ import { createInterface } from "node:readline";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ const DIR = join(homedir(), ".clario");
22
+ const CONFIG_PATH = join(DIR, "agent.json");
23
+ const HOOK_PATH = join(DIR, "hook.mjs");
24
+ const HOOK_SOURCE = join(dirname(fileURLToPath(import.meta.url)), "clario-agent-hook.mjs");
25
+ // Where this package kept its key before it was renamed: setup reuses that key instead of asking again.
26
+ const OLD_CONFIG_PATH = join(homedir(), ".viora", "agent.json");
27
+ // The live Clario server; pass --api-url http://localhost:3001 for a local one.
28
+ const DEFAULT_API_URL = "https://api.clario.games";
29
+ const HOOK_EVENTS = ["PreToolUse", "PostToolUse", "Stop", "SessionEnd"];
30
+ const RENEW_DAYS = 14; // setup makes a new key when the saved one has less time left than this
31
+
32
+ class CliError extends Error {}
33
+ const fail = (message) => {
34
+ throw new CliError(message);
35
+ };
36
+
37
+ /** { value } for a JSON file, {} when it doesn't exist; fails on invalid JSON instead of overwriting it. */
38
+ function readJson(file) {
39
+ let text;
40
+ try {
41
+ text = readFileSync(file, "utf8");
42
+ } catch {
43
+ return {};
44
+ }
45
+ try {
46
+ return { value: JSON.parse(text) };
47
+ } catch {
48
+ fail(`${file} is not valid JSON. Fix or remove it, then run this again.`);
49
+ }
50
+ }
51
+
52
+ function writeJson(file, value, { mode } = {}) {
53
+ mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
54
+ const tmp = `${file}.${process.pid}.tmp`;
55
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, mode ? { mode } : {});
56
+ renameSync(tmp, file);
57
+ if (mode) chmodSync(file, mode);
58
+ }
59
+
60
+ /** Days until a JWT expires, or null when it can't be read. */
61
+ function daysLeft(token) {
62
+ try {
63
+ const { exp } = JSON.parse(Buffer.from(String(token).split(".")[1], "base64url").toString());
64
+ return typeof exp === "number" ? (exp * 1000 - Date.now()) / 86_400_000 : null;
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ async function post(apiUrl, path, body, token) {
71
+ let res;
72
+ try {
73
+ res = await fetch(`${apiUrl}/api/v1${path}`, {
74
+ method: "POST",
75
+ headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
76
+ body: JSON.stringify(body),
77
+ });
78
+ } catch {
79
+ fail(`Can't reach the Clario server at ${apiUrl}. Start it, or pass --api-url, then run this again.`);
80
+ }
81
+ return { status: res.status, json: await res.json().catch(() => null) };
82
+ }
83
+
84
+ /** Email and password from the terminal (the password isn't echoed), or CLARIO_EMAIL / CLARIO_PASSWORD. */
85
+ async function askCredentials() {
86
+ if (process.env.CLARIO_EMAIL && process.env.CLARIO_PASSWORD) {
87
+ return { email: process.env.CLARIO_EMAIL, password: process.env.CLARIO_PASSWORD };
88
+ }
89
+ if (!process.stdin.isTTY) fail("No terminal to ask for your email and password; set CLARIO_EMAIL and CLARIO_PASSWORD.");
90
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
91
+ const ask = (text) => new Promise((resolve) => rl.question(text, resolve));
92
+ const email = (await ask("Clario email: ")).trim();
93
+ const answer = ask("Password: ");
94
+ const write = rl._writeToOutput;
95
+ rl._writeToOutput = () => {}; // hide the password as it's typed
96
+ const password = await answer;
97
+ rl._writeToOutput = write;
98
+ rl.close();
99
+ process.stdout.write("\n");
100
+ return { email, password };
101
+ }
102
+
103
+ /** A usable agent key: the saved one if it has time left, else a new one from the server. */
104
+ async function ensureAgentKey(config, apiUrl) {
105
+ const left = daysLeft(config.agentKey);
106
+ if (config.agentKey && config.apiUrl === apiUrl && left !== null && left > RENEW_DAYS) {
107
+ return { key: config.agentKey, days: left, reused: true };
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 };
122
+ }
123
+
124
+ // ---- Claude Code settings -------------------------------------------------------
125
+
126
+ const settingsFile = (global) =>
127
+ global ? join(homedir(), ".claude", "settings.json") : join(process.cwd(), ".claude", "settings.local.json");
128
+
129
+ /** Our hooks from any version (also from before the rename): this package's copy, or the repo script. */
130
+ const isOurHook = (hook) =>
131
+ typeof hook?.command === "string" && /(clario|viora)-agent-hook\.mjs|[\\/]\.(clario|viora)[\\/]hook\.mjs/.test(hook.command);
132
+
133
+ /** Settings with every one of our hooks removed; other hooks and settings are left exactly as they were. */
134
+ function withoutOurHooks(settings) {
135
+ if (!settings.hooks || typeof settings.hooks !== "object") return settings;
136
+ const hooks = {};
137
+ for (const [event, groups] of Object.entries(settings.hooks)) {
138
+ if (!Array.isArray(groups)) {
139
+ hooks[event] = groups;
140
+ continue;
141
+ }
142
+ const kept = groups
143
+ .map((group) => (Array.isArray(group?.hooks) ? { ...group, hooks: group.hooks.filter((h) => !isOurHook(h)) } : group))
144
+ .filter((group) => !Array.isArray(group?.hooks) || group.hooks.length > 0);
145
+ if (kept.length) hooks[event] = kept;
146
+ }
147
+ const { hooks: _old, ...rest } = settings;
148
+ return Object.keys(hooks).length ? { ...rest, hooks } : rest;
149
+ }
150
+
151
+ const countOurHooks = (settings) =>
152
+ Object.values(settings?.hooks ?? {}).flat().flatMap((group) => group?.hooks ?? []).filter(isOurHook).length;
153
+
154
+ /** Add the hook for every event, replacing older entries of ours so it never fires twice. */
155
+ function registerHooks(file) {
156
+ const { value = {} } = readJson(file);
157
+ if (existsSync(file)) copyFileSync(file, `${file}.clario-backup`);
158
+ const settings = withoutOurHooks(value);
159
+ const hooks = { ...(settings.hooks ?? {}) };
160
+ const command = `node "${HOOK_PATH}"`;
161
+ for (const event of HOOK_EVENTS) hooks[event] = [...(hooks[event] ?? []), { hooks: [{ type: "command", command, timeout: 10 }] }];
162
+ writeJson(file, { ...settings, hooks });
163
+ }
164
+
165
+ function gitIgnored(file) {
166
+ try {
167
+ execFileSync("git", ["check-ignore", "-q", file], { cwd: dirname(file), stdio: "ignore" });
168
+ return true;
169
+ } catch (e) {
170
+ return e.status === 1 ? false : null; // 1: tracked-able; anything else: not a git repo
171
+ }
172
+ }
173
+
174
+ // ---- Commands -----------------------------------------------------------------------
175
+
176
+ async function setup(opts) {
177
+ const { value: config = readJson(OLD_CONFIG_PATH).value ?? {} } = readJson(CONFIG_PATH);
178
+ const apiUrl = String(opts.apiUrl || config.apiUrl || DEFAULT_API_URL).replace(/\/+$/, "");
179
+ const { key, days, reused } = await ensureAgentKey(config, apiUrl);
180
+
181
+ // The 7-day login token isn't needed any more; everything else (shareGit…) stays.
182
+ const { token: _loginToken, ...rest } = config;
183
+ writeJson(CONFIG_PATH, { ...rest, apiUrl, agentKey: key }, { mode: 0o600 });
184
+ mkdirSync(DIR, { recursive: true, mode: 0o700 });
185
+ copyFileSync(HOOK_SOURCE, HOOK_PATH);
186
+
187
+ const file = settingsFile(opts.global);
188
+ registerHooks(file);
189
+
190
+ const until = new Date(Date.now() + days * 86_400_000).toDateString();
191
+ console.log(`${reused ? "Kept your agent key" : "Saved a new agent key"} in ${CONFIG_PATH} (works until ${until}).`);
192
+ console.log(`Hook registered in ${file}${opts.global ? " for every project" : " for this project only"}.`);
193
+ if (!opts.global && gitIgnored(file) === false) {
194
+ console.log("Note: .claude/settings.local.json isn't git-ignored here. It holds no secrets, but you may want to ignore it.");
195
+ }
196
+ console.log("Open your Clario space and give Claude a task: your avatar's card shows what it's doing.");
197
+ }
198
+
199
+ function status() {
200
+ const { value: config = {} } = readJson(CONFIG_PATH);
201
+ const left = daysLeft(config.agentKey ?? config.token);
202
+ console.log(`Server: ${config.apiUrl ?? "not set"}`);
203
+ console.log(
204
+ 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)"}`
206
+ : "Key: none (run setup)",
207
+ );
208
+ console.log(`Hook: ${existsSync(HOOK_PATH) ? HOOK_PATH : "not installed (run setup)"}`);
209
+ for (const global of [false, true]) {
210
+ const file = settingsFile(global);
211
+ const n = countOurHooks(readJson(file).value);
212
+ console.log(`${global ? "All projects" : "This project"}: ${n ? `registered (${file})` : "not registered"}`);
213
+ }
214
+ }
215
+
216
+ function uninstall(opts) {
217
+ const file = settingsFile(opts.global);
218
+ const { value } = readJson(file);
219
+ if (!value || !countOurHooks(value)) {
220
+ console.log(`No Clario hook in ${file}.`);
221
+ return;
222
+ }
223
+ copyFileSync(file, `${file}.clario-backup`);
224
+ writeJson(file, withoutOurHooks(value));
225
+ console.log(`Removed the Clario hook from ${file}. Your key stays in ${CONFIG_PATH}; delete that file to remove it too.`);
226
+ }
227
+
228
+ const HELP = `clario-agent: show what your Claude Code agent is doing on your Clario avatar
229
+
230
+ clario-agent setup [--api-url URL] [--global] save an agent key and register the hook
231
+ clario-agent status show what is set up
232
+ clario-agent uninstall [--global] remove the hook
233
+
234
+ Without --global the hook is registered only for the project you run it in.
235
+ --api-url defaults to the saved server, then ${DEFAULT_API_URL}.`;
236
+
237
+ export async function run(argv) {
238
+ const [command = "help", ...rest] = argv;
239
+ const i = rest.indexOf("--api-url");
240
+ const opts = { global: rest.includes("--global"), apiUrl: i > -1 ? rest[i + 1] : undefined };
241
+ try {
242
+ if (command === "setup") await setup(opts);
243
+ else if (command === "status") status();
244
+ else if (command === "uninstall") uninstall(opts);
245
+ else console.log(HELP);
246
+ } catch (e) {
247
+ if (!(e instanceof CliError)) throw e;
248
+ console.error(e.message);
249
+ process.exitCode = 1;
250
+ }
251
+ }
252
+
253
+ // Run when called as a command (npm links the bin, so compare real paths).
254
+ if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
255
+ run(process.argv.slice(2));
256
+ }
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.1.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)]);