claude-inspect 0.1.0 → 0.2.1
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 +2 -2
- package/dist/app/server/cli/demo.js +4 -0
- package/dist/app/server/cli/service.js +3 -1
- package/dist/app/server/history/search.js +2 -1
- package/dist/app/server/inspector.js +18 -2
- package/dist/app/server/model/analyze.js +24 -0
- package/dist/app/server/run.js +10 -4
- package/dist/app/server/sources/processes.js +70 -11
- package/dist/app/server/sources/projects.js +9 -4
- package/dist/app/server/sources/spawns.js +1 -1
- package/dist/web/assets/index-DhsFCY0I.js +14 -0
- package/dist/web/index.html +1 -1
- package/package.json +4 -3
- package/dist/web/assets/index-DdxYQpDc.js +0 -14
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://www.npmjs.com/package/claude-inspect)
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|

|
|
8
8
|

|
|
@@ -69,7 +69,7 @@ becomes hard to tell *who is doing what*. claude-inspect answers:
|
|
|
69
69
|
|
|
70
70
|
## Quick start
|
|
71
71
|
|
|
72
|
-
Requirements: Node.js ≥
|
|
72
|
+
Requirements: Node.js ≥ 22, macOS, Linux or Windows, Claude Code writing to `~/.claude`. On Windows, live process status is read via PowerShell; the autostart service and the live `demo` are macOS/Linux only (`demo --out <dir>` works everywhere).
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
75
|
npx claude-inspect --open # try it once
|
|
@@ -292,6 +292,10 @@ export async function runDemo(opts) {
|
|
|
292
292
|
console.log(`Demo data written to ${ROOT} (no processes started)`);
|
|
293
293
|
return;
|
|
294
294
|
}
|
|
295
|
+
if (process.platform === 'win32') {
|
|
296
|
+
console.error('The live demo starts Unix processes (sh, sleep, pgrep) and needs macOS or Linux. Use `demo --out <dir>` to only generate the data.');
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
295
299
|
// orchestrator = shell that forks the implementer (process tree: implementer → orchestrator)
|
|
296
300
|
const children = [
|
|
297
301
|
spawn('sh', ['-c', 'sleep 7200 & wait'], { stdio: 'ignore', detached: true }),
|
|
@@ -114,13 +114,15 @@ function macStatus() {
|
|
|
114
114
|
// ---------------------------------------------------------------------------
|
|
115
115
|
// Linux (systemd --user)
|
|
116
116
|
const unitPath = join(homedir(), '.config', 'systemd', 'user', 'claude-inspect.service');
|
|
117
|
+
/** systemd-Quoting für ExecStart (Pfade mit Leerzeichen, Anführungszeichen, Backslashes, %). */
|
|
118
|
+
const q = (s) => `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/%/g, '%%')}"`;
|
|
117
119
|
function linuxInstall() {
|
|
118
120
|
mkdirSync(dirname(unitPath), { recursive: true });
|
|
119
121
|
writeFileSync(unitPath, `[Unit]
|
|
120
122
|
Description=claude-inspect – live dashboard for Claude Code agents
|
|
121
123
|
|
|
122
124
|
[Service]
|
|
123
|
-
ExecStart=${nodePath()} ${ENTRY} --port ${port} --no-auth
|
|
125
|
+
ExecStart=${q(nodePath())} ${q(ENTRY)} --port ${port} --no-auth
|
|
124
126
|
WorkingDirectory=${ROOT}
|
|
125
127
|
Restart=on-failure
|
|
126
128
|
RestartSec=10
|
|
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { toolSummary } from '../../shared/tools.js';
|
|
3
3
|
import { paths } from '../config.js';
|
|
4
4
|
import { registry } from '../formats/index.js';
|
|
5
|
+
import { pathTail } from '../sources/projects.js';
|
|
5
6
|
function snippet(text, needle, radius = 90) {
|
|
6
7
|
const i = text.toLowerCase().indexOf(needle);
|
|
7
8
|
if (i < 0)
|
|
@@ -117,7 +118,7 @@ export async function search(query, files, knownSessions, limit = 200, budgetMs
|
|
|
117
118
|
continue;
|
|
118
119
|
hits.push({
|
|
119
120
|
sessionId: p.sessionId ?? '',
|
|
120
|
-
project: p.project
|
|
121
|
+
project: p.project ? pathTail(p.project) : '?',
|
|
121
122
|
title: 'transcript no longer exists',
|
|
122
123
|
line: i,
|
|
123
124
|
kind: 'history-prompt',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { watch } from 'node:fs';
|
|
3
3
|
import { stat } from 'node:fs/promises';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
5
|
import { CLAUDE_DIR, relToClaude } from './config.js';
|
|
6
6
|
import { registry } from './formats/index.js';
|
|
7
7
|
import { HistoryIndex } from './history/aggregator.js';
|
|
@@ -15,6 +15,17 @@ import { ProjectIndex } from './sources/projects.js';
|
|
|
15
15
|
import { SpawnIndex } from './sources/spawns.js';
|
|
16
16
|
import { agentCallIds, listSubagents } from './sources/subagents.js';
|
|
17
17
|
import { TranscriptStore } from './sources/transcripts.js';
|
|
18
|
+
/**
|
|
19
|
+
* IDE-Erweiterungen und SDK-Hosts (VS Code, JetBrains ACP …) halten claude-Prozesse am Leben,
|
|
20
|
+
* auch wenn das Chat-Panel längst zu ist. Ohne Transcript gab es darin nie ein Gespräch.
|
|
21
|
+
* Terminal-Sessions (entrypoint "cli") bleiben außen vor: Dort sitzt jemand vor dem Prompt.
|
|
22
|
+
*/
|
|
23
|
+
const IDE_GRACE_MS = 2 * 60_000;
|
|
24
|
+
function idleIdeProcess(p) {
|
|
25
|
+
if (p.status === 'busy' || !p.entrypoint || p.entrypoint === 'cli' || p.kind === 'bg')
|
|
26
|
+
return false;
|
|
27
|
+
return Date.now() - (p.startedAt ?? 0) > IDE_GRACE_MS;
|
|
28
|
+
}
|
|
18
29
|
/** Subagenten, deren Datei so lange unverändert ist, gelten ohne Abschluss als "stale". */
|
|
19
30
|
const SUBAGENT_STALE_MS = 10 * 60_000;
|
|
20
31
|
/** Subagenten erscheinen auf dem Dashboard, wenn sie so kürzlich aktiv waren. */
|
|
@@ -166,10 +177,14 @@ export class Inspector extends EventEmitter {
|
|
|
166
177
|
if (p.alive) {
|
|
167
178
|
try {
|
|
168
179
|
dp.session = await this.sessionLive(p.sessionId, p);
|
|
180
|
+
if (dp.session?.ended && p.status !== 'busy')
|
|
181
|
+
dp.inactive = dp.session.ended;
|
|
169
182
|
}
|
|
170
183
|
catch {
|
|
171
184
|
/* Transcript nicht lesbar */
|
|
172
185
|
}
|
|
186
|
+
if (!dp.session && !this.index.get(p.sessionId) && idleIdeProcess(p))
|
|
187
|
+
dp.inactive = { reason: 'no-conversation', at: p.updatedAt ? new Date(p.updatedAt).toISOString() : undefined };
|
|
173
188
|
}
|
|
174
189
|
processes.push(dp);
|
|
175
190
|
}
|
|
@@ -214,6 +229,7 @@ export class Inspector extends EventEmitter {
|
|
|
214
229
|
costUSD: a.costUSD,
|
|
215
230
|
lastPrompt: a.lastPrompt,
|
|
216
231
|
spawned: await this.spawnedSessions(sessionId),
|
|
232
|
+
ended: a.ended,
|
|
217
233
|
};
|
|
218
234
|
}
|
|
219
235
|
async spawnParent(sessionId) {
|
|
@@ -260,7 +276,7 @@ export class Inspector extends EventEmitter {
|
|
|
260
276
|
* Status und aktuelle Aktivität zu bestimmen (für Dashboard und Detailansicht).
|
|
261
277
|
*/
|
|
262
278
|
async resolveSubagents(sessionPath, files, load) {
|
|
263
|
-
const sessionId = sessionPath
|
|
279
|
+
const sessionId = basename(sessionPath, '.jsonl');
|
|
264
280
|
const out = [];
|
|
265
281
|
for (const sf of files) {
|
|
266
282
|
const info = {
|
|
@@ -9,6 +9,29 @@ function short(s, max = 140) {
|
|
|
9
9
|
.trim();
|
|
10
10
|
return one.length > max ? one.slice(0, max - 1) + '…' : one;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Wurde die Session nach der letzten echten Eingabe verlassen? Eine neue Eingabe danach hebt das auf.
|
|
14
|
+
* - `/exit`: bei Hintergrund-Sessions trennt das nur das Terminal, der Prozess läuft weiter
|
|
15
|
+
* - Remote Control meldet, dass die Session auf einem anderen Gerät beendet oder archiviert wurde
|
|
16
|
+
*/
|
|
17
|
+
function endedAfterLastPrompt(entries) {
|
|
18
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
19
|
+
const e = entries[i];
|
|
20
|
+
if (e.kind === 'user-text') {
|
|
21
|
+
if (/^<command-name>\/(exit|quit)<\/command-name>/.test(e.text))
|
|
22
|
+
return { reason: 'exit', at: e.timestamp };
|
|
23
|
+
if (!e.isMeta && e.text && !e.text.startsWith('<'))
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
else if (e.kind === 'system' && /ended or archived/i.test(e.text ?? '')) {
|
|
27
|
+
return { reason: 'archived', at: e.timestamp };
|
|
28
|
+
}
|
|
29
|
+
else if (e.kind === 'assistant-text' || e.kind === 'tool-use') {
|
|
30
|
+
return undefined; // danach wurde noch gearbeitet
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
12
35
|
export function analyze(entries) {
|
|
13
36
|
const a = { outputTokens: 0, toolCalls: 0, pendingTools: [], current: null, finished: false, errored: false };
|
|
14
37
|
const open = new Map();
|
|
@@ -68,6 +91,7 @@ export function analyze(entries) {
|
|
|
68
91
|
summary: toolSummary(t.name, t.input),
|
|
69
92
|
since: t.timestamp,
|
|
70
93
|
}));
|
|
94
|
+
a.ended = endedAfterLastPrompt(entries);
|
|
71
95
|
// Aktuelle Aktivität aus dem letzten bedeutsamen Eintrag
|
|
72
96
|
let last;
|
|
73
97
|
for (let i = entries.length - 1; i >= 0; i--) {
|
package/dist/app/server/run.js
CHANGED
|
@@ -11,6 +11,14 @@ function findWebRoot() {
|
|
|
11
11
|
const candidates = [join(here, '..', '..', 'dist', 'web'), join(here, '..', '..', 'web')];
|
|
12
12
|
return candidates.find((d) => existsSync(join(d, 'index.html'))) ?? candidates[0];
|
|
13
13
|
}
|
|
14
|
+
function openBrowser(url) {
|
|
15
|
+
const [bin, args] = process.platform === 'darwin'
|
|
16
|
+
? ['open', [url]]
|
|
17
|
+
: process.platform === 'win32'
|
|
18
|
+
? ['rundll32', ['url.dll,FileProtocolHandler', url]] // kein cmd/start: "&" in der URL wäre dort ein Befehlstrenner
|
|
19
|
+
: ['xdg-open', [url]];
|
|
20
|
+
execFile(bin, args, () => undefined).on('error', () => undefined);
|
|
21
|
+
}
|
|
14
22
|
export async function runServer(opts) {
|
|
15
23
|
const inspector = new Inspector();
|
|
16
24
|
const t0 = Date.now();
|
|
@@ -24,10 +32,8 @@ export async function runServer(opts) {
|
|
|
24
32
|
const url = noAuth ? `http://localhost:${opts.port}/` : `http://localhost:${opts.port}/?t=${token}`;
|
|
25
33
|
console.log(`claude-inspect is reading ${CLAUDE_DIR} (started in ${Date.now() - t0} ms)`);
|
|
26
34
|
console.log(opts.dev ? `API (dev, no token): ${url} – UI: npm run dev:web` : `Dashboard: ${url}`);
|
|
27
|
-
if (opts.open
|
|
28
|
-
|
|
29
|
-
else if (opts.open && process.platform === 'linux')
|
|
30
|
-
execFile('xdg-open', [url]);
|
|
35
|
+
if (opts.open)
|
|
36
|
+
openBrowser(url);
|
|
31
37
|
const shutdown = () => {
|
|
32
38
|
inspector.stop();
|
|
33
39
|
process.exit(0);
|
|
@@ -4,13 +4,14 @@ import { join } from 'node:path';
|
|
|
4
4
|
import { paths } from '../config.js';
|
|
5
5
|
import { registry } from '../formats/index.js';
|
|
6
6
|
import { readJson } from '../util/jsonl.js';
|
|
7
|
+
const norm = (s) => s.replace(/\s+/g, ' ').trim();
|
|
7
8
|
/**
|
|
8
9
|
* Startzeiten laufender Prozesse per `ps` (rein lesend).
|
|
9
10
|
* TZ=UTC, weil Claude Code `procStart` in UTC schreibt.
|
|
10
11
|
*/
|
|
11
12
|
function procStarts(pids) {
|
|
12
13
|
return new Promise((resolve) => {
|
|
13
|
-
if (pids.length === 0
|
|
14
|
+
if (pids.length === 0)
|
|
14
15
|
return resolve(new Map());
|
|
15
16
|
execFile('ps', ['-o', 'pid=,lstart=', '-p', pids.join(',')], { env: { ...process.env, TZ: 'UTC', LC_ALL: 'C' }, timeout: 5000 }, (_err, stdout) => {
|
|
16
17
|
// ps endet mit Code 1, wenn einzelne PIDs fehlen – die Ausgabe ist trotzdem gültig.
|
|
@@ -25,10 +26,8 @@ function procStarts(pids) {
|
|
|
25
26
|
});
|
|
26
27
|
}
|
|
27
28
|
/** Eltern-PID und Programmname aller Prozesse (`ps -A`), für die Zuordnung per Prozessbaum. */
|
|
28
|
-
function
|
|
29
|
+
function unixTable() {
|
|
29
30
|
return new Promise((resolve) => {
|
|
30
|
-
if (process.platform === 'win32')
|
|
31
|
-
return resolve(new Map());
|
|
32
31
|
execFile('ps', ['-A', '-o', 'pid=,ppid=,comm='], { timeout: 5000, maxBuffer: 8 * 1024 * 1024 }, (_err, stdout) => {
|
|
33
32
|
const out = new Map();
|
|
34
33
|
for (const line of String(stdout ?? '').split('\n')) {
|
|
@@ -40,13 +39,66 @@ function processTable() {
|
|
|
40
39
|
});
|
|
41
40
|
});
|
|
42
41
|
}
|
|
42
|
+
/** Windows: Prozesstabelle inkl. Startzeit per CIM (rein lesend). */
|
|
43
|
+
const WIN_PS = "Get-CimInstance Win32_Process | ForEach-Object { '{0} {1} {2} {3}' -f $_.ProcessId, $_.ParentProcessId, " +
|
|
44
|
+
"$(if ($_.CreationDate) { $_.CreationDate.ToUniversalTime().ToString('o') } else { '-' }), $_.Name }";
|
|
45
|
+
function windowsTable() {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', WIN_PS], { timeout: 10_000, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (_err, stdout) => {
|
|
48
|
+
const out = new Map();
|
|
49
|
+
for (const line of String(stdout ?? '').split(/\r?\n/)) {
|
|
50
|
+
const m = /^(\d+) (\d+) (\S+) (.*)$/.exec(line.trim());
|
|
51
|
+
if (m)
|
|
52
|
+
out.set(Number(m[1]), { ppid: Number(m[2]), start: m[3] === '-' ? undefined : m[3], comm: m[4].toLowerCase().replace(/\.exe$/, '') });
|
|
53
|
+
}
|
|
54
|
+
resolve(out);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// PowerShell zu starten kostet spürbar CPU – die Tabelle wird unter Windows kurz zwischengespeichert.
|
|
59
|
+
let winCache;
|
|
60
|
+
function cachedWindowsTable() {
|
|
61
|
+
if (!winCache || Date.now() - winCache.at > 5000)
|
|
62
|
+
winCache = { at: Date.now(), table: windowsTable() };
|
|
63
|
+
return winCache.table;
|
|
64
|
+
}
|
|
65
|
+
/** Startzeit als Epoch-ms. `ps lstart` ("Mon Sep 28 19:15:43 2026") hat keine Zone und ist UTC. */
|
|
66
|
+
function parseStart(s) {
|
|
67
|
+
if (s === undefined || s === '')
|
|
68
|
+
return undefined;
|
|
69
|
+
if (typeof s === 'number' || /^\d+$/.test(s))
|
|
70
|
+
return Number(s);
|
|
71
|
+
const zoned = /(?:Z|[+-]\d{2}:?\d{2}|UTC|GMT)$/.test(s.trim());
|
|
72
|
+
const t = Date.parse(zoned ? s : `${s} UTC`);
|
|
73
|
+
return Number.isNaN(t) ? undefined : t;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Läuft der Prozess noch, und ist es derselbe (PIDs werden wiederverwendet)?
|
|
77
|
+
* Exakter Textvergleich, wo das Format bekannt ist; sonst Vergleich als Zeitpunkt.
|
|
78
|
+
* Ist `procStart` unbekannt oder unlesbar, reicht die Existenz der PID.
|
|
79
|
+
*/
|
|
80
|
+
export function sameProcess(actual, expected) {
|
|
81
|
+
if (actual === undefined)
|
|
82
|
+
return false;
|
|
83
|
+
if (!expected || norm(actual) === norm(expected))
|
|
84
|
+
return true;
|
|
85
|
+
const a = parseStart(actual);
|
|
86
|
+
const e = parseStart(expected);
|
|
87
|
+
if (a !== undefined && e !== undefined)
|
|
88
|
+
return Math.abs(a - e) <= 2000;
|
|
89
|
+
// Unbekanntes Format: unter Windows (kein ps-Format) zählt die PID, sonst lieber "nicht aktiv".
|
|
90
|
+
return process.platform === 'win32';
|
|
91
|
+
}
|
|
43
92
|
/**
|
|
44
93
|
* Programme, die zwischen aufrufendem Agenten und gestartetem claude liegen dürfen
|
|
45
94
|
* (Bash-Tool → Shell → evtl. Wrapper → claude). Alles andere – etwa der Daemon,
|
|
46
95
|
* der Hintergrund-Jobs forkt – ist keine Aufrufbeziehung.
|
|
47
96
|
*/
|
|
48
|
-
const LAUNCH_INTERMEDIATES = new Set([
|
|
49
|
-
|
|
97
|
+
const LAUNCH_INTERMEDIATES = new Set([
|
|
98
|
+
'sh', 'zsh', 'bash', 'dash', 'fish', '-zsh', '-bash', 'env', 'caffeinate', 'timeout', 'gtimeout', 'nohup', 'time', 'script', 'xargs', 'stdbuf', 'nice', 'sudo',
|
|
99
|
+
// Windows (Namen ohne .exe)
|
|
100
|
+
'cmd', 'powershell', 'pwsh',
|
|
101
|
+
]);
|
|
50
102
|
export async function readProcesses() {
|
|
51
103
|
let names = [];
|
|
52
104
|
try {
|
|
@@ -72,12 +124,19 @@ export async function readProcesses() {
|
|
|
72
124
|
/* halb geschrieben oder gelöscht */
|
|
73
125
|
}
|
|
74
126
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
127
|
+
if (decoded.length === 0)
|
|
128
|
+
return decoded;
|
|
129
|
+
let running;
|
|
130
|
+
let table;
|
|
131
|
+
if (process.platform === 'win32') {
|
|
132
|
+
table = await cachedWindowsTable();
|
|
133
|
+
running = new Map([...table].map(([pid, r]) => [pid, r.start ?? '']));
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
[running, table] = await Promise.all([procStarts(decoded.map((p) => p.pid).filter((p) => p > 0)), unixTable()]);
|
|
80
137
|
}
|
|
138
|
+
for (const p of decoded)
|
|
139
|
+
p.alive = sameProcess(running.get(p.pid), procStart.get(p.pid));
|
|
81
140
|
// Prozessbaum hochlaufen (claude → zsh → claude …) bis zum nächsten bekannten Claude-Prozess.
|
|
82
141
|
const alivePids = new Set(decoded.filter((p) => p.alive).map((p) => p.pid));
|
|
83
142
|
for (const p of decoded) {
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { paths } from '../config.js';
|
|
3
|
+
import { paths, relToClaude } from '../config.js';
|
|
4
4
|
import { registry } from '../formats/index.js';
|
|
5
5
|
import { isObj, num, str } from '../formats/util.js';
|
|
6
6
|
import { readHeadTail } from '../util/jsonl.js';
|
|
7
7
|
import { compareVersions } from '../util/semver.js';
|
|
8
|
+
/** Letztes Segment eines Pfads – egal ob mit "/" (macOS/Linux) oder "\\" (Windows). */
|
|
9
|
+
export function pathTail(p) {
|
|
10
|
+
return p.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || p;
|
|
11
|
+
}
|
|
8
12
|
export function projectName(cwd, projectDir) {
|
|
9
13
|
if (cwd)
|
|
10
|
-
return cwd
|
|
11
|
-
|
|
14
|
+
return pathTail(cwd);
|
|
15
|
+
// kodiertes Home-Verzeichnis: -Users-<name>- (macOS), -home-<name>- (Linux), C--Users-<name>- (Windows)
|
|
16
|
+
return projectDir.replace(/^(?:[A-Za-z]-)?-(?:Users|home)-[^-]+-/, '');
|
|
12
17
|
}
|
|
13
18
|
/**
|
|
14
19
|
* Index über alle Session-Transcripts unter ~/.claude/projects.
|
|
@@ -54,7 +59,7 @@ export class ProjectIndex {
|
|
|
54
59
|
}
|
|
55
60
|
/** Aktualisiert einen einzelnen Eintrag nach einem Watch-Event. */
|
|
56
61
|
async touch(path) {
|
|
57
|
-
const m =
|
|
62
|
+
const m = /^projects\/([^/]+)\/([^/]+)\.jsonl$/.exec(relToClaude(path));
|
|
58
63
|
if (!m)
|
|
59
64
|
return;
|
|
60
65
|
try {
|
|
@@ -120,7 +120,7 @@ export function scoreSpawn(child, call) {
|
|
|
120
120
|
evidence.push('time (rough)');
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
if (child.cwd && call.cwd && child.cwd.replace(
|
|
123
|
+
if (child.cwd && call.cwd && child.cwd.replace(/[\\/]+$/, '') === call.cwd.replace(/[\\/]+$/, '')) {
|
|
124
124
|
score += 5;
|
|
125
125
|
evidence.push('directory');
|
|
126
126
|
}
|