useathena 0.2.0 → 0.2.1

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
@@ -165,6 +165,9 @@ installs the browser-sensor API as a login service, and prints the extension set
165
165
  with its token. `--yes` takes the first detected provider and says yes to all of it;
166
166
  opt out per-piece with `--no-hook`, `--no-service`, `--skip-test`, or `--model <spec>`.
167
167
 
168
+ Already installed? `athena update` pulls the latest published version and
169
+ restarts the background serve service on the new code.
170
+
168
171
  Model providers are spec strings, set once by `setup` (or per-run via `ATHENA_MODEL`):
169
172
 
170
173
  ```text
@@ -43,6 +43,13 @@ Restart=always
43
43
  WantedBy=default.target
44
44
  `;
45
45
  }
46
+ export function serviceInstalled(platform = process.platform) {
47
+ if (platform === "darwin")
48
+ return existsSync(join(homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`));
49
+ if (platform === "linux")
50
+ return existsSync(join(homedir(), ".config", "systemd", "user", SYSTEMD_UNIT));
51
+ return false;
52
+ }
46
53
  export function installService(athenaBin, platform = process.platform) {
47
54
  const logPath = join(dirname(dbPath()), "serve.log");
48
55
  if (platform === "darwin") {
@@ -0,0 +1,81 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { installService, serviceInstalled } from "./service.js";
5
+ import { bold, dim, green, red, yellow } from "./format.js";
6
+ export function compareVersions(a, b) {
7
+ const left = a.split(".").map(Number);
8
+ const right = b.split(".").map(Number);
9
+ for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
10
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
11
+ if (diff !== 0)
12
+ return Math.sign(diff);
13
+ }
14
+ return 0;
15
+ }
16
+ export function planUpdate(opts) {
17
+ if (opts.devCheckout)
18
+ return "dev-checkout";
19
+ return compareVersions(opts.current, opts.latest) < 0 ? "install" : "up-to-date";
20
+ }
21
+ export function readPackageInfo(packageRoot) {
22
+ const pkg = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
23
+ return { name: pkg.name, version: pkg.version };
24
+ }
25
+ export async function fetchLatestVersion(name, timeoutMs = 10_000) {
26
+ const response = await fetch(`https://registry.npmjs.org/${name}/latest`, { signal: AbortSignal.timeout(timeoutMs) });
27
+ if (!response.ok)
28
+ throw new Error(`npm registry lookup failed: HTTP ${response.status}`);
29
+ const body = (await response.json());
30
+ if (!body.version)
31
+ throw new Error("npm registry reply had no version");
32
+ return body.version;
33
+ }
34
+ export function nudgeLine(current, latest) {
35
+ if (compareVersions(current, latest) >= 0)
36
+ return undefined;
37
+ return `\n${yellow("↑")} ${latest} is out (you run ${current}) — update with ${bold("athena update")}`;
38
+ }
39
+ /** One quiet line at the end of `athena status` when the registry is ahead; silent offline. */
40
+ export async function updateNudge(packageRoot) {
41
+ try {
42
+ const { name, version } = readPackageInfo(packageRoot);
43
+ return nudgeLine(version, await fetchLatestVersion(name, 1_500));
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ function globalAthenaBin() {
50
+ const prefix = spawnSync("npm", ["prefix", "-g"], { encoding: "utf8" }).stdout?.trim();
51
+ if (!prefix)
52
+ return undefined;
53
+ const bin = join(prefix, "bin", "athena");
54
+ return existsSync(bin) ? bin : undefined;
55
+ }
56
+ export async function runUpdate(packageRoot, athenaBin) {
57
+ const { name, version } = readPackageInfo(packageRoot);
58
+ const latest = await fetchLatestVersion(name);
59
+ const plan = planUpdate({ current: version, latest, devCheckout: existsSync(join(packageRoot, ".git")) });
60
+ if (plan === "dev-checkout") {
61
+ console.log(`${yellow("dev checkout")} at ${packageRoot} — update with ${bold("git pull")} (running ${version}, registry has ${latest})`);
62
+ return;
63
+ }
64
+ if (plan === "up-to-date") {
65
+ console.log(`${green("✓")} athena ${version} is up to date`);
66
+ return;
67
+ }
68
+ console.log(dim(`updating ${name} ${version} → ${latest} (npm install -g)…`));
69
+ const install = spawnSync("npm", ["install", "-g", `${name}@latest`], { encoding: "utf8" });
70
+ if (install.status !== 0) {
71
+ console.log(red(`✗ update failed: ${(install.stderr ?? "").trim().slice(-300)}`));
72
+ console.log(yellow(` retry manually: npm install -g ${name}@latest`));
73
+ process.exitCode = 1;
74
+ return;
75
+ }
76
+ console.log(`${green("✓")} athena ${latest} installed`);
77
+ if (serviceInstalled()) {
78
+ const result = installService(globalAthenaBin() ?? athenaBin);
79
+ console.log(result.installed ? `${green("✓")} serve service restarted on ${latest}` : yellow(result.message));
80
+ }
81
+ }
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@ import { maybeAutoLearn } from "./serve/auto-learn.js";
15
15
  import { recordOutcome } from "./serve/outcome.js";
16
16
  import { loadConfig } from "./config.js";
17
17
  import { installClaudeCodeHook, runSetup } from "./cli/setup.js";
18
+ import { readPackageInfo, runUpdate, updateNudge } from "./cli/update.js";
18
19
  import { bold, dim, green, red, yellow } from "./cli/format.js";
19
20
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
20
21
  const athenaBin = join(packageRoot, "bin", "athena");
@@ -43,6 +44,7 @@ usage: athena <command> [args]
43
44
  ${bold("hook")} install [--print] install the Claude Code sensor hook into ~/.claude/settings.json
44
45
  ${bold("serve")} [--port n] local API for browser sensors (default 127.0.0.1:4517)
45
46
  ${bold("serve")} install|uninstall run the API at login (launchd/systemd user service)
47
+ ${bold("update")} update to the latest published version (restarts serve)
46
48
  ${bold("mcp")} run the MCP stdio server (claude mcp add athena -- athena mcp)
47
49
 
48
50
  store: ${dbPath()} ${dim("(override with ATHENA_DB)")}
@@ -57,6 +59,10 @@ async function main() {
57
59
  console.log(HELP);
58
60
  return;
59
61
  }
62
+ if (command === "version" || command === "--version") {
63
+ console.log(readPackageInfo(packageRoot).version);
64
+ return;
65
+ }
60
66
  if (command === "hook") {
61
67
  await runHook(args);
62
68
  return;
@@ -76,9 +82,13 @@ async function main() {
76
82
  console.log(dim(`\nfor the guided version (model provider, sensors): athena setup`));
77
83
  break;
78
84
  }
79
- case "status":
85
+ case "status": {
80
86
  console.log(cmdStatus(store));
87
+ const nudge = await updateNudge(packageRoot);
88
+ if (nudge)
89
+ console.log(nudge);
81
90
  break;
91
+ }
82
92
  case "rules": {
83
93
  const domain = flag(args, "domain");
84
94
  console.log(cmdRules(store, {
@@ -206,6 +216,10 @@ async function main() {
206
216
  });
207
217
  break;
208
218
  }
219
+ case "update": {
220
+ await runUpdate(packageRoot, athenaBin);
221
+ break;
222
+ }
209
223
  case "mcp": {
210
224
  // stdio transport: stdout belongs to the protocol, so print nothing.
211
225
  const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "useathena",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "athena captures tacit knowledge from real work so agents become truly autonomous and reliable.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {