mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 16:09:43 +02:00
fix: decode dotdir working directories in history session scanning
decodeProjectKey() couldn't recover a dotdir path (e.g. ~/.codeman) from Claude Code's encoded project-key names: the encoder maps both '/' and '.' to '-', so the decoder's candidate joins never matched a hidden directory on disk. It silently fell through to bare $HOME instead, which corrupted workingDir for any resumed session under a dotdir case (observed on ~/.codeman itself: history rows and state.json recorded "/home/timkjr" instead of "/home/timkjr/.codeman"). Add a dot-prefixed candidate to both the backtracking decoder and its greedy fallback so a leading empty split segment (the signature of a literal '.' in the original path) is retried as a hidden directory. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
7e357691af
commit
f470f3a4e7
@@ -2514,6 +2514,25 @@ export function registerSessionRoutes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The encoder maps both '/' and '.' to '-', so a literal '.' in the
|
||||||
|
// original path (e.g. "/home/timkjr/.codeman") collapses into an empty
|
||||||
|
// split segment here. Retry this window as a dotdir/dotfile: ".<join>".
|
||||||
|
if (segments[idx] === '' && idx + 1 < segments.length) {
|
||||||
|
const dotMaxLook = Math.min(idx + 1 + 4, segments.length);
|
||||||
|
for (let end = dotMaxLook - 1; end >= idx + 1; end--) {
|
||||||
|
const dotCandidates =
|
||||||
|
end === idx + 1
|
||||||
|
? [segments[idx + 1]]
|
||||||
|
: [segments.slice(idx + 1, end + 1).join('-'), segments.slice(idx + 1, end + 1).join('_')];
|
||||||
|
for (const child of dotCandidates) {
|
||||||
|
const candidate = current + '/.' + child;
|
||||||
|
if (await isDir(candidate)) {
|
||||||
|
const result = await tryDecode(end + 1, candidate);
|
||||||
|
if (result) return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2547,6 +2566,25 @@ export function registerSessionRoutes(
|
|||||||
}
|
}
|
||||||
if (matched) break;
|
if (matched) break;
|
||||||
}
|
}
|
||||||
|
if (!matched && segments[i] === '' && i + 1 < segments.length) {
|
||||||
|
const dotMaxLook = Math.min(i + 1 + 4, segments.length);
|
||||||
|
for (let end = i + 1; end < dotMaxLook; end++) {
|
||||||
|
const dotCandidates =
|
||||||
|
end === i + 1
|
||||||
|
? [segments[i + 1]]
|
||||||
|
: [segments.slice(i + 1, end + 1).join('_'), segments.slice(i + 1, end + 1).join('-')];
|
||||||
|
for (const child of dotCandidates) {
|
||||||
|
const candidate = current + '/.' + child;
|
||||||
|
if (await isDir(candidate)) {
|
||||||
|
current = candidate;
|
||||||
|
i = end + 1;
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matched) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!matched) {
|
if (!matched) {
|
||||||
current = current + '/' + segments[i];
|
current = current + '/' + segments[i];
|
||||||
i++;
|
i++;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import Fastify, { type FastifyInstance } from 'fastify';
|
|||||||
import fastifyCookie from '@fastify/cookie';
|
import fastifyCookie from '@fastify/cookie';
|
||||||
import fastifyMultipart from '@fastify/multipart';
|
import fastifyMultipart from '@fastify/multipart';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { mkdtemp, rm } from 'node:fs/promises';
|
import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import { createMockRouteContext, type MockRouteContext } from '../mocks/index.js';
|
import { createMockRouteContext, type MockRouteContext } from '../mocks/index.js';
|
||||||
import { installRouteErrorHandler } from '../../src/web/route-error-handler.js';
|
import { installRouteErrorHandler } from '../../src/web/route-error-handler.js';
|
||||||
@@ -1288,6 +1288,39 @@ describe('session-routes', () => {
|
|||||||
const body = JSON.parse(res.body);
|
const body = JSON.parse(res.body);
|
||||||
expect(body.data.sessions.length).toBeLessThanOrEqual(50);
|
expect(body.data.sessions.length).toBeLessThanOrEqual(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('decodes a dotdir working directory (e.g. ~/.codeman) instead of falling back to $HOME', async () => {
|
||||||
|
// Claude Code's project-key encoding maps both '/' and '.' to '-', so
|
||||||
|
// "/home/x/.dotcase" and "/home/x/dotcase" collapse to the same-looking
|
||||||
|
// dash run except for a doubled dash. decodeProjectKey() must still
|
||||||
|
// recover the real (dotdir) path rather than silently falling back to
|
||||||
|
// bare $HOME (COD bug: 2026-08-01, ~/.codeman resumed sessions got
|
||||||
|
// workingDir "/home/timkjr" instead of "/home/timkjr/.codeman").
|
||||||
|
const home = process.env.HOME as string;
|
||||||
|
const realDir = join(home, '.dotcase');
|
||||||
|
await mkdir(realDir, { recursive: true });
|
||||||
|
|
||||||
|
const projectKey = realDir.replace(/\//g, '-').replace(/\./g, '-');
|
||||||
|
const projDir = join(home, '.claude', 'projects', projectKey);
|
||||||
|
await mkdir(projDir, { recursive: true });
|
||||||
|
|
||||||
|
const sessionId = '12345678-1234-1234-1234-123456789012';
|
||||||
|
const transcriptLine = JSON.stringify({ type: 'user', message: { role: 'user', content: 'hello world' } }) + '\n';
|
||||||
|
// scanProjectDir skips files under 4000 bytes.
|
||||||
|
const padding = '#'.repeat(4200 - transcriptLine.length);
|
||||||
|
await writeFile(join(projDir, `${sessionId}.jsonl`), transcriptLine + padding);
|
||||||
|
|
||||||
|
const res = await harness.app.inject({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/api/history/sessions?projectKey=${projectKey}`,
|
||||||
|
});
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
const body = JSON.parse(res.body);
|
||||||
|
const row = body.data.sessions.find((s: { sessionId: string }) => s.sessionId === sessionId);
|
||||||
|
expect(row).toBeDefined();
|
||||||
|
expect(row.workingDir).toBe(realDir);
|
||||||
|
expect(row.workingDir).not.toBe(home);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== POST /api/sessions (with resumeSessionId) ==========
|
// ========== POST /api/sessions (with resumeSessionId) ==========
|
||||||
|
|||||||
Reference in New Issue
Block a user