embertime 0.1.3 → 0.1.4
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/.claude-plugin/plugin.json +1 -1
- package/cli.mjs +1 -0
- package/ember-hook.mjs +57 -6
- package/package.json +4 -2
- package/postinstall.mjs +20 -0
package/cli.mjs
CHANGED
|
@@ -193,6 +193,7 @@ function uninstall(o) {
|
|
|
193
193
|
const HELP = `embertime: track coding time across editors and AI agents
|
|
194
194
|
|
|
195
195
|
npx embertime setup --key <ember_key> [--api-url <url>] [--vsix <file>] [--skip-editors]
|
|
196
|
+
npx embertime setup re-run with the key you already saved
|
|
196
197
|
npx embertime status
|
|
197
198
|
npx embertime uninstall [--purge]
|
|
198
199
|
|
package/ember-hook.mjs
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
// * never writes to stdout for Claude Code / Codex (they may interpret it); for
|
|
10
10
|
// Cursor it prints the minimal JSON reply Cursor expects. Always exits 0
|
|
11
11
|
// * never sends file contents, raw paths or prompt text: files are reduced
|
|
12
|
-
// to
|
|
12
|
+
// to an HMAC of the absolute path, keyed with a random per-machine secret
|
|
13
|
+
// (~/.ember/entity.key), and the project to the git root's basename
|
|
13
14
|
// * heartbeats are appended to ~/.ember/queue.jsonl before any network I/O,
|
|
14
15
|
// so nothing is lost when the server is down
|
|
15
16
|
//
|
|
@@ -21,7 +22,7 @@ import path from 'node:path';
|
|
|
21
22
|
import crypto from 'node:crypto';
|
|
22
23
|
import { fileURLToPath } from 'node:url';
|
|
23
24
|
|
|
24
|
-
export const VERSION = '0.1.
|
|
25
|
+
export const VERSION = '0.1.4';
|
|
25
26
|
export const AGENTS = ['claude-code', 'codex', 'cursor-agent'];
|
|
26
27
|
|
|
27
28
|
/** Events whose heartbeats are throttled: they fire on every tool call or file edit. */
|
|
@@ -59,6 +60,7 @@ function paths(home) {
|
|
|
59
60
|
state: path.join(home, 'hook-state.json'),
|
|
60
61
|
log: path.join(home, 'hook.log'),
|
|
61
62
|
lastError: path.join(home, 'last_error'),
|
|
63
|
+
entityKey: path.join(home, 'entity.key'),
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
|
|
@@ -409,8 +411,57 @@ export function extractFilePath(toolInput, cwd) {
|
|
|
409
411
|
return path.resolve(cwd || process.cwd(), p);
|
|
410
412
|
}
|
|
411
413
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
+
/**
|
|
415
|
+
* File identity sent as `entity`: HMAC-SHA256 of the absolute path under the
|
|
416
|
+
* per-machine key, cut to the 40 hex characters the server accepts. A plain hash
|
|
417
|
+
* of a path could be reversed by guessing likely paths; without the key it can't.
|
|
418
|
+
*/
|
|
419
|
+
export function entityHash(key, file) {
|
|
420
|
+
return crypto.createHmac('sha256', key).update(file).digest('hex').slice(0, 40);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const ENTITY_KEY_RE = /^[a-f0-9]{64}$/;
|
|
424
|
+
|
|
425
|
+
function readEntityKey(file) {
|
|
426
|
+
try {
|
|
427
|
+
const key = fs.readFileSync(file, 'utf8').trim();
|
|
428
|
+
return ENTITY_KEY_RE.test(key) ? key : undefined;
|
|
429
|
+
} catch {
|
|
430
|
+
return undefined;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The random key shared by every collector on this machine (mode 0600), created
|
|
436
|
+
* on first use. Concurrent first runs agree on one key: the first hard link wins.
|
|
437
|
+
* If it can't be stored, a one-off key is used: entities then differ between runs,
|
|
438
|
+
* which only weakens de-duplication, never privacy.
|
|
439
|
+
*/
|
|
440
|
+
export function loadEntityKey(home) {
|
|
441
|
+
const file = paths(home).entityKey;
|
|
442
|
+
const existing = readEntityKey(file);
|
|
443
|
+
if (existing) return existing;
|
|
444
|
+
const key = crypto.randomBytes(32).toString('hex');
|
|
445
|
+
const tmp = `${file}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
|
|
446
|
+
try {
|
|
447
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
448
|
+
fs.writeFileSync(tmp, `${key}\n`, { mode: 0o600 });
|
|
449
|
+
try {
|
|
450
|
+
fs.linkSync(tmp, file);
|
|
451
|
+
return key;
|
|
452
|
+
} catch (e) {
|
|
453
|
+
if (e.code !== 'EEXIST') throw e;
|
|
454
|
+
const winner = readEntityKey(file);
|
|
455
|
+
if (winner) return winner;
|
|
456
|
+
fs.renameSync(tmp, file); // existing file is corrupt: replace it
|
|
457
|
+
return key;
|
|
458
|
+
}
|
|
459
|
+
} catch (e) {
|
|
460
|
+
log(home, 'warn', `could not store entity key: ${e.message}`);
|
|
461
|
+
return key;
|
|
462
|
+
} finally {
|
|
463
|
+
fs.rmSync(tmp, { force: true });
|
|
464
|
+
}
|
|
414
465
|
}
|
|
415
466
|
|
|
416
467
|
export function parsePayload(text) {
|
|
@@ -427,7 +478,7 @@ export function parsePayload(text) {
|
|
|
427
478
|
* Build exactly one heartbeat from a hook payload. Only derived, non-sensitive
|
|
428
479
|
* fields are copied: never prompt text, tool output, file contents or paths.
|
|
429
480
|
*/
|
|
430
|
-
export function buildHeartbeat(payload, { agent, now = Date.now() / 1000, env = process.env } = {}) {
|
|
481
|
+
export function buildHeartbeat(payload, { agent, now = Date.now() / 1000, env = process.env, entityKey } = {}) {
|
|
431
482
|
// Claude Code and Codex send `cwd`; Cursor sends `workspace_roots` (and sets CURSOR_PROJECT_DIR).
|
|
432
483
|
const roots = Array.isArray(payload.workspace_roots) ? payload.workspace_roots.filter((r) => typeof r === 'string' && r) : [];
|
|
433
484
|
const cwd =
|
|
@@ -450,7 +501,7 @@ export function buildHeartbeat(payload, { agent, now = Date.now() / 1000, env =
|
|
|
450
501
|
extractFilePath(payload.tool_input, cwd) ||
|
|
451
502
|
(typeof payload.file_path === 'string' && payload.file_path.trim() ? path.resolve(cwd, payload.file_path.trim()) : undefined);
|
|
452
503
|
if (file) {
|
|
453
|
-
hb.entity =
|
|
504
|
+
hb.entity = entityHash(entityKey ?? loadEntityKey(emberHome(env)), file);
|
|
454
505
|
const language = languageForPath(file);
|
|
455
506
|
if (language) hb.language = language;
|
|
456
507
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "embertime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Connect VS Code, Cursor, Windsurf, Claude Code and Codex to Ember in one command. Tracks coding time, never code or prompts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.mjs",
|
|
11
11
|
"editors.mjs",
|
|
12
|
+
"postinstall.mjs",
|
|
12
13
|
"ember-hook.mjs",
|
|
13
14
|
"install.mjs",
|
|
14
15
|
"hooks",
|
|
@@ -30,7 +31,8 @@
|
|
|
30
31
|
"heatmap"
|
|
31
32
|
],
|
|
32
33
|
"scripts": {
|
|
33
|
-
"test": "node --test test/*.test.mjs"
|
|
34
|
+
"test": "node --test test/*.test.mjs",
|
|
35
|
+
"postinstall": "node postinstall.mjs"
|
|
34
36
|
},
|
|
35
37
|
"repository": {
|
|
36
38
|
"type": "git",
|
package/postinstall.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Runs after `npm install -g embertime` (and upgrades). If Ember was already set
|
|
2
|
+
// up on this machine, refresh the hook copy in ~/.ember/agent-hook so agents run
|
|
3
|
+
// the new version straight away. Your key in ~/.ember/config.json is untouched.
|
|
4
|
+
// Best effort: it never fails the install and does nothing on a fresh machine.
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const dest = path.join(process.env.EMBER_HOME || path.join(os.homedir(), '.ember'), 'agent-hook');
|
|
14
|
+
if (fs.existsSync(path.join(dest, 'ember-hook.mjs'))) {
|
|
15
|
+
for (const f of ['ember-hook.mjs', 'install.mjs', 'package.json']) fs.copyFileSync(path.join(here, f), path.join(dest, f));
|
|
16
|
+
console.log('embertime: updated the hook in ~/.ember/agent-hook (your key is unchanged).');
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
/* never block an install */
|
|
20
|
+
}
|