embertime 0.1.1 → 0.1.3
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/README.md +2 -1
- package/cli.mjs +32 -20
- package/editors.mjs +50 -0
- package/ember-hook.mjs +10 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@ Cursor Agent** with one command. Your time shows up on
|
|
|
5
5
|
[Ember](https://ember-dev.vercel.app) as a yearly heatmap, language stats and a
|
|
6
6
|
developer leaderboard.
|
|
7
7
|
|
|
8
|
-
Ember records **when** and **in which project** you code.
|
|
8
|
+
Ember records **when** and **in which project** you write code. Only writing counts:
|
|
9
|
+
an editor or agent that is just open adds no time. It never sends your
|
|
9
10
|
code, file contents, file paths or AI prompts.
|
|
10
11
|
|
|
11
12
|
## Setup
|
package/cli.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import path from 'node:path';
|
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { apply, writeEmberConfig } from './install.mjs';
|
|
17
|
+
import { EDITORS, findEditor, runEditor } from './editors.mjs';
|
|
17
18
|
|
|
18
19
|
// Public server and Marketplace id. Change DEFAULT_API_URL before publishing.
|
|
19
20
|
export const DEFAULT_API_URL = process.env.EMBER_DEFAULT_API_URL || 'https://ember-dev.vercel.app';
|
|
@@ -21,12 +22,6 @@ export const EXTENSION_ID = 'ember-dev.embertime';
|
|
|
21
22
|
const KEY_RE = /^ember_[0-9a-f]{40}$/i;
|
|
22
23
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const PAYLOAD = ['ember-hook.mjs', 'install.mjs', 'package.json'];
|
|
24
|
-
const EDITORS = [
|
|
25
|
-
{ cli: 'code', name: 'VS Code' },
|
|
26
|
-
{ cli: 'cursor', name: 'Cursor' },
|
|
27
|
-
{ cli: 'windsurf', name: 'Windsurf' },
|
|
28
|
-
{ cli: 'codium', name: 'VSCodium' },
|
|
29
|
-
];
|
|
30
25
|
|
|
31
26
|
const env = process.env;
|
|
32
27
|
const home = () => env.HOME || os.homedir();
|
|
@@ -36,6 +31,11 @@ const c = process.stdout.isTTY && !env.NO_COLOR
|
|
|
36
31
|
? { ok: (s) => `\x1b[32m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m`, warn: (s) => `\x1b[33m${s}\x1b[0m`, bold: (s) => `\x1b[1m${s}\x1b[0m` }
|
|
37
32
|
: { ok: (s) => s, dim: (s) => s, warn: (s) => s, bold: (s) => s };
|
|
38
33
|
|
|
34
|
+
/** Each installed editor with the command that launches it. */
|
|
35
|
+
function installedEditors() {
|
|
36
|
+
return EDITORS.map((e) => ({ ...e, command: findEditor(e, { home: home(), localAppData: env.LOCALAPPDATA || '' }) })).filter((e) => e.command);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
39
|
function onPath(cmd) {
|
|
40
40
|
const r = spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd], { encoding: 'utf8' });
|
|
41
41
|
return r.status === 0;
|
|
@@ -121,10 +121,14 @@ async function setup(o) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
if (!o.skipEditors) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const r =
|
|
127
|
-
|
|
124
|
+
const editors = installedEditors();
|
|
125
|
+
for (const e of editors) {
|
|
126
|
+
const r = runEditor(e.command, ['--install-extension', o.vsix || EXTENSION_ID, '--force']);
|
|
127
|
+
const why = (r.stderr || r.stdout || r.error?.message || '').trim().split('\n').pop();
|
|
128
|
+
results.push([e.name, r.status === 0 ? 'extension installed (restart the editor)' : `install failed: ${why}`, r.status === 0]);
|
|
129
|
+
}
|
|
130
|
+
if (editors.length === 0) {
|
|
131
|
+
results.push(['Editors', 'none found. In VS Code, Cursor or Windsurf, open Extensions, search "embertime" and install it']);
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
134
|
|
|
@@ -136,28 +140,33 @@ async function setup(o) {
|
|
|
136
140
|
|
|
137
141
|
async function status() {
|
|
138
142
|
const cfg = readConfig();
|
|
139
|
-
console.log(
|
|
143
|
+
console.log(`${'Config'.padEnd(13)}${cfg.api_key ? `key …${cfg.api_key.slice(-4)}, server ${cfg.api_url}` : 'no key (run setup)'}`);
|
|
140
144
|
for (const [label, file] of [
|
|
141
|
-
['
|
|
142
|
-
['
|
|
143
|
-
['
|
|
145
|
+
['Claude Code', path.join(home(), '.claude', 'settings.json')],
|
|
146
|
+
['Codex', path.join(env.CODEX_HOME || path.join(home(), '.codex'), 'hooks.json')],
|
|
147
|
+
['Cursor Agent', path.join(home(), '.cursor', 'hooks.json')],
|
|
144
148
|
]) {
|
|
145
149
|
let on = false;
|
|
146
150
|
try { on = fs.readFileSync(file, 'utf8').includes('ember-hook.mjs'); } catch { /* missing */ }
|
|
147
|
-
console.log(`${label.padEnd(
|
|
151
|
+
console.log(`${label.padEnd(13)}${on ? 'hooks installed' : 'not connected'}`);
|
|
152
|
+
}
|
|
153
|
+
for (const e of installedEditors()) {
|
|
154
|
+
const r = runEditor(e.command, ['--list-extensions']);
|
|
155
|
+
const on = r.status === 0 && r.stdout.toLowerCase().split(/\r?\n/).includes(EXTENSION_ID);
|
|
156
|
+
console.log(`${e.name.padEnd(13)}${on ? 'extension installed' : 'extension missing (run setup)'}`);
|
|
148
157
|
}
|
|
149
158
|
const q = path.join(emberHome(), 'queue.jsonl');
|
|
150
159
|
const queued = fs.existsSync(q) ? fs.readFileSync(q, 'utf8').split('\n').filter(Boolean).length : 0;
|
|
151
|
-
console.log(
|
|
160
|
+
console.log(`${'Queue'.padEnd(13)}${queued} heartbeat(s) waiting`);
|
|
152
161
|
const err = path.join(emberHome(), 'last_error');
|
|
153
|
-
if (fs.existsSync(err)) console.log(
|
|
162
|
+
if (fs.existsSync(err)) console.log(`${'Last error'.padEnd(13)}${fs.readFileSync(err, 'utf8').trim()}`);
|
|
154
163
|
if (cfg.api_key) {
|
|
155
164
|
try {
|
|
156
165
|
const res = await fetch(`${cfg.api_url}/api/v1/me/today`, { headers: { Authorization: `Bearer ${cfg.api_key}` }, signal: AbortSignal.timeout(6000) });
|
|
157
166
|
const body = res.ok ? await res.json() : null;
|
|
158
|
-
console.log(
|
|
167
|
+
console.log(`${'Server'.padEnd(13)}${body ? `today ${body.text} as @${body.username}` : `HTTP ${res.status}`}`);
|
|
159
168
|
} catch {
|
|
160
|
-
console.log('
|
|
169
|
+
console.log(`${'Server'.padEnd(13)}unreachable`);
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
}
|
|
@@ -171,7 +180,10 @@ function uninstall(o) {
|
|
|
171
180
|
console.log(`${t.padEnd(8)} skipped: ${e.message}`);
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
|
-
for (const e of
|
|
183
|
+
for (const e of installedEditors()) {
|
|
184
|
+
const r = runEditor(e.command, ['--uninstall-extension', EXTENSION_ID]);
|
|
185
|
+
console.log(`${e.name.padEnd(13)}${r.status === 0 ? 'extension removed' : 'extension not installed'}`);
|
|
186
|
+
}
|
|
175
187
|
if (o.purge) {
|
|
176
188
|
fs.rmSync(emberHome(), { recursive: true, force: true });
|
|
177
189
|
console.log(`removed ${emberHome()}`);
|
package/editors.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Finds the command-line launcher of each VS Code-family editor.
|
|
2
|
+
//
|
|
3
|
+
// Most people never run "Shell Command: Install 'code' command in PATH", so
|
|
4
|
+
// looking only at PATH misses editors that are installed. When the command is
|
|
5
|
+
// not on PATH we also look inside the usual install locations.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
export const EDITORS = [
|
|
12
|
+
{ cli: 'code', name: 'VS Code', mac: 'Visual Studio Code.app', win: 'Microsoft VS Code', winBin: 'bin', linux: ['/usr/share/code/bin/code', '/snap/bin/code'] },
|
|
13
|
+
{ cli: 'cursor', name: 'Cursor', mac: 'Cursor.app', win: 'cursor', winBin: 'resources/app/bin', linux: ['/usr/share/cursor/bin/cursor', '/opt/Cursor/resources/app/bin/cursor'] },
|
|
14
|
+
{ cli: 'windsurf', name: 'Windsurf', mac: 'Windsurf.app', win: 'Windsurf', winBin: 'bin', linux: ['/usr/share/windsurf/bin/windsurf'] },
|
|
15
|
+
{ cli: 'codium', name: 'VSCodium', mac: 'VSCodium.app', win: 'VSCodium', winBin: 'bin', linux: ['/usr/share/codium/bin/codium', '/snap/bin/codium'] },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function onPath(cmd, platform) {
|
|
19
|
+
const r = spawnSync(platform === 'win32' ? 'where' : 'which', [cmd], { encoding: 'utf8' });
|
|
20
|
+
return r.status === 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Install locations to try for one editor, most likely first. */
|
|
24
|
+
export function candidates(editor, { platform = process.platform, home = '', localAppData = '' } = {}) {
|
|
25
|
+
if (platform === 'darwin') {
|
|
26
|
+
const bin = path.posix.join('Contents/Resources/app/bin', editor.cli);
|
|
27
|
+
return ['/Applications', path.posix.join(home, 'Applications')].map((dir) => path.posix.join(dir, editor.mac, bin));
|
|
28
|
+
}
|
|
29
|
+
if (platform === 'win32') {
|
|
30
|
+
if (!localAppData) return [];
|
|
31
|
+
return [path.win32.join(localAppData, 'Programs', editor.win, ...editor.winBin.split('/'), `${editor.cli}.cmd`)];
|
|
32
|
+
}
|
|
33
|
+
return editor.linux;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The command to run for an editor: its PATH name, the full path of an
|
|
38
|
+
* installed copy, or null when the editor isn't installed.
|
|
39
|
+
*/
|
|
40
|
+
export function findEditor(editor, { platform = process.platform, home = '', localAppData = '', exists = fs.existsSync, which = onPath } = {}) {
|
|
41
|
+
if (which(editor.cli, platform)) return editor.cli;
|
|
42
|
+
return candidates(editor, { platform, home, localAppData }).find((p) => exists(p)) ?? null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Runs an editor's CLI. Windows launchers are .cmd files, which need a shell. */
|
|
46
|
+
export function runEditor(command, args) {
|
|
47
|
+
if (process.platform !== 'win32') return spawnSync(command, args, { encoding: 'utf8' });
|
|
48
|
+
const quote = (s) => (/[\s"]/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s);
|
|
49
|
+
return spawnSync([command, ...args].map(quote).join(' '), { encoding: 'utf8', shell: true });
|
|
50
|
+
}
|
package/ember-hook.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import path from 'node:path';
|
|
|
21
21
|
import crypto from 'node:crypto';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
|
|
24
|
-
export const VERSION = '0.1.
|
|
24
|
+
export const VERSION = '0.1.3';
|
|
25
25
|
export const AGENTS = ['claude-code', 'codex', 'cursor-agent'];
|
|
26
26
|
|
|
27
27
|
/** Events whose heartbeats are throttled: they fire on every tool call or file edit. */
|
|
@@ -759,6 +759,15 @@ export async function runHook({ argv = [], env = process.env, stdinText = '' } =
|
|
|
759
759
|
const payload = parsePayload(stdinText);
|
|
760
760
|
const event = typeof payload.hook_event_name === 'string' ? payload.hook_event_name : '';
|
|
761
761
|
const hb = buildHeartbeat(payload, { agent, env });
|
|
762
|
+
const { apiUrl, apiKey } = loadConfig(home, env);
|
|
763
|
+
|
|
764
|
+
// Only an agent changing a file counts as coding. Opening a session,
|
|
765
|
+
// sending a prompt or reading files does not; those events just deliver
|
|
766
|
+
// anything still queued.
|
|
767
|
+
if (!hb.is_write) {
|
|
768
|
+
await flushQueue(home, { apiUrl, apiKey });
|
|
769
|
+
return { status: 'not-writing' };
|
|
770
|
+
}
|
|
762
771
|
|
|
763
772
|
const state = readState(home);
|
|
764
773
|
if (shouldThrottle(state, hb, event)) return { status: 'throttled' };
|
|
@@ -766,7 +775,6 @@ export async function runHook({ argv = [], env = process.env, stdinText = '' } =
|
|
|
766
775
|
writeState(home, state);
|
|
767
776
|
|
|
768
777
|
appendToQueue(home, hb);
|
|
769
|
-
const { apiUrl, apiKey } = loadConfig(home, env);
|
|
770
778
|
return await flushQueue(home, { apiUrl, apiKey });
|
|
771
779
|
} catch (e) {
|
|
772
780
|
log(home, 'error', `hook failed: ${e?.stack || e}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "embertime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"cli.mjs",
|
|
11
|
+
"editors.mjs",
|
|
11
12
|
"ember-hook.mjs",
|
|
12
13
|
"install.mjs",
|
|
13
14
|
"hooks",
|