@ai-adoption-score/ai-coding-usage 1.1.0 → 1.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 +10 -6
- package/package.json +1 -1
- package/src/checkin.mjs +3 -1
- package/src/cli.mjs +7 -7
- package/src/config.mjs +33 -1
- package/src/scan.mjs +7 -11
- package/src/sync.mjs +31 -8
package/README.md
CHANGED
|
@@ -21,15 +21,19 @@ In short: connect, choose on the survey page, then share. Before the choice, onl
|
|
|
21
21
|
2. **The add-on puts its hooks and the ping-back in place** and copies itself to `~/.ai-adoption-score`. Why: the
|
|
22
22
|
survey gets the numbers after every session without anyone running a command again, and a hook can't run from
|
|
23
23
|
npx's temporary folder. The ping-back carries no numbers; it lets the report tell "no AI use today" from "no signal".
|
|
24
|
-
3. **It sends the names of the repositories used in the last
|
|
24
|
+
3. **It sends the names of the repositories used in the last 90 days, and nothing else.** Why: the person chooses on
|
|
25
25
|
their survey page, and the page needs the names to list them. The choice isn't asked in the terminal, because the
|
|
26
26
|
line usually runs inside Claude Code or Codex, where the add-on can't ask questions. The server keeps the list for
|
|
27
27
|
an hour at most.
|
|
28
28
|
4. **The person ticks what to share** on their survey page; everything is ticked to start with. When they press
|
|
29
29
|
Share, the list is deleted, and with it the names they didn't tick.
|
|
30
|
-
5. **Only then do numbers leave the Mac:** the last
|
|
30
|
+
5. **Only then do numbers leave the Mac:** the last 90 days of what they ticked, then the new numbers after each
|
|
31
31
|
session. Before every sync the add-on asks what they chose and keeps everything else on the Mac; the server checks
|
|
32
|
-
the same rule again.
|
|
32
|
+
the same rule again. After that first sync, each sync reads the last 30 days only, where rows can still change.
|
|
33
|
+
Why 90: the survey's report can show three months. Claude Code deletes its own records after 30 days unless its
|
|
34
|
+
`cleanupPeriodDays` setting says otherwise, so the older days hold what is left of them; Codex keeps its records.
|
|
35
|
+
A Mac connected with an earlier version, which sent 30 days, sends the 60 before them once, after it updates
|
|
36
|
+
itself and the server says it takes 90.
|
|
33
37
|
6. **It keeps itself up to date.** Once a day the ping-back asks npm for the latest version. A newer one is
|
|
34
38
|
downloaded, checked against the checksum npm lists for it, started once to see that it runs on this Mac's Node, and
|
|
35
39
|
put in place of the add-on's copy; the hooks, the ping-back and the settings stay as they are. Why: a fix or a new
|
|
@@ -41,7 +45,7 @@ In short: connect, choose on the survey page, then share. Before the choice, onl
|
|
|
41
45
|
The person makes their choice on the survey page, so the survey's server keeps it and the add-on follows the server's
|
|
42
46
|
answer. It still checks what it's told:
|
|
43
47
|
|
|
44
|
-
- It reads
|
|
48
|
+
- It reads 90 days back at most, whatever the server asks for, and 30 when the server names no number.
|
|
45
49
|
- It shares nothing when the choice isn't one of the two kinds the server keeps: what the person ticked, or
|
|
46
50
|
`{ "all": true }` for links made before people could choose.
|
|
47
51
|
- It sends the token over https only. Plain http works only for this Mac's own address (`localhost`), for testing.
|
|
@@ -49,8 +53,8 @@ answer. It still checks what it's told:
|
|
|
49
53
|
## Commands
|
|
50
54
|
|
|
51
55
|
- `connect <code> [--server <url>]`: links this Mac to the survey with the code from the survey page and puts the
|
|
52
|
-
hooks and the ping-back in place. Then it sends the names of the repositories used in the last
|
|
53
|
-
else, so the person can choose on their survey page which to share; a background wait shares the last
|
|
56
|
+
hooks and the ping-back in place. Then it sends the names of the repositories used in the last 90 days, and nothing
|
|
57
|
+
else, so the person can choose on their survey page which to share; a background wait shares the last 90 days of
|
|
54
58
|
those once they press Share. The server defaults to `https://app.ai-adoption-score.app`;
|
|
55
59
|
`AI_ADOPTION_SCORE_SERVER` or `--server http://localhost:3000` point it at a local app; any other server has to be
|
|
56
60
|
https. Run again on a Mac that is connected, it keeps that Mac's link on the same server (the survey page still
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-adoption-score/ai-coding-usage",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Shares how you use Claude Code and Codex with your company's AI Adoption Score survey. Never prompts, AI answers, code or file names.",
|
|
5
5
|
"homepage": "https://ai-adoption-score.app/product/ai-coding-usage",
|
|
6
6
|
"repository": {
|
package/src/checkin.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
4
|
import { CLIENT_VERSION, post } from './api.mjs'
|
|
5
|
-
import { isCurrentConfig, readConfig } from './config.mjs'
|
|
5
|
+
import { isCurrentConfig, readConfig, takeHistoryDays } from './config.mjs'
|
|
6
6
|
import { hookState, uninstallAll } from './install.mjs'
|
|
7
7
|
import { log } from './log.mjs'
|
|
8
8
|
import { paths } from './paths.mjs'
|
|
@@ -89,6 +89,8 @@ export async function runCheckIn(env = process.env) {
|
|
|
89
89
|
updateState((current) => {
|
|
90
90
|
current.lastCheckInAt = new Date().toISOString()
|
|
91
91
|
}, env)
|
|
92
|
+
// A server that now takes more days back than these settings say: the next sync sends the days in between once.
|
|
93
|
+
if (status === 'active') takeHistoryDays(config, response.body, env)
|
|
92
94
|
log(`check-in: ${response.status} ${status ?? ''} claude=${checkIn.claudeHook} codex=${checkIn.codexHook}`, env)
|
|
93
95
|
} catch (error) {
|
|
94
96
|
log(`check-in failed: ${error instanceof Error ? error.message : String(error)}`, env)
|
package/src/cli.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { DEFAULT_SERVER, readConfig, writeConfig } from './config.mjs'
|
|
|
10
10
|
import { runHook } from './hook.mjs'
|
|
11
11
|
import { hookState, installAll, uninstallAll } from './install.mjs'
|
|
12
12
|
import { paths } from './paths.mjs'
|
|
13
|
-
import { daysToRead, scanUsage } from './scan.mjs'
|
|
13
|
+
import { MAX_HISTORY_DAYS, daysToRead, scanUsage } from './scan.mjs'
|
|
14
14
|
import { timeByHour } from './rows.mjs'
|
|
15
15
|
import { readState } from './state.mjs'
|
|
16
16
|
import { describeChoice, repositoryList } from './choice.mjs'
|
|
@@ -28,15 +28,15 @@ Usage
|
|
|
28
28
|
ai-coding-usage preview [--days 30] [--company github.com/acme] [--json]
|
|
29
29
|
|
|
30
30
|
connect Link this Mac to your survey with the code from your survey page. It adds a hook to Claude Code and
|
|
31
|
-
Codex and an automatic ping-back, and sends the names of the repositories you used in the last
|
|
32
|
-
you can choose on your survey page which to share. Then it shares the last
|
|
31
|
+
Codex and an automatic ping-back, and sends the names of the repositories you used in the last 90 days, so
|
|
32
|
+
you can choose on your survey page which to share. Then it shares the last 90 days of those, and after
|
|
33
33
|
that, after each session. Run it again to change your choice. Once a day the ping-back looks for a
|
|
34
34
|
newer version of the add-on and puts it in place.
|
|
35
35
|
status Show which survey this Mac shares with, what you chose, its hooks, when it last shared, and its version.
|
|
36
36
|
sync Share now, instead of after the next session.
|
|
37
37
|
disconnect Stop sharing from this Mac: removes the hooks, the ping-back and the add-on's settings.
|
|
38
38
|
preview Count this Mac's last 30 days and show what would be shared. Nothing is sent.
|
|
39
|
-
--days How many days back, 1 to
|
|
39
|
+
--days How many days back, 1 to 90.
|
|
40
40
|
--company An account whose repositories are your company's, to mark them. Repeat for more.
|
|
41
41
|
--json Print the exact rows instead of the summary.
|
|
42
42
|
`
|
|
@@ -131,7 +131,7 @@ async function connect(args) {
|
|
|
131
131
|
return fail(response.body?.error ?? `Couldn't connect: the server answered ${response.status}.`)
|
|
132
132
|
}
|
|
133
133
|
const { token, companyName, syncIntervalSeconds } = response.body
|
|
134
|
-
// However far back the survey asks for, the add-on reads
|
|
134
|
+
// However far back the survey asks for, the add-on reads 90 days at most (config.mjs).
|
|
135
135
|
const historyDays = daysToRead(response.body.historyDays)
|
|
136
136
|
// A new connection starts with nothing sent.
|
|
137
137
|
rmSync(paths().state, { force: true })
|
|
@@ -159,7 +159,7 @@ async function connect(args) {
|
|
|
159
159
|
const found = list.repositories.length
|
|
160
160
|
say(`Found ${found} ${found === 1 ? 'repository' : 'repositories'}${list.outsideGit ? ', and work outside a repository' : ''}, in the last ${historyDays} days.`)
|
|
161
161
|
say('Now choose on your survey page which to share. Nothing from your sessions is sent until you do.')
|
|
162
|
-
say(
|
|
162
|
+
say(`Then the last ${historyDays} days of what you chose are shared, and after that, your numbers after each session.`)
|
|
163
163
|
// Waits in the background, so this line returns at once in Claude Code or Codex too.
|
|
164
164
|
const waiter = spawn(process.execPath, [paths().appBin, 'sync', '--background', '--wait-for-choice'], {
|
|
165
165
|
detached: true,
|
|
@@ -259,7 +259,7 @@ async function preview(rest) {
|
|
|
259
259
|
if (option === '--json') json = true
|
|
260
260
|
else if (option === '--days') {
|
|
261
261
|
days = Number(rest[++index])
|
|
262
|
-
if (!Number.isInteger(days) || days < 1 || days >
|
|
262
|
+
if (!Number.isInteger(days) || days < 1 || days > MAX_HISTORY_DAYS) return fail(`--days needs a whole number from 1 to ${MAX_HISTORY_DAYS}.`)
|
|
263
263
|
} else if (option === '--company') {
|
|
264
264
|
const account = rest[++index]
|
|
265
265
|
if (!account) return fail('--company needs an account, like github.com/acme.')
|
package/src/config.mjs
CHANGED
|
@@ -7,12 +7,44 @@ import { join } from 'node:path'
|
|
|
7
7
|
* The add-on's own folder, `~/.ai-adoption-score`: the connection's settings and its device token. Only the person's
|
|
8
8
|
* own account can read it. `AI_ADOPTION_SCORE_HOME` points it elsewhere, for testing.
|
|
9
9
|
*
|
|
10
|
+
* `historyDays` is how many days back the server takes, and `historyDaysSent` how many of them went out in one sync:
|
|
11
|
+
* fewer, and the next sync reads them all once (`sync.mjs`).
|
|
12
|
+
*
|
|
10
13
|
* @typedef {{ server: string, token: string, companyName: string, connectedAt: string, syncIntervalSeconds: number,
|
|
11
|
-
* historyDays: number, lastSyncAt?: string, initialSyncDone?: boolean }} UsageConfig
|
|
14
|
+
* historyDays: number, historyDaysSent?: number, lastSyncAt?: string, initialSyncDone?: boolean }} UsageConfig
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
export const DEFAULT_SERVER = 'https://app.ai-adoption-score.app'
|
|
15
18
|
|
|
19
|
+
/** The add-on never reads further back than this, whatever the server asks for. */
|
|
20
|
+
export const MAX_HISTORY_DAYS = 90
|
|
21
|
+
/** What a server that names no number takes: every server before add-on 1.2 took 30 days. */
|
|
22
|
+
const DEFAULT_HISTORY_DAYS = 30
|
|
23
|
+
|
|
24
|
+
/** How many days to read: a whole number from 1 to 90, and 30 when `days` isn't one. @param {unknown} days */
|
|
25
|
+
export function daysToRead(days) {
|
|
26
|
+
const whole = Math.trunc(Number(days))
|
|
27
|
+
return Number.isFinite(whole) && whole >= 1 ? Math.min(whole, MAX_HISTORY_DAYS) : DEFAULT_HISTORY_DAYS
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A server's answer to a sync or a check-in that takes more days than this Mac's settings say (`historyDays`: 30 before,
|
|
32
|
+
* 90 since add-on 1.2): the settings take the new number, so the next sync sends the days in between once. Never fewer,
|
|
33
|
+
* so a Mac never sends a day its server would refuse. Returns the settings as they are now.
|
|
34
|
+
* @param {UsageConfig} config
|
|
35
|
+
* @param {unknown} answer
|
|
36
|
+
* @returns {UsageConfig}
|
|
37
|
+
*/
|
|
38
|
+
export function takeHistoryDays(config, answer, env = process.env) {
|
|
39
|
+
const named = answer && typeof answer === 'object' && 'historyDays' in answer ? answer.historyDays : undefined
|
|
40
|
+
if (named === undefined) return config
|
|
41
|
+
const offered = daysToRead(named)
|
|
42
|
+
if (offered <= (config.historyDays ?? 0) || !isCurrentConfig(config, env)) return config
|
|
43
|
+
const next = { ...(readConfig(env) ?? config), historyDays: offered }
|
|
44
|
+
writeConfig(next, env)
|
|
45
|
+
return next
|
|
46
|
+
}
|
|
47
|
+
|
|
16
48
|
export function configDir(env = process.env) {
|
|
17
49
|
return env.AI_ADOPTION_SCORE_HOME || join(homedir(), '.ai-adoption-score')
|
|
18
50
|
}
|
package/src/scan.mjs
CHANGED
|
@@ -4,22 +4,18 @@ import { readCodex } from './codex.mjs'
|
|
|
4
4
|
import { loadDesktopConnectorNames } from './connectors.mjs'
|
|
5
5
|
import { assignCommits, createGitInfo } from './git.mjs'
|
|
6
6
|
import { createRowSet, finishRow, hasUse, windowStart } from './rows.mjs'
|
|
7
|
+
import { daysToRead } from './config.mjs'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
export const MAX_HISTORY_DAYS = 30
|
|
10
|
-
|
|
11
|
-
/** How many days to read: a whole number from 1 to 30, and 30 when `days` isn't one. @param {unknown} days */
|
|
12
|
-
export function daysToRead(days) {
|
|
13
|
-
const whole = Math.trunc(Number(days))
|
|
14
|
-
return Number.isFinite(whole) && whole >= 1 ? Math.min(whole, MAX_HISTORY_DAYS) : MAX_HISTORY_DAYS
|
|
15
|
-
}
|
|
9
|
+
export { MAX_HISTORY_DAYS, daysToRead } from './config.mjs'
|
|
16
10
|
|
|
17
11
|
/**
|
|
18
|
-
* Reads this Mac's Claude Code and Codex records for the last `days` calendar days
|
|
19
|
-
* that would be sent, with how much of the records could be read. Nothing is written and nothing leaves
|
|
12
|
+
* Reads this Mac's Claude Code and Codex records for the last `days` calendar days (30 unless asked, 90 at most) and
|
|
13
|
+
* returns the rows that would be sent, with how much of the records could be read. Nothing is written and nothing leaves
|
|
14
|
+
* the Mac. Claude Code deletes its own records after 30 days unless its `cleanupPeriodDays` says otherwise, so older days
|
|
15
|
+
* hold what is left of them; Codex keeps its records.
|
|
20
16
|
* @param {{ days?: number, now?: number, env?: NodeJS.ProcessEnv }} [options]
|
|
21
17
|
*/
|
|
22
|
-
export async function scanUsage({ days =
|
|
18
|
+
export async function scanUsage({ days = 30, now = Date.now(), env = process.env } = {}) {
|
|
23
19
|
const since = windowStart(now, daysToRead(days))
|
|
24
20
|
const rows = createRowSet()
|
|
25
21
|
const gitInfo = createGitInfo()
|
package/src/sync.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { constants, setPriority } from 'node:os'
|
|
|
5
5
|
import { get, post, CLIENT_VERSION } from './api.mjs'
|
|
6
6
|
import { reportedHookState } from './checkin.mjs'
|
|
7
7
|
import { isShared, readChoice } from './choice.mjs'
|
|
8
|
-
import { isCurrentConfig, writeConfig } from './config.mjs'
|
|
8
|
+
import { isCurrentConfig, readConfig, takeHistoryDays, writeConfig } from './config.mjs'
|
|
9
9
|
import { hookState, uninstallAll } from './install.mjs'
|
|
10
10
|
import { log } from './log.mjs'
|
|
11
11
|
import { paths } from './paths.mjs'
|
|
@@ -18,6 +18,12 @@ const MAX_ROWS = 400
|
|
|
18
18
|
const MAX_BYTES = 200 * 1024
|
|
19
19
|
/** A sync that started this long ago and still holds the lock has died. */
|
|
20
20
|
const STALE_LOCK_MS = 15 * 60_000
|
|
21
|
+
/**
|
|
22
|
+
* The days every sync reads once all the days the server takes went out: the rows that can still change (today's, a
|
|
23
|
+
* branch pushed later, a commit made after a session) are in them, and reading more would only take longer (about 20
|
|
24
|
+
* seconds for 30 days on a busy Mac).
|
|
25
|
+
*/
|
|
26
|
+
export const SYNC_DAYS = 30
|
|
21
27
|
|
|
22
28
|
/** @param {import('./rows.mjs').UsageRow[]} rows */
|
|
23
29
|
export function batches(rows) {
|
|
@@ -136,7 +142,11 @@ export async function syncNow(config, { background = false, reason = 'manual', e
|
|
|
136
142
|
}
|
|
137
143
|
const { choice } = asked
|
|
138
144
|
|
|
139
|
-
|
|
145
|
+
// All the days the server takes go out in one sync: the first one, and once more when the server comes to take
|
|
146
|
+
// more than before (90 instead of 30 since add-on 1.2). Every other sync reads the last 30 days.
|
|
147
|
+
const wholeWindow = (config.historyDaysSent ?? 0) < config.historyDays
|
|
148
|
+
const days = wholeWindow ? config.historyDays : Math.min(config.historyDays, SYNC_DAYS)
|
|
149
|
+
const scan = await scanUsage({ days, env })
|
|
140
150
|
// Time by hour among the rows that go out only, so a session that stays on the Mac takes no share of it.
|
|
141
151
|
const included = timeByHour(scan.rows.filter((row) => sendsTool(row.tool, config, state, env) && isShared(choice, row.repository)))
|
|
142
152
|
const changed = included.filter((row) => state.sent[rowKey(row)] !== rowHash(row))
|
|
@@ -148,9 +158,12 @@ export async function syncNow(config, { background = false, reason = 'manual', e
|
|
|
148
158
|
const groups = changed.length > 0 ? batches(changed) : [[]]
|
|
149
159
|
let accepted = 0
|
|
150
160
|
let status = asked.status === 'revoked' || asked.status === 'closed' ? asked.status : 'active'
|
|
161
|
+
/** @type {unknown} */
|
|
162
|
+
let answer = null
|
|
151
163
|
for (const [index, rows] of status === 'active' ? groups.entries() : []) {
|
|
152
164
|
const body = { schemaVersion: 1, clientVersion: CLIENT_VERSION, rows, ...(index === 0 ? { checkIn } : {}) }
|
|
153
165
|
const response = await post(config.server, '/api/usage/sync', body, config.token)
|
|
166
|
+
answer = response.body
|
|
154
167
|
status = response.body?.status ?? (response.status === 401 ? 'revoked' : 'error')
|
|
155
168
|
if (response.status === 429 || status === 'paused') {
|
|
156
169
|
const wait = (response.retryAfterSeconds ?? response.body?.nextSyncAfterSeconds ?? 600) * 1000
|
|
@@ -183,9 +196,19 @@ export async function syncNow(config, { background = false, reason = 'manual', e
|
|
|
183
196
|
updateState((current) => {
|
|
184
197
|
current.sent = Object.fromEntries(Object.entries(current.sent).filter(([key]) => keep.has(key)))
|
|
185
198
|
}, env)
|
|
186
|
-
// The first sync is the one with the person's choice: it sends
|
|
187
|
-
|
|
188
|
-
|
|
199
|
+
// The first sync is the one with the person's choice: it sends every tool's days. Once all the days the server takes
|
|
200
|
+
// went out, the next syncs read the last 30.
|
|
201
|
+
if (choice && (!config.initialSyncDone || wholeWindow) && isCurrentConfig(config, env)) {
|
|
202
|
+
const current = readConfig(env) ?? config
|
|
203
|
+
writeConfig({
|
|
204
|
+
...current,
|
|
205
|
+
...(config.initialSyncDone ? {} : { initialSyncDone: true, lastSyncAt: new Date().toISOString() }),
|
|
206
|
+
...(wholeWindow ? { historyDaysSent: days } : {}),
|
|
207
|
+
}, env)
|
|
208
|
+
}
|
|
209
|
+
// A server that takes more days than these settings say: the next sync sends the days in between.
|
|
210
|
+
takeHistoryDays(config, answer, env)
|
|
211
|
+
log(`sync (${reason}): sent ${accepted} of ${included.length} rows${wholeWindow ? `, reading ${days} days` : ''}`, env)
|
|
189
212
|
} else {
|
|
190
213
|
log(`sync (${reason}): ${status}`, env)
|
|
191
214
|
}
|
|
@@ -205,9 +228,9 @@ export async function syncNow(config, { background = false, reason = 'manual', e
|
|
|
205
228
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
206
229
|
|
|
207
230
|
/**
|
|
208
|
-
* Right after connecting, in the background: waits while the person chooses on their survey page, then sends the
|
|
209
|
-
*
|
|
210
|
-
* choose sends them all the same.
|
|
231
|
+
* Right after connecting, in the background: waits while the person chooses on their survey page, then sends the days
|
|
232
|
+
* the server takes (90) of what they chose. It gives up when the list on their page expires (an hour); the next sync
|
|
233
|
+
* after they choose sends them all the same.
|
|
211
234
|
* @param {import('./config.mjs').UsageConfig} config
|
|
212
235
|
* @param {{ env?: NodeJS.ProcessEnv, timeoutMs?: number }} [options]
|
|
213
236
|
*/
|