litmus-cli 1.4.34 → 1.4.36
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 +28 -2
- package/dist/commands/connect.d.ts +43 -4
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +91 -11
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +9 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/ai-tracking.d.ts +5 -0
- package/dist/lib/ai-tracking.d.ts.map +1 -1
- package/dist/lib/ai-tracking.js +24 -9
- package/dist/lib/ai-tracking.js.map +1 -1
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +6 -5
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/backfill-tools.d.ts +37 -1
- package/dist/lib/backfill-tools.d.ts.map +1 -1
- package/dist/lib/backfill-tools.js +63 -12
- package/dist/lib/backfill-tools.js.map +1 -1
- package/dist/lib/capture-selftest.d.ts +7 -2
- package/dist/lib/capture-selftest.d.ts.map +1 -1
- package/dist/lib/capture-selftest.js +4 -4
- package/dist/lib/capture-selftest.js.map +1 -1
- package/dist/lib/cursor-restart.js +1 -1
- package/dist/lib/cursor-restart.js.map +1 -1
- package/dist/lib/detection.d.ts +75 -11
- package/dist/lib/detection.d.ts.map +1 -1
- package/dist/lib/detection.js +259 -33
- package/dist/lib/detection.js.map +1 -1
- package/dist/lib/editor-autosave.d.ts +30 -0
- package/dist/lib/editor-autosave.d.ts.map +1 -0
- package/dist/lib/editor-autosave.js +63 -0
- package/dist/lib/editor-autosave.js.map +1 -0
- package/dist/lib/hook-logger.cjs +57 -60
- package/dist/lib/platform.d.ts +19 -6
- package/dist/lib/platform.d.ts.map +1 -1
- package/dist/lib/platform.js +40 -9
- package/dist/lib/platform.js.map +1 -1
- package/dist/lib/watcher.js +4 -41
- package/dist/lib/watcher.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
/**
|
|
4
|
+
* ENG-2543: turn on autosave for VS Code and Cursor in the assessment folder.
|
|
5
|
+
*
|
|
6
|
+
* Every capture path reads the disk: the watcher's change events, the periodic
|
|
7
|
+
* snapshot, and the zip `submit` builds. An unsaved editor buffer is on none of
|
|
8
|
+
* them. Both editors ship with autosave off, and the notebook editor lets a
|
|
9
|
+
* candidate write and run cells, outputs and all, without ever writing the
|
|
10
|
+
* .ipynb. At the deadline the auto-submit zips what is on disk and deletes the
|
|
11
|
+
* folder, so a candidate who never pressed Cmd+S is graded on their last save.
|
|
12
|
+
* On the 9/22 Mercor Benchmark cohort that was 53 minutes of work.
|
|
13
|
+
*
|
|
14
|
+
* A workspace `.vscode/settings.json` is read by both editors and overrides the
|
|
15
|
+
* candidate's user setting for this folder only.
|
|
16
|
+
*
|
|
17
|
+
* ADDS, NEVER REPLACES. A starter that ships its own settings keeps every key,
|
|
18
|
+
* and a key the author set on purpose (`files.autoSave` included) wins. A file
|
|
19
|
+
* that does not parse as plain JSON (VS Code allows comments) is left alone:
|
|
20
|
+
* rewriting it would drop the comments, and autosave is not worth that.
|
|
21
|
+
*
|
|
22
|
+
* Runs before `init` commits the baseline, so the file is part of the starting
|
|
23
|
+
* state and never shows up in the candidate's diff. The watcher and snapshot
|
|
24
|
+
* already ignore dotfiles, so it is never a signal either.
|
|
25
|
+
*/
|
|
26
|
+
export const AUTOSAVE_SETTINGS = {
|
|
27
|
+
"files.autoSave": "afterDelay",
|
|
28
|
+
"files.autoSaveDelay": 1000,
|
|
29
|
+
};
|
|
30
|
+
export function ensureEditorAutosave(targetDir) {
|
|
31
|
+
const dir = path.join(targetDir, ".vscode");
|
|
32
|
+
const file = path.join(dir, "settings.json");
|
|
33
|
+
try {
|
|
34
|
+
if (!existsSync(file)) {
|
|
35
|
+
mkdirSync(dir, { recursive: true });
|
|
36
|
+
writeFileSync(file, JSON.stringify(AUTOSAVE_SETTINGS, null, 2) + "\n");
|
|
37
|
+
return "created";
|
|
38
|
+
}
|
|
39
|
+
let existing;
|
|
40
|
+
try {
|
|
41
|
+
existing = JSON.parse(readFileSync(file, "utf8"));
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return "skipped";
|
|
45
|
+
}
|
|
46
|
+
if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
|
|
47
|
+
return "skipped";
|
|
48
|
+
}
|
|
49
|
+
const settings = existing;
|
|
50
|
+
if ("files.autoSave" in settings)
|
|
51
|
+
return "already-set";
|
|
52
|
+
// The author's keys spread last, so a delay they already chose survives.
|
|
53
|
+
const merged = { ...AUTOSAVE_SETTINGS, ...settings };
|
|
54
|
+
writeFileSync(file, JSON.stringify(merged, null, 2) + "\n");
|
|
55
|
+
return "merged";
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Best-effort, like the kernel binding: an editor setting is never a
|
|
59
|
+
// reason to fail a start.
|
|
60
|
+
return "skipped";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=editor-autosave.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor-autosave.js","sourceRoot":"","sources":["../../src/lib/editor-autosave.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AACvE,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,gBAAgB,EAAE,YAAY;IAC9B,qBAAqB,EAAE,IAAI;CACnB,CAAA;AAIV,MAAM,UAAU,oBAAoB,CAAC,SAAiB;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAA;IAE5C,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YACnC,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,iBAAiB,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;YACtE,OAAO,SAAS,CAAA;QAClB,CAAC;QAED,IAAI,QAAiB,CAAA;QACrB,IAAI,CAAC;YACH,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAA;QACnD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACjF,OAAO,SAAS,CAAA;QAClB,CAAC;QAED,MAAM,QAAQ,GAAG,QAAmC,CAAA;QACpD,IAAI,gBAAgB,IAAI,QAAQ;YAAE,OAAO,aAAa,CAAA;QAEtD,yEAAyE;QACzE,MAAM,MAAM,GAAG,EAAE,GAAG,iBAAiB,EAAE,GAAG,QAAQ,EAAE,CAAA;QACpD,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAA;QAC3D,OAAO,QAAQ,CAAA;IACjB,CAAC;IAAC,MAAM,CAAC;QACP,qEAAqE;QACrE,0BAA0B;QAC1B,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC"}
|
package/dist/lib/hook-logger.cjs
CHANGED
|
@@ -123,9 +123,7 @@ function isAtOrUnder(child, parent) {
|
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
125
|
* Does an explicitly named assessment agree with where the session reports it
|
|
126
|
-
* is? Agreement means
|
|
127
|
-
* that assessment, or the assessment sits under the session's directory (the
|
|
128
|
-
* editor-opened-at-a-parent shape, ENG-771).
|
|
126
|
+
* is? Agreement means the session sits inside that assessment.
|
|
129
127
|
*
|
|
130
128
|
* `sessionDirs` are locations the TOOL reported (workspace roots, payload cwd)
|
|
131
129
|
* — deliberately not this process's cwd. A hook runner's cwd is not the
|
|
@@ -135,21 +133,13 @@ function isAtOrUnder(child, parent) {
|
|
|
135
133
|
* the silent Copilot Chat loss ENG-1292 fixed. When the tool reports nothing —
|
|
136
134
|
* exactly the Copilot Chat case the argument exists for — the argument stands.
|
|
137
135
|
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* copied into a sibling project under that
|
|
141
|
-
* original assessment.
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* parent of the assessment" is ENG-771, the reason resolution looks
|
|
146
|
-
* downward at all; when the session reports only the parent, nothing in the
|
|
147
|
-
* payload says which child the prompt came from.
|
|
148
|
-
* - Rejecting here changes nothing. Verified: with the assessment registered
|
|
149
|
-
* (which `litmus init` always does), dropping the explicit path just falls
|
|
150
|
-
* through to the registry, which returns that same assessment. The only
|
|
151
|
-
* candidates it would affect are project-scope ones, who have no registry
|
|
152
|
-
* entry — so tightening this trades zero privacy gain for real capture loss.
|
|
136
|
+
* A session reporting a PARENT of the named assessment does not agree
|
|
137
|
+
* (ENG-953). It used to, as the "editor opened at a parent" shape (ENG-771),
|
|
138
|
+
* which also let a config copied into a sibling project under that parent file
|
|
139
|
+
* its prompts against the original assessment. Nothing in such a payload says
|
|
140
|
+
* the prompt came from inside the assessment, and the registry fallback no
|
|
141
|
+
* longer claims parent-rooted sessions either, so neither route records work
|
|
142
|
+
* from outside the assessment folder.
|
|
153
143
|
*/
|
|
154
144
|
function explicitAgreesWithSession(assessmentDir, sessionDirs) {
|
|
155
145
|
const target = realpathOrNull(assessmentDir)
|
|
@@ -161,7 +151,7 @@ function explicitAgreesWithSession(assessmentDir, sessionDirs) {
|
|
|
161
151
|
if (real && !roots.includes(real)) roots.push(real)
|
|
162
152
|
}
|
|
163
153
|
if (roots.length === 0) return true // tool reported no location — the blind case
|
|
164
|
-
return roots.some(root => isAtOrUnder(root, target)
|
|
154
|
+
return roots.some(root => isAtOrUnder(root, target))
|
|
165
155
|
}
|
|
166
156
|
|
|
167
157
|
// A directory only counts as an assessment if it has a config.json — the
|
|
@@ -229,21 +219,29 @@ function walkUpForLitmusDir(startDir) {
|
|
|
229
219
|
}
|
|
230
220
|
|
|
231
221
|
/**
|
|
232
|
-
*
|
|
222
|
+
* The registered assessment whose tree holds `location`, as a project dir, or
|
|
223
|
+
* null. When registered assessments nest, the innermost one wins: it is the
|
|
224
|
+
* assessment the session is actually in.
|
|
233
225
|
*
|
|
234
226
|
* Reuses ENG-1311's `realpathOrNull` + `isAtOrUnder` rather than carrying a
|
|
235
227
|
* second path comparison: realpath is what makes this work at all, since the
|
|
236
228
|
* registry stores the string `litmus init` was given while payloads report the
|
|
237
229
|
* tool's own cwd, and on macOS the same directory routinely differs in form
|
|
238
|
-
* (/tmp vs /private/tmp, symlinked homes).
|
|
239
|
-
* "inside itself" for the purpose of picking a registry entry — an exact match
|
|
240
|
-
* is already handled by the walk-up above.
|
|
230
|
+
* (/tmp vs /private/tmp, symlinked homes).
|
|
241
231
|
*/
|
|
242
|
-
function
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
232
|
+
function registeredAssessmentHolding(location, registered) {
|
|
233
|
+
const loc = realpathOrNull(location)
|
|
234
|
+
if (!loc) return null
|
|
235
|
+
let best = null
|
|
236
|
+
let bestReal = ""
|
|
237
|
+
for (const p of registered) {
|
|
238
|
+
const real = realpathOrNull(p)
|
|
239
|
+
if (real && isAtOrUnder(loc, real) && real.length > bestReal.length) {
|
|
240
|
+
best = p
|
|
241
|
+
bestReal = real
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return best
|
|
247
245
|
}
|
|
248
246
|
|
|
249
247
|
// Find the .litmus directory — walk up from any workspace roots the tool told
|
|
@@ -252,24 +250,29 @@ function isStrictlyInside(parent, child) {
|
|
|
252
250
|
// every tool runs the hook from the workspace (Cursor invokes hooks with
|
|
253
251
|
// cwd = its own config dir and passes the workspace in `workspace_roots`).
|
|
254
252
|
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
253
|
+
// The walk-ups are never gated: a session placed inside an assessment is
|
|
254
|
+
// captured whatever the registry says.
|
|
255
|
+
//
|
|
256
|
+
// The registry fallback is SCOPED (ENG-953). It answers only for a session
|
|
257
|
+
// that is inside a registered assessment's tree, and names that assessment.
|
|
258
|
+
// That is the case walk-up misses while the assessment is live: the in-repo
|
|
259
|
+
// config was deleted mid-session and the shadow mirror carries identity
|
|
260
|
+
// (ENG-1453), or the session sits deeper than the walk-up's ten levels.
|
|
261
|
+
//
|
|
262
|
+
// It used to answer for anything. With no match it returned the first
|
|
263
|
+
// registered assessment, so a prompt typed in an unrelated project on the same
|
|
264
|
+
// machine was written into the assessment and uploaded, and a later version
|
|
265
|
+
// also claimed any session rooted at a PARENT of an assessment ($HOME
|
|
266
|
+
// included). Both captured work outside the assessment folder, which the
|
|
267
|
+
// candidate disclosure says we do not record. A session placed nowhere now
|
|
268
|
+
// captures nothing.
|
|
264
269
|
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
for (const hint of hintDirs) {
|
|
272
|
-
if (typeof hint !== "string" || !hint) continue
|
|
270
|
+
// Where the session is: the tool's reported locations (`hintDirs`), or this
|
|
271
|
+
// process's cwd when the tool reported none, since a hook runner's cwd is only
|
|
272
|
+
// a stand-in for the session's.
|
|
273
|
+
function findLitmusDir(hintDirs = []) {
|
|
274
|
+
const reported = hintDirs.filter(d => typeof d === "string" && d)
|
|
275
|
+
for (const hint of reported) {
|
|
273
276
|
const found = walkUpForLitmusDir(hint)
|
|
274
277
|
if (found) return found
|
|
275
278
|
}
|
|
@@ -282,7 +285,7 @@ function findLitmusDir(hintDirs = [], sessionDirs = []) {
|
|
|
282
285
|
const list = JSON.parse(fs.readFileSync(registry, "utf8"))
|
|
283
286
|
if (!Array.isArray(list)) return null
|
|
284
287
|
const valid = list.filter(
|
|
285
|
-
p => typeof p === "string" && (
|
|
288
|
+
p => typeof p === "string" && p && (
|
|
286
289
|
isAssessmentLitmusDir(path.join(p, ".litmus"))
|
|
287
290
|
// ENG-1453: config wiped but the assessment is alive — the dir still
|
|
288
291
|
// exists and its shadow mirror supplies identity + credentials.
|
|
@@ -291,15 +294,11 @@ function findLitmusDir(hintDirs = [], sessionDirs = []) {
|
|
|
291
294
|
)
|
|
292
295
|
if (!valid.length) return null
|
|
293
296
|
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
if (contained.length) return path.join(contained[contained.length - 1], ".litmus")
|
|
297
|
+
const locations = reported.length ? reported : [process.cwd()]
|
|
298
|
+
for (const location of locations) {
|
|
299
|
+
const holder = registeredAssessmentHolding(location, valid)
|
|
300
|
+
if (holder) return path.join(holder, ".litmus")
|
|
299
301
|
}
|
|
300
|
-
// Nothing placeable — keep the long-standing behavior rather than dropping
|
|
301
|
-
// the event, so a tool that reports no location at all still captures.
|
|
302
|
-
return path.join(valid[0], ".litmus")
|
|
303
302
|
} catch { /* no registry — give up silently */ }
|
|
304
303
|
return null
|
|
305
304
|
}
|
|
@@ -1676,8 +1675,8 @@ function claimPrompt(generationId) {
|
|
|
1676
1675
|
* Where the TOOL says this session is: workspace roots (Cursor) or the
|
|
1677
1676
|
* payload's own cwd (Claude Code, Codex, Copilot CLI). VS Code Copilot Chat
|
|
1678
1677
|
* reports neither. Two uses: checking the explicit path (ENG-1311), and
|
|
1679
|
-
*
|
|
1680
|
-
* findLitmusDir).
|
|
1678
|
+
* deciding which registered assessment, if any, holds the session when
|
|
1679
|
+
* walk-up finds nothing (ENG-953, see findLitmusDir).
|
|
1681
1680
|
*
|
|
1682
1681
|
* Explicit hint first: it is correct by construction — but only once it
|
|
1683
1682
|
* agrees with the session's own reported location, since the hook config
|
|
@@ -1687,9 +1686,7 @@ function claimPrompt(generationId) {
|
|
|
1687
1686
|
* Those merits include every location the tool reported, cwd included. The
|
|
1688
1687
|
* reported cwd beats this process's cwd (a hook runner's need not be the
|
|
1689
1688
|
* session's) and is the only pointer left once a copied explicit path has
|
|
1690
|
-
* been rejected
|
|
1691
|
-
* workspace roots, would fall through to the registry and be filed against
|
|
1692
|
-
* an unrelated assessment.
|
|
1689
|
+
* been rejected.
|
|
1693
1690
|
*/
|
|
1694
1691
|
function resolveAssessmentDir(parsed, workspaceRoots) {
|
|
1695
1692
|
const sessionDirs = [
|
|
@@ -1701,7 +1698,7 @@ function resolveAssessmentDir(parsed, workspaceRoots) {
|
|
|
1701
1698
|
const hintDirs = trustExplicit
|
|
1702
1699
|
? [explicitAssessmentDir, ...sessionDirs]
|
|
1703
1700
|
: sessionDirs
|
|
1704
|
-
return findLitmusDir(hintDirs
|
|
1701
|
+
return findLitmusDir(hintDirs)
|
|
1705
1702
|
}
|
|
1706
1703
|
|
|
1707
1704
|
async function main() {
|
package/dist/lib/platform.d.ts
CHANGED
|
@@ -10,15 +10,28 @@
|
|
|
10
10
|
* a no-op on Unix; tests/unit/windows-hide.test.ts enforces it.
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
|
-
* Returns lowercase text listing
|
|
14
|
-
*
|
|
13
|
+
* Returns lowercase text listing running processes, one per line.
|
|
14
|
+
* Tool detection parses it into rows and keeps the watcher's own user's
|
|
15
|
+
* (detection.ts detectTools); never match a word against the whole text.
|
|
15
16
|
*
|
|
16
|
-
* - macOS/Linux: `ps aux` (full command lines)
|
|
17
|
-
* - Windows: `tasklist` (image names like Code.exe,
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* - macOS/Linux: `ps aux` (full command lines, every user's)
|
|
18
|
+
* - Windows: `tasklist /fo csv /nh` (full image names like Code.exe,
|
|
19
|
+
* Cursor.exe; the default table truncates them at 25 characters), asked for
|
|
20
|
+
* the current user's processes only. tasklist rows carry a Session#, not
|
|
21
|
+
* an account, and another account can run in the candidate's session
|
|
22
|
+
* (`runas`), so the user filter has to happen here, and it never widens:
|
|
23
|
+
* see ownTasklist.
|
|
20
24
|
*/
|
|
21
25
|
export declare function getProcessList(): string;
|
|
26
|
+
/**
|
|
27
|
+
* The first user-filtered tasklist that contains this very process (`pid`),
|
|
28
|
+
* trying each spelling of the account in turn (DOMAIN\user, then user).
|
|
29
|
+
* When none does, the filter did not match how this machine names the
|
|
30
|
+
* account, and ownership cannot be established: the result is empty, so
|
|
31
|
+
* nothing counts. It never falls back to an unfiltered or session-scoped
|
|
32
|
+
* list, which would badge another account's processes.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ownTasklist(run: (account: string) => string, accounts: string[], pid: number): string;
|
|
22
35
|
/**
|
|
23
36
|
* Lists active network connections asynchronously.
|
|
24
37
|
* Calls cb(err, stdout) with raw connection output.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAavC;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAQrG;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAarF;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAqBtE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAkBrF"}
|
package/dist/lib/platform.js
CHANGED
|
@@ -9,21 +9,52 @@
|
|
|
9
9
|
* see terminal windows flash on every detection cycle (ENG-2010). The flag is
|
|
10
10
|
* a no-op on Unix; tests/unit/windows-hide.test.ts enforces it.
|
|
11
11
|
*/
|
|
12
|
-
import { execSync, execFile } from "child_process";
|
|
12
|
+
import { execSync, execFile, execFileSync } from "child_process";
|
|
13
|
+
import { userInfo } from "os";
|
|
13
14
|
const isWin = process.platform === "win32";
|
|
14
15
|
const isLinux = process.platform === "linux";
|
|
15
16
|
/**
|
|
16
|
-
* Returns lowercase text listing
|
|
17
|
-
*
|
|
17
|
+
* Returns lowercase text listing running processes, one per line.
|
|
18
|
+
* Tool detection parses it into rows and keeps the watcher's own user's
|
|
19
|
+
* (detection.ts detectTools); never match a word against the whole text.
|
|
18
20
|
*
|
|
19
|
-
* - macOS/Linux: `ps aux` (full command lines)
|
|
20
|
-
* - Windows: `tasklist` (image names like Code.exe,
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* - macOS/Linux: `ps aux` (full command lines, every user's)
|
|
22
|
+
* - Windows: `tasklist /fo csv /nh` (full image names like Code.exe,
|
|
23
|
+
* Cursor.exe; the default table truncates them at 25 characters), asked for
|
|
24
|
+
* the current user's processes only. tasklist rows carry a Session#, not
|
|
25
|
+
* an account, and another account can run in the candidate's session
|
|
26
|
+
* (`runas`), so the user filter has to happen here, and it never widens:
|
|
27
|
+
* see ownTasklist.
|
|
23
28
|
*/
|
|
24
29
|
export function getProcessList() {
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
if (!isWin)
|
|
31
|
+
return execSync("ps aux", { timeout: 5000, encoding: "utf8", windowsHide: true }).toLowerCase();
|
|
32
|
+
const user = userInfo().username;
|
|
33
|
+
const domain = process.env.USERDOMAIN;
|
|
34
|
+
return ownTasklist((account) => execFileSync("tasklist", ["/fo", "csv", "/nh", "/fi", `USERNAME eq ${account}`], {
|
|
35
|
+
timeout: 5000,
|
|
36
|
+
encoding: "utf8",
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
}), domain ? [`${domain}\\${user}`, user] : [user], process.pid);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The first user-filtered tasklist that contains this very process (`pid`),
|
|
42
|
+
* trying each spelling of the account in turn (DOMAIN\user, then user).
|
|
43
|
+
* When none does, the filter did not match how this machine names the
|
|
44
|
+
* account, and ownership cannot be established: the result is empty, so
|
|
45
|
+
* nothing counts. It never falls back to an unfiltered or session-scoped
|
|
46
|
+
* list, which would badge another account's processes.
|
|
47
|
+
*/
|
|
48
|
+
export function ownTasklist(run, accounts, pid) {
|
|
49
|
+
for (const account of accounts) {
|
|
50
|
+
try {
|
|
51
|
+
const out = run(account).toLowerCase();
|
|
52
|
+
if (out.includes(`","${pid}","`))
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
catch { /* try the next spelling */ }
|
|
56
|
+
}
|
|
57
|
+
return "";
|
|
27
58
|
}
|
|
28
59
|
/**
|
|
29
60
|
* Lists active network connections asynchronously.
|
package/dist/lib/platform.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"platform.js","sourceRoot":"","sources":["../../src/lib/platform.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/watcher.js
CHANGED
|
@@ -36,7 +36,7 @@ import { setupAiTracking } from "../commands/init.js";
|
|
|
36
36
|
import { writeShadowChainCursor, readShadowChainCursor, writeShadowTrackerFile, writeShadowReviveInfo, readShadowConfigById, findShadowConfigForDir, deleteShadowForAssessment, writeRepairFile, removeRepairFile, REPAIR_FILE_NAME, } from "./shadow.js";
|
|
37
37
|
import { createKeepAliveGuard } from "./keepalive.js";
|
|
38
38
|
import { getProcessList, getConnections, extractConnection, resolvePidNames } from "./platform.js";
|
|
39
|
-
import {
|
|
39
|
+
import { detectTools, refineClaudeTool } from "./detection.js";
|
|
40
40
|
import { decideNudge, loadNudgeState, saveNudgeState, initialPromptCursor, scanPromptCount, writeAiNoticeFile, removeAiNoticeFile, nudgeNotificationCopy, isCliNoticeArtifact, AI_NOTICE_FILE_NAME, } from "./capture-nudge.js";
|
|
41
41
|
import { desktopNotify } from "./notify.js";
|
|
42
42
|
// ENG-1491: an uncaught throw no longer always kills the watcher — on a
|
|
@@ -999,34 +999,6 @@ function checkGitCommits() {
|
|
|
999
999
|
catch { /* git not available or command failed — skip */ }
|
|
1000
1000
|
}
|
|
1001
1001
|
setInterval(checkGitCommits, 30 * 1000); // Every 30 seconds
|
|
1002
|
-
// ── Editor & AI tool detection ───────────────────────────────────
|
|
1003
|
-
const EDITOR_PATTERNS = {
|
|
1004
|
-
vscode: /\bcode\b/,
|
|
1005
|
-
cursor: /\bcursor\b/,
|
|
1006
|
-
vim: /\b[gn]?vim\b/,
|
|
1007
|
-
intellij: /\bidea\b/,
|
|
1008
|
-
webstorm: /\bwebstorm\b/,
|
|
1009
|
-
sublime: /\bsubl\b/,
|
|
1010
|
-
emacs: /\bemacs\b/,
|
|
1011
|
-
zed: /\bzed\b/,
|
|
1012
|
-
};
|
|
1013
|
-
const AI_TOOL_PATTERNS = {
|
|
1014
|
-
aider: /\baider\b/,
|
|
1015
|
-
codeium: /\bcodeium\b/,
|
|
1016
|
-
codex: /\bcodex\b/,
|
|
1017
|
-
copilot: /\bcopilot\b/,
|
|
1018
|
-
gemini: /\bgemini\b/,
|
|
1019
|
-
windsurf: /\bwindsurf\b/,
|
|
1020
|
-
ollama: /\bollama\b/,
|
|
1021
|
-
tabnine: /\btabnine\b/,
|
|
1022
|
-
supermaven: /\bsupermaven\b/,
|
|
1023
|
-
cline: /\bcline\b/,
|
|
1024
|
-
cody: /\bsourcegraph-cody\b|\bcody-agent\b/,
|
|
1025
|
-
"amazon-q": /\bamazon-q\b/,
|
|
1026
|
-
"roo-code": /\broo-code\b|\broo_code\b/,
|
|
1027
|
-
pearai: /\bpearai\b/,
|
|
1028
|
-
ghostwriter: /\bghostwriter\b/,
|
|
1029
|
-
};
|
|
1030
1002
|
// ── Capture nudge (ENG-1857) ─────────────────────────────────────
|
|
1031
1003
|
// A hook-capturable AI tool running with ZERO captured prompts is the silent
|
|
1032
1004
|
// Codex/Copilot trust-gate signature: the candidate's AI work is invisibly
|
|
@@ -1091,18 +1063,9 @@ function runCaptureNudge(aiTools) {
|
|
|
1091
1063
|
}
|
|
1092
1064
|
function detectEnvironment() {
|
|
1093
1065
|
try {
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
.map(([name]) => name);
|
|
1098
|
-
const aiTools = Object.entries(AI_TOOL_PATTERNS)
|
|
1099
|
-
.filter(([, re]) => re.test(ps))
|
|
1100
|
-
.map(([name]) => name);
|
|
1101
|
-
aiTools.push(...classifyClaudeProcesses(ps));
|
|
1102
|
-
// Cursor is both an editor and an AI tool
|
|
1103
|
-
if (editors.includes("cursor") && !aiTools.includes("cursor")) {
|
|
1104
|
-
aiTools.push("cursor");
|
|
1105
|
-
}
|
|
1066
|
+
// Per process row, own user only, matched on the program — see
|
|
1067
|
+
// detectTools (ENG-1273).
|
|
1068
|
+
const { editors, aiTools } = detectTools(getProcessList(), process.platform, process.pid);
|
|
1106
1069
|
if (editors.length > 0 || aiTools.length > 0) {
|
|
1107
1070
|
const event = {
|
|
1108
1071
|
ts: new Date().toISOString(),
|