bestviber 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,43 @@
1
- # Temporary Holding Version
1
+ # bestviber
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Focus timer and focus analytics for you and your AI agents.
4
+
5
+ ## Claude Code, in two commands
6
+
7
+ ```sh
8
+ npm install -g bestviber
9
+ bestviber login
10
+ ```
11
+
12
+ `login` opens your browser to approve a short code, installs the agent skill,
13
+ and asks whether to track your Claude Code sessions automatically. Say yes and
14
+ there is nothing else to do: every Claude Code session is timed and filed
15
+ under the repository you work in. Quiet stretches are recorded as pauses, and
16
+ ten quiet minutes end the session. Then ask your agent "how was my week?", or
17
+ run `bestviber week`.
18
+
19
+ `bestviber logout` signs out and removes the tracking.
20
+
21
+ ## By hand
22
+
23
+ ```sh
24
+ bestviber start # start a focus session
25
+ bestviber stop --note "wrote the launch post"
26
+ bestviber week # this week: total, by day, by hour, by tag and project
27
+ bestviber insights # peak hours, best weekday, streaks, trends
28
+ ```
29
+
30
+ Every command takes `--json` for scripts and agents. `bestviber tools` lists
31
+ every action the server offers and `bestviber run <action>` runs any of them,
32
+ so new features work without updating the CLI.
33
+
34
+ ## Other agents
35
+
36
+ - The skill goes to `~/.claude/skills/bestviber`, and also to
37
+ `~/.agents/skills/bestviber` if you have an `~/.agents` folder. Reinstall or
38
+ update it with `bestviber skill install`.
39
+ - `bestviber mcp` prints how to connect any MCP client to
40
+ `https://www.bestviber.com/api/mcp`. Sign in first: the setup line uses
41
+ your token.
42
+
43
+ Free. Same account as [bestviber.com](https://www.bestviber.com).
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs'
3
+ import { parse } from '../src/args.mjs'
4
+ import { CliError } from '../src/client.mjs'
5
+ import { commands } from '../src/commands.mjs'
6
+
7
+ const pkg = JSON.parse(
8
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
9
+ )
10
+
11
+ function help() {
12
+ const width = Math.max(...Object.keys(commands).map((c) => c.length))
13
+ const lines = Object.entries(commands)
14
+ .filter(([, c]) => !c.hidden)
15
+ .map(([name, c]) => ` ${name.padEnd(width)} ${c.summary}`)
16
+ return `bestviber ${pkg.version}: focus timer and focus analytics for you and your agents
17
+
18
+ Usage: bestviber <command> [options]
19
+
20
+ ${lines.join('\n')}
21
+
22
+ Options:
23
+ --json Print raw JSON (for scripts and agents)
24
+ --help Show help for a command
25
+
26
+ Start with \`bestviber login\`. Full docs: https://www.bestviber.com/llms.txt`
27
+ }
28
+
29
+ async function main() {
30
+ const [name, ...rest] = process.argv.slice(2)
31
+ if (!name || name === 'help' || name === '--help' || name === '-h') {
32
+ console.log(help())
33
+ return
34
+ }
35
+ if (name === '--version' || name === '-v') {
36
+ console.log(pkg.version)
37
+ return
38
+ }
39
+ const command = commands[name]
40
+ if (!command)
41
+ throw new CliError(`Unknown command "${name}". Run \`bestviber help\`.`)
42
+ const { flags, args } = parse(rest)
43
+ if (flags.help) {
44
+ console.log(
45
+ `${command.summary}\n\nUsage: bestviber ${command.usage ?? name}`,
46
+ )
47
+ return
48
+ }
49
+ const json = flags.json === true
50
+ delete flags.json
51
+ const result = await command.run({ flags, args, json })
52
+ if (result === undefined) return
53
+ console.log(
54
+ json && typeof result !== 'string'
55
+ ? JSON.stringify(result, null, 2)
56
+ : result,
57
+ )
58
+ }
59
+
60
+ main().catch((err) => {
61
+ if (err instanceof CliError) {
62
+ console.error(`bestviber: ${err.message}`)
63
+ process.exit(err.exitCode)
64
+ }
65
+ console.error(err)
66
+ process.exit(1)
67
+ })
package/package.json CHANGED
@@ -1,6 +1,30 @@
1
1
  {
2
2
  "name": "bestviber",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0",
4
+ "description": "Focus timer and focus analytics for you and your AI agents. Start, pause and finish sessions, and get daily, weekly and monthly stats from the terminal.",
5
+ "keywords": [
6
+ "focus",
7
+ "timer",
8
+ "pomodoro",
9
+ "deep-work",
10
+ "productivity",
11
+ "time-tracking",
12
+ "cli",
13
+ "ai-agent",
14
+ "mcp"
15
+ ],
16
+ "homepage": "https://www.bestviber.com",
17
+ "license": "MIT",
18
+ "type": "module",
19
+ "bin": {
20
+ "bestviber": "bin/bestviber.mjs"
21
+ },
22
+ "files": [
23
+ "bin",
24
+ "src",
25
+ "README.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20"
29
+ }
6
30
  }
package/src/args.mjs ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `--key value`, `--key=value`, `--flag`, `--no-flag` (sets both `flag:
3
+ * false` and `no-flag: true`); a repeated key becomes an array. Everything
4
+ * else is positional.
5
+ * @param {string[]} argv
6
+ */
7
+ export function parse(argv) {
8
+ /** @type {Record<string, any>} */
9
+ const flags = {}
10
+ /** @type {string[]} */
11
+ const args = []
12
+ const set = (/** @type {string} */ key, /** @type {unknown} */ value) => {
13
+ flags[key] = key in flags ? [...[flags[key]].flat(), value] : value
14
+ }
15
+ for (let i = 0; i < argv.length; i++) {
16
+ const a = argv[i]
17
+ if (!a.startsWith('--')) {
18
+ args.push(a)
19
+ continue
20
+ }
21
+ const body = a.slice(2)
22
+ const eq = body.indexOf('=')
23
+ if (eq !== -1) set(body.slice(0, eq), body.slice(eq + 1))
24
+ else if (body.startsWith('no-')) {
25
+ flags[body.slice(3)] = false
26
+ flags[body] = true
27
+ } else if (i + 1 < argv.length && !argv[i + 1].startsWith('--'))
28
+ set(body, argv[++i])
29
+ else set(body, true)
30
+ }
31
+ return { flags, args }
32
+ }
package/src/client.mjs ADDED
@@ -0,0 +1,136 @@
1
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'
2
+ import { homedir } from 'node:os'
3
+ import { dirname, join } from 'node:path'
4
+
5
+ export const DEFAULT_URL = 'https://www.bestviber.com'
6
+ export const CLIENT_ID = 'bestviber-cli'
7
+
8
+ export class CliError extends Error {
9
+ /** @param {string} message @param {number} [exitCode] */
10
+ constructor(message, exitCode = 1) {
11
+ super(message)
12
+ this.exitCode = exitCode
13
+ }
14
+ }
15
+
16
+ // ───── config ─────
17
+
18
+ export function configPath() {
19
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), '.config')
20
+ return join(base, 'bestviber', 'config.json')
21
+ }
22
+
23
+ /** @returns {Promise<{ token?: string, url?: string, email?: string }>} */
24
+ export async function readConfig() {
25
+ try {
26
+ return JSON.parse(await readFile(configPath(), 'utf8'))
27
+ } catch {
28
+ return {}
29
+ }
30
+ }
31
+
32
+ /** @param {{ token?: string, url?: string, email?: string }} config */
33
+ export async function writeConfig(config) {
34
+ const path = configPath()
35
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 })
36
+ // The token is a bearer credential: owner-only.
37
+ await writeFile(path, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 })
38
+ }
39
+
40
+ export async function clearConfig() {
41
+ await rm(configPath(), { force: true })
42
+ }
43
+
44
+ export async function baseUrl() {
45
+ const config = await readConfig()
46
+ return (process.env.BESTVIBER_URL || config.url || DEFAULT_URL).replace(
47
+ /\/+$/,
48
+ '',
49
+ )
50
+ }
51
+
52
+ export async function token() {
53
+ return process.env.BESTVIBER_TOKEN || (await readConfig()).token
54
+ }
55
+
56
+ export function localTimeZone() {
57
+ return Intl.DateTimeFormat().resolvedOptions().timeZone
58
+ }
59
+
60
+ // ───── http ─────
61
+
62
+ /**
63
+ * @param {string} path
64
+ * @param {{ method?: string, body?: unknown, auth?: boolean }} [opts]
65
+ */
66
+ export async function request(path, opts = {}) {
67
+ /** @type {Record<string, string>} */
68
+ const headers = {
69
+ 'Content-Type': 'application/json',
70
+ 'X-Timezone': localTimeZone(),
71
+ 'User-Agent': 'bestviber-cli',
72
+ }
73
+ if (opts.auth !== false) {
74
+ const t = await token()
75
+ if (!t) throw new CliError('Not signed in. Run `bestviber login` first.', 2)
76
+ headers.Authorization = `Bearer ${t}`
77
+ }
78
+ let res
79
+ try {
80
+ res = await fetch(`${await baseUrl()}${path}`, {
81
+ method: opts.method ?? (opts.body === undefined ? 'GET' : 'POST'),
82
+ headers,
83
+ body: opts.body === undefined ? undefined : JSON.stringify(opts.body),
84
+ })
85
+ } catch (err) {
86
+ throw new CliError(
87
+ `Could not reach bestviber: ${/** @type {Error} */ (err).message}`,
88
+ )
89
+ }
90
+ const text = await res.text()
91
+ let data
92
+ try {
93
+ data = text ? JSON.parse(text) : null
94
+ } catch {
95
+ data = { error: text }
96
+ }
97
+ return { status: res.status, ok: res.ok, data }
98
+ }
99
+
100
+ /** Fetches the live action list; new server actions appear here. */
101
+ export async function manifest() {
102
+ const res = await request('/api/v1/actions', { auth: false })
103
+ if (!res.ok)
104
+ throw new CliError(`Could not load actions (HTTP ${res.status}).`)
105
+ return /** @type {Array<{ name: string, title: string, description: string, inputSchema: any, readOnly: boolean, consequential: boolean }>} */ (
106
+ res.data.actions
107
+ )
108
+ }
109
+
110
+ /**
111
+ * Runs an action on the server and returns its result.
112
+ * @param {string} name @param {Record<string, unknown>} [input]
113
+ */
114
+ export async function call(name, input = {}) {
115
+ const res = await request(`/api/v1/actions/${encodeURIComponent(name)}`, {
116
+ body: input,
117
+ })
118
+ if (res.status === 401)
119
+ throw new CliError(
120
+ 'Not signed in, or the session expired. Run `bestviber login`.',
121
+ 2,
122
+ )
123
+ if (!res.ok || !res.data?.ok) {
124
+ const issues = res.data?.issues
125
+ ? '\n' +
126
+ res.data.issues
127
+ .map(
128
+ (/** @type {any} */ i) =>
129
+ ` ${i.path?.join('.') || 'input'}: ${i.message}`,
130
+ )
131
+ .join('\n')
132
+ : ''
133
+ throw new CliError(`${res.data?.error ?? `HTTP ${res.status}`}${issues}`)
134
+ }
135
+ return res.data.result
136
+ }