clario-agent 0.1.0 → 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.
Files changed (3) hide show
  1. package/README.md +30 -41
  2. package/cli.mjs +59 -60
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,55 +7,44 @@ and commit (e.g. `graphite · feat/missions · a1b2c3d`). The badge clears when
7
7
 
8
8
  ## Setup (one command)
9
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`.
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`.
35
33
 
36
34
  ```bash
37
35
  npx clario-agent status # server, key expiry, where the hook is registered
38
36
  npx clario-agent uninstall # remove the hook from this project (add --global for all)
39
37
  ```
40
38
 
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>
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.
49
41
 
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>
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.
55
44
 
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`.
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).
59
48
 
60
49
  ## How it behaves
61
50
 
package/cli.mjs CHANGED
@@ -2,20 +2,21 @@
2
2
  /**
3
3
  * clario-agent: show what your Claude Code agent is doing on your Clario avatar.
4
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
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
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.
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.
12
14
  */
13
15
 
14
16
  import { execFileSync } from "node:child_process";
15
17
  import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
16
18
  import { homedir } from "node:os";
17
19
  import { dirname, join } from "node:path";
18
- import { createInterface } from "node:readline";
19
20
  import { fileURLToPath } from "node:url";
20
21
 
21
22
  const DIR = join(homedir(), ".clario");
@@ -27,7 +28,8 @@ const OLD_CONFIG_PATH = join(homedir(), ".viora", "agent.json");
27
28
  // The live Clario server; pass --api-url http://localhost:3001 for a local one.
28
29
  const DEFAULT_API_URL = "https://api.clario.games";
29
30
  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
+ 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.";
31
33
 
32
34
  class CliError extends Error {}
33
35
  const fail = (message) => {
@@ -57,16 +59,26 @@ function writeJson(file, value, { mode } = {}) {
57
59
  if (mode) chmodSync(file, mode);
58
60
  }
59
61
 
60
- /** Days until a JWT expires, or null when it can't be read. */
61
- function daysLeft(token) {
62
+ /** The readable part of a key (a JWT), or null. The server checks the signature. */
63
+ function keyPayload(token) {
62
64
  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
+ const payload = JSON.parse(Buffer.from(String(token).split(".")[1], "base64url").toString());
66
+ return payload && typeof payload === "object" ? payload : null;
65
67
  } catch {
66
68
  return null;
67
69
  }
68
70
  }
69
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
+
70
82
  async function post(apiUrl, path, body, token) {
71
83
  let res;
72
84
  try {
@@ -81,44 +93,25 @@ async function post(apiUrl, path, body, token) {
81
93
  return { status: res.status, json: await res.json().catch(() => null) };
82
94
  }
83
95
 
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 };
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 };
101
108
  }
102
109
 
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 };
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}`);
122
115
  }
123
116
 
124
117
  // ---- Claude Code settings -------------------------------------------------------
@@ -175,10 +168,11 @@ function gitIgnored(file) {
175
168
 
176
169
  async function setup(opts) {
177
170
  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);
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);
180
174
 
181
- // The 7-day login token isn't needed any more; everything else (shareGit…) stays.
175
+ // An old 7-day login token isn't needed any more; everything else (shareGit…) stays.
182
176
  const { token: _loginToken, ...rest } = config;
183
177
  writeJson(CONFIG_PATH, { ...rest, apiUrl, agentKey: key }, { mode: 0o600 });
184
178
  mkdirSync(DIR, { recursive: true, mode: 0o700 });
@@ -187,8 +181,8 @@ async function setup(opts) {
187
181
  const file = settingsFile(opts.global);
188
182
  registerHooks(file);
189
183
 
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}).`);
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}`);
192
186
  console.log(`Hook registered in ${file}${opts.global ? " for every project" : " for this project only"}.`);
193
187
  if (!opts.global && gitIgnored(file) === false) {
194
188
  console.log("Note: .claude/settings.local.json isn't git-ignored here. It holds no secrets, but you may want to ignore it.");
@@ -202,7 +196,7 @@ function status() {
202
196
  console.log(`Server: ${config.apiUrl ?? "not set"}`);
203
197
  console.log(
204
198
  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)"}`
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)"}`
206
200
  : "Key: none (run setup)",
207
201
  );
208
202
  console.log(`Hook: ${existsSync(HOOK_PATH) ? HOOK_PATH : "not installed (run setup)"}`);
@@ -227,17 +221,22 @@ function uninstall(opts) {
227
221
 
228
222
  const HELP = `clario-agent: show what your Claude Code agent is doing on your Clario avatar
229
223
 
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
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
233
227
 
228
+ Get the setup command (with your key) from the Agent button in your Clario space.
234
229
  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}.`;
230
+ Without --key, setup reuses the key you saved before. The server is ${DEFAULT_API_URL}
231
+ unless you pass --api-url.`;
236
232
 
237
233
  export async function run(argv) {
238
234
  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 };
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") };
241
240
  try {
242
241
  if (command === "setup") await setup(opts);
243
242
  else if (command === "status") status();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clario-agent",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Show what your Claude Code agent is doing (project, branch, commit) on your Clario avatar.",
5
5
  "type": "module",
6
6
  "bin": {