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.
- package/README.md +30 -41
- package/cli.mjs +59 -60
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
51
|
-
`
|
|
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
|
-
>
|
|
57
|
-
>
|
|
58
|
-
>
|
|
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]
|
|
6
|
-
* npx clario-agent status
|
|
7
|
-
* npx clario-agent uninstall [--global]
|
|
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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
61
|
-
function
|
|
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
|
|
64
|
-
return typeof
|
|
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
|
-
/**
|
|
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 };
|
|
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
|
-
/**
|
|
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 };
|
|
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
|
-
|
|
179
|
-
const
|
|
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
|
-
//
|
|
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
|
-
|
|
191
|
-
|
|
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
|
|
231
|
-
clario-agent status
|
|
232
|
-
clario-agent uninstall [--global]
|
|
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
|
-
--
|
|
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
|
|
240
|
-
|
|
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();
|