@adhdev/daemon-core 0.9.82-rc.313 → 0.9.82-rc.315
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/dist/commands/upgrade-helper.d.ts +12 -0
- package/dist/index.js +116 -37
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +116 -37
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/commands/upgrade-helper.ts +52 -30
- package/src/providers/spec/native-history-executor.ts +104 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.315",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.315",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -321,42 +321,64 @@ function removeDaemonPidFile(): void {
|
|
|
321
321
|
}
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Best-effort removal of a leftover npm staging entry.
|
|
326
|
+
*
|
|
327
|
+
* A stale staging dir can hold a locked native binary — e.g. `ghostty-vt.dll`
|
|
328
|
+
* from `@adhdev/ghostty-vt-node` still mapped by a lingering session-host
|
|
329
|
+
* process — which makes `rmSync` throw `EPERM` on Windows. Staging cleanup is
|
|
330
|
+
* only housekeeping: the leftover is inert and npm creates its own fresh
|
|
331
|
+
* staging dir for the real install, so a lock on an old leftover must NOT abort
|
|
332
|
+
* the upgrade. Log and continue instead of letting the error propagate.
|
|
333
|
+
*/
|
|
334
|
+
export function safeRemoveStaleEntry(target: string, label: string): void {
|
|
335
|
+
try {
|
|
336
|
+
fs.rmSync(target, { recursive: true, force: true });
|
|
337
|
+
appendUpgradeLog(`${label}: ${target}`);
|
|
338
|
+
} catch (error: any) {
|
|
339
|
+
appendUpgradeLog(`Skipped locked stale entry (${error?.code || 'error'}): ${target} — ${error?.message || String(error)}`);
|
|
335
340
|
}
|
|
341
|
+
}
|
|
336
342
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
343
|
+
export function cleanupStaleGlobalInstallDirs(pkgName: string, surface: CurrentGlobalInstallSurface): void {
|
|
344
|
+
// The whole routine is housekeeping — never let it throw out and abort the
|
|
345
|
+
// upgrade (npm root/prefix probing or readdir can fail for unrelated reasons).
|
|
346
|
+
try {
|
|
347
|
+
const prefixArgs = surface.installPrefix ? ['--prefix', surface.installPrefix] : [];
|
|
348
|
+
const npmRoot = String(execNpmCommandSync(['root', '-g', ...prefixArgs], { encoding: 'utf8' }, surface)).trim();
|
|
349
|
+
if (!npmRoot) return;
|
|
350
|
+
const npmPrefix = surface.installPrefix
|
|
351
|
+
|| String(execNpmCommandSync(['prefix', '-g', ...prefixArgs], { encoding: 'utf8' }, surface)).trim();
|
|
352
|
+
const binDir = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
|
|
353
|
+
const packageBaseName = pkgName.startsWith('@') ? pkgName.split('/')[1] : pkgName;
|
|
354
|
+
const binNames = new Set<string>([packageBaseName]);
|
|
355
|
+
if (pkgName === '@adhdev/daemon-standalone') {
|
|
356
|
+
binNames.add('adhdev-standalone');
|
|
345
357
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
358
|
+
|
|
359
|
+
if (pkgName.startsWith('@')) {
|
|
360
|
+
const [scope, name] = pkgName.split('/');
|
|
361
|
+
const scopeDir = path.join(npmRoot, scope);
|
|
362
|
+
if (!fs.existsSync(scopeDir)) return;
|
|
363
|
+
for (const entry of fs.readdirSync(scopeDir)) {
|
|
364
|
+
if (!entry.startsWith(`.${name}-`)) continue;
|
|
365
|
+
safeRemoveStaleEntry(path.join(scopeDir, entry), 'Removed stale scoped staging dir');
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
for (const entry of fs.readdirSync(npmRoot)) {
|
|
369
|
+
if (!entry.startsWith(`.${pkgName}-`)) continue;
|
|
370
|
+
safeRemoveStaleEntry(path.join(npmRoot, entry), 'Removed stale staging dir');
|
|
371
|
+
}
|
|
351
372
|
}
|
|
352
|
-
}
|
|
353
373
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
374
|
+
if (fs.existsSync(binDir)) {
|
|
375
|
+
for (const entry of fs.readdirSync(binDir)) {
|
|
376
|
+
if (!Array.from(binNames).some((name) => entry.startsWith(`.${name}-`))) continue;
|
|
377
|
+
safeRemoveStaleEntry(path.join(binDir, entry), 'Removed stale bin staging entry');
|
|
378
|
+
}
|
|
359
379
|
}
|
|
380
|
+
} catch (error: any) {
|
|
381
|
+
appendUpgradeLog(`Stale staging cleanup skipped (${error?.code || 'error'}): ${error?.message || String(error)}`);
|
|
360
382
|
}
|
|
361
383
|
}
|
|
362
384
|
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import * as fs from 'node:fs';
|
|
22
22
|
import * as os from 'node:os';
|
|
23
23
|
import * as path from 'node:path';
|
|
24
|
+
import { LOG } from '../../logging/logger.js';
|
|
24
25
|
import type {
|
|
25
26
|
NativeHistoryConfig,
|
|
26
27
|
NativeHistoryJsonlSource,
|
|
@@ -138,8 +139,48 @@ function executeJsonl(src: NativeHistoryJsonlSource, input: NativeHistoryInput):
|
|
|
138
139
|
|| (requestedSessionId ? null : pickSessionBoundFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor, workspaceHint))
|
|
139
140
|
|| (requestedSessionId ? null : newestRecentFileAcrossDateWindow(src.path, input, filePat, windowMs, sessionFloor));
|
|
140
141
|
}
|
|
142
|
+
// Raw-workspace slug candidate: `resolved` derives its {cwd*} slug from
|
|
143
|
+
// fs.realpathSync(workspace). On Windows realpath normalizes the path
|
|
144
|
+
// (drive-letter case D:↔d:, \\?\ long-path prefix, junction expansion)
|
|
145
|
+
// so the slug can diverge from the one the CLI actually wrote — and the
|
|
146
|
+
// concrete path above then misses with ENOENT. Retry with the slug built
|
|
147
|
+
// from the RAW workspace string before falling back to a scan; it's the
|
|
148
|
+
// cheap fix when realpath divergence is the only problem.
|
|
149
|
+
if (!sourcePath && !hasDateTemplateSegment(src.path)) {
|
|
150
|
+
const resolvedRaw = expandPath(src.path, input, { skipWorkspaceRealpath: true });
|
|
151
|
+
if (resolvedRaw && resolvedRaw !== resolved) {
|
|
152
|
+
try {
|
|
153
|
+
const rawStat = fs.statSync(resolvedRaw);
|
|
154
|
+
if (rawStat.isFile()) sourcePath = resolvedRaw;
|
|
155
|
+
else if (rawStat.isDirectory()) {
|
|
156
|
+
sourcePath = pickExactSessionFile(resolvedRaw, filePat, requestedSessionId)
|
|
157
|
+
|| (requestedSessionId ? null : newestRecentFile(resolvedRaw, filePat, windowMs, sessionFloor));
|
|
158
|
+
}
|
|
159
|
+
} catch { /* raw slug also missed — fall through to scan */ }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Last-resort scan: the slug-derived directory missed entirely (the
|
|
163
|
+
// dominant Windows failure: realpath/raw slug both diverge from the CLI's
|
|
164
|
+
// on-disk project dir → 0 messages, no PTY fallback for native-source
|
|
165
|
+
// providers). When we have an exact session id, walk the projects root
|
|
166
|
+
// for `<sessionId>.jsonl` regardless of which project subdir holds it.
|
|
167
|
+
// The session id is a UUID, so basename matching is unambiguous; mirrors
|
|
168
|
+
// the standalone reader's scan (claude-cli-transcript.ts resolveTranscriptPath).
|
|
169
|
+
if (!sourcePath && requestedSessionId) {
|
|
170
|
+
sourcePath = scanProjectsRootForSessionFile(src.path, input, requestedSessionId);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!sourcePath) {
|
|
174
|
+
// Was silent before — a slug miss produced 0 messages with no trace, so
|
|
175
|
+
// a live read_chat returning empty was indistinguishable from "no file"
|
|
176
|
+
// vs "wrong path". Log the attempted concrete path + both slug variants
|
|
177
|
+
// so the failure mode is greppable in daemon logs.
|
|
178
|
+
const wsRaw = typeof input.workspace === 'string' ? input.workspace : '';
|
|
179
|
+
let wsReal = wsRaw;
|
|
180
|
+
try { if (wsRaw) wsReal = fs.realpathSync(wsRaw); } catch { /* keep raw */ }
|
|
181
|
+
LOG.debug('NativeHistory', `jsonl unresolved: tried=${JSON.stringify(resolved)} sessionId=${requestedSessionId || '(none)'} wsRaw=${JSON.stringify(wsRaw)} wsReal=${JSON.stringify(wsReal)} rawSlug=${JSON.stringify(claudeProjectDirName(wsRaw))} realSlug=${JSON.stringify(claudeProjectDirName(wsReal))} (concrete miss + raw-slug retry + projects scan all failed)`);
|
|
182
|
+
return null;
|
|
141
183
|
}
|
|
142
|
-
if (!sourcePath) return null;
|
|
143
184
|
|
|
144
185
|
const mtime = safeMtimeMs(sourcePath);
|
|
145
186
|
const lines = readJsonlLines(sourcePath);
|
|
@@ -278,7 +319,7 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
|
|
|
278
319
|
// Path expansion + globbing
|
|
279
320
|
// ────────────────────────────────────────────────────────────────────────────
|
|
280
321
|
|
|
281
|
-
function expandPath(template: string, input: NativeHistoryInput): string | null {
|
|
322
|
+
function expandPath(template: string, input: NativeHistoryInput, opts?: { skipWorkspaceRealpath?: boolean }): string | null {
|
|
282
323
|
if (!template) return null;
|
|
283
324
|
let out = template;
|
|
284
325
|
if (out.startsWith('~/') || out === '~') {
|
|
@@ -301,7 +342,11 @@ function expandPath(template: string, input: NativeHistoryInput): string | null
|
|
|
301
342
|
// `-`. Realpath also handles aliases such as /tmp -> /private/tmp.
|
|
302
343
|
const workspaceRaw = input.workspace ?? '';
|
|
303
344
|
let workspaceResolved = workspaceRaw;
|
|
304
|
-
|
|
345
|
+
// The caller may request the RAW slug (skip realpath) to recover from
|
|
346
|
+
// Windows realpath normalization diverging the {cwd*} slug from the dir
|
|
347
|
+
// the CLI actually created. Default keeps realpath (handles /tmp ->
|
|
348
|
+
// /private/tmp aliasing that the CLI itself resolves on macOS).
|
|
349
|
+
if (workspaceRaw && !opts?.skipWorkspaceRealpath) {
|
|
305
350
|
try { workspaceResolved = fs.realpathSync(workspaceRaw); }
|
|
306
351
|
catch { /* path may not exist yet — keep the raw value */ }
|
|
307
352
|
}
|
|
@@ -333,6 +378,62 @@ function claudeProjectDirName(workspace: string): string {
|
|
|
333
378
|
return workspace.replace(/[^A-Za-z0-9_-]/g, '-');
|
|
334
379
|
}
|
|
335
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Last-resort lookup for a transcript by its exact session id, ignoring the
|
|
383
|
+
* per-cwd slug entirely. The slug-derived directory above can miss completely
|
|
384
|
+
* when the CLI's on-disk project dir disagrees with the slug we reconstruct
|
|
385
|
+
* (notably on Windows, where fs.realpathSync normalizes drive-letter case and
|
|
386
|
+
* adds a \\?\ prefix). Since the session id is a UUID, scanning the projects
|
|
387
|
+
* root for `<sessionId>.jsonl` is unambiguous.
|
|
388
|
+
*
|
|
389
|
+
* Derives the scan base from the template's segments up to (but excluding) the
|
|
390
|
+
* first one that references a per-session variable ({cwd*} or {session_id}) —
|
|
391
|
+
* e.g. `~/.claude/projects/{cwd_claude_project}/{session_id}.jsonl` → scan
|
|
392
|
+
* `~/.claude/projects`. Returns the matching file path, or null.
|
|
393
|
+
*/
|
|
394
|
+
function scanProjectsRootForSessionFile(template: string, input: NativeHistoryInput, requestedSessionId: string): string | null {
|
|
395
|
+
if (!requestedSessionId) return null;
|
|
396
|
+
// Resolve the leading static portion of the template (everything before the
|
|
397
|
+
// first {var} segment) into a concrete base directory.
|
|
398
|
+
let head = template;
|
|
399
|
+
if (head.startsWith('~/') || head === '~') head = path.join(os.homedir(), head.slice(2));
|
|
400
|
+
const segs = head.split('/');
|
|
401
|
+
const baseParts: string[] = [];
|
|
402
|
+
for (const seg of segs) {
|
|
403
|
+
if (/[{}*?]/.test(seg)) break;
|
|
404
|
+
baseParts.push(seg);
|
|
405
|
+
}
|
|
406
|
+
const base = baseParts.join('/');
|
|
407
|
+
if (!base) return null;
|
|
408
|
+
let baseStat: fs.Stats | null = null;
|
|
409
|
+
try { baseStat = fs.statSync(base); } catch { return null; }
|
|
410
|
+
if (!baseStat.isDirectory()) return null;
|
|
411
|
+
|
|
412
|
+
const needle = `${requestedSessionId.toLowerCase()}.jsonl`;
|
|
413
|
+
// Bounded walk: project layouts are <root>/<projectDir>/<uuid>.jsonl, so a
|
|
414
|
+
// shallow scan (root + one level of subdirs) suffices and avoids walking an
|
|
415
|
+
// unbounded tree. Check the root itself first, then each immediate subdir.
|
|
416
|
+
const dirsToScan: string[] = [base];
|
|
417
|
+
try {
|
|
418
|
+
for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
|
|
419
|
+
if (entry.isDirectory()) dirsToScan.push(path.join(base, entry.name));
|
|
420
|
+
}
|
|
421
|
+
} catch { /* readdir failed — fall back to scanning base only */ }
|
|
422
|
+
|
|
423
|
+
for (const dir of dirsToScan) {
|
|
424
|
+
let entries: fs.Dirent[];
|
|
425
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
426
|
+
for (const entry of entries) {
|
|
427
|
+
if (!entry.isFile()) continue;
|
|
428
|
+
if (entry.name.toLowerCase() !== needle) continue;
|
|
429
|
+
const found = path.join(dir, entry.name);
|
|
430
|
+
LOG.debug('NativeHistory', `jsonl scan-fallback hit: sessionId=${requestedSessionId} resolved via projects-root scan → ${JSON.stringify(found)} (slug-derived path missed; likely realpath/slug divergence)`);
|
|
431
|
+
return found;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
336
437
|
function globToRegex(pattern: string): RegExp {
|
|
337
438
|
// Minimal glob: `*` → `[^/]*`, `?` → `[^/]`, `.` → `\.`. Anchored.
|
|
338
439
|
const re = pattern
|