open-claude-p 1.0.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/LICENSE +21 -0
- package/README.ja.md +708 -0
- package/README.ko.md +713 -0
- package/README.md +850 -0
- package/README.zh.md +708 -0
- package/bin/cli.js +782 -0
- package/package.json +68 -0
- package/scripts/postinstall.js +60 -0
- package/src/chat/event-filters.js +116 -0
- package/src/chat/index.js +1225 -0
- package/src/completion/detector.js +163 -0
- package/src/daemon/client.js +172 -0
- package/src/daemon/server.js +267 -0
- package/src/daemon/socket.js +78 -0
- package/src/index.js +908 -0
- package/src/options/index.js +4 -0
- package/src/options/parse-argv.js +214 -0
- package/src/options/spec.js +519 -0
- package/src/options/validate.js +104 -0
- package/src/output/index.js +8 -0
- package/src/output/json.js +83 -0
- package/src/output/registry.js +35 -0
- package/src/output/stream-json.js +111 -0
- package/src/output/text.js +94 -0
- package/src/parsers/ansi-strip.js +94 -0
- package/src/parsers/index.js +8 -0
- package/src/parsers/pipeline.js +50 -0
- package/src/parsers/registry.js +43 -0
- package/src/parsers/sentinel.js +41 -0
- package/src/parsers/tui-frame.js +256 -0
- package/src/print-mode.js +214 -0
- package/src/pty/index.js +3 -0
- package/src/pty/pool.js +127 -0
- package/src/pty/session.js +88 -0
- package/src/session-log.js +124 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Per-cwd session log under `<cwd>/.ocp/<sessionId>/`.
|
|
2
|
+
//
|
|
3
|
+
// Each turn writes four files:
|
|
4
|
+
// - prompt.txt the user prompt verbatim
|
|
5
|
+
// - response.md the assistant's clean markdown reply
|
|
6
|
+
// - meta.json timestamps, tokens, cost, tools, completion reason
|
|
7
|
+
// - events.jsonl one event per line (omitted when no events present)
|
|
8
|
+
//
|
|
9
|
+
// Also appends one line to `<cwd>/.ocp/sessions.jsonl` for chronological
|
|
10
|
+
// listing (sessionId, ts, summary).
|
|
11
|
+
//
|
|
12
|
+
// Failure mode: best-effort. Logging errors NEVER propagate to the
|
|
13
|
+
// chat result — a cwd that's read-only just means no log, not a failed
|
|
14
|
+
// request. We DO emit a one-shot warning so silent failure isn't
|
|
15
|
+
// totally invisible.
|
|
16
|
+
//
|
|
17
|
+
// The directory is auto-created with mode 0o700 and refused if it
|
|
18
|
+
// already exists with looser perms or different owner — same trust
|
|
19
|
+
// stance as the lock dir.
|
|
20
|
+
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { mkdir, lstat, chmod, writeFile, appendFile } from 'node:fs/promises';
|
|
23
|
+
|
|
24
|
+
const SESSIONS_DIR_NAME = '.ocp';
|
|
25
|
+
|
|
26
|
+
let dirVerifiedFor = new Map(); // cwd → boolean
|
|
27
|
+
let dirWarned = false;
|
|
28
|
+
|
|
29
|
+
function emitOnce(msg) {
|
|
30
|
+
if (dirWarned) return;
|
|
31
|
+
dirWarned = true;
|
|
32
|
+
try { process.emitWarning(msg, { type: 'OcpSessionLogUnsafe' }); } catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function ensureSessionsDir(cwd) {
|
|
36
|
+
const dir = path.join(cwd, SESSIONS_DIR_NAME);
|
|
37
|
+
if (dirVerifiedFor.get(cwd)) return dir;
|
|
38
|
+
try {
|
|
39
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
40
|
+
const st = await lstat(dir);
|
|
41
|
+
if (st.isSymbolicLink()) {
|
|
42
|
+
emitOnce(`ocp: refusing to write session log — ${dir} is a symbolic link`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
if (typeof process.getuid === 'function' && st.uid !== process.getuid()) {
|
|
46
|
+
emitOnce(`ocp: refusing to write session log — ${dir} owned by uid ${st.uid}`);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if ((st.mode & 0o077) !== 0) {
|
|
50
|
+
// Group/other-readable — try to tighten but don't fail if chmod
|
|
51
|
+
// can't (we still own it, so the contents leak is bounded to the
|
|
52
|
+
// owner's reads).
|
|
53
|
+
await chmod(dir, 0o700).catch(() => {});
|
|
54
|
+
}
|
|
55
|
+
dirVerifiedFor.set(cwd, true);
|
|
56
|
+
return dir;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (e?.code !== 'EACCES' && e?.code !== 'EROFS') {
|
|
59
|
+
emitOnce(`ocp: session log dir ${dir} unusable (${e?.code || e?.message})`);
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Record a single turn under `<cwd>/.ocp/<sessionId>/`.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} args
|
|
69
|
+
* @param {string} args.cwd
|
|
70
|
+
* @param {string|null} args.sessionId claude sessionId (UUID); falls back to ts-suffix if null
|
|
71
|
+
* @param {string} args.prompt original user prompt
|
|
72
|
+
* @param {string} args.response assistant text (clean markdown)
|
|
73
|
+
* @param {object} [args.meta] {durationMs, inputTokens, outputTokens, costUsd, tools, completionReason, isError}
|
|
74
|
+
* @param {Array<object>} [args.events] full event stream (writes events.jsonl when non-empty)
|
|
75
|
+
*/
|
|
76
|
+
export async function recordSession({ cwd, sessionId, prompt, response, meta, events }) {
|
|
77
|
+
if (!cwd || typeof cwd !== 'string') return;
|
|
78
|
+
const baseDir = await ensureSessionsDir(cwd);
|
|
79
|
+
if (!baseDir) return;
|
|
80
|
+
// Fall back to a synthetic id when claude didn't return one (e.g.
|
|
81
|
+
// error before session-init). Use timestamp so the dir name is still
|
|
82
|
+
// sortable, prefixed so it can't collide with a real UUID.
|
|
83
|
+
const id = sessionId || `noid-${Date.now()}`;
|
|
84
|
+
const turnDir = path.join(baseDir, id);
|
|
85
|
+
try {
|
|
86
|
+
await mkdir(turnDir, { recursive: true, mode: 0o700 });
|
|
87
|
+
const ts = new Date().toISOString();
|
|
88
|
+
const writes = [
|
|
89
|
+
writeFile(path.join(turnDir, 'prompt.txt'), prompt ?? '', { mode: 0o600 }),
|
|
90
|
+
writeFile(path.join(turnDir, 'response.md'), response ?? '', { mode: 0o600 }),
|
|
91
|
+
writeFile(
|
|
92
|
+
path.join(turnDir, 'meta.json'),
|
|
93
|
+
JSON.stringify({ sessionId: sessionId ?? null, timestamp: ts, ...(meta ?? {}) }, null, 2),
|
|
94
|
+
{ mode: 0o600 },
|
|
95
|
+
),
|
|
96
|
+
];
|
|
97
|
+
if (Array.isArray(events) && events.length > 0) {
|
|
98
|
+
// Cap to avoid an unbounded write — events can balloon to MB on
|
|
99
|
+
// long sessions. Keep the tail (most recent), the head usually
|
|
100
|
+
// is just framing/prompt-box init noise.
|
|
101
|
+
const MAX_EVENTS_LOG = 2000;
|
|
102
|
+
const slice = events.length > MAX_EVENTS_LOG ? events.slice(-MAX_EVENTS_LOG) : events;
|
|
103
|
+
writes.push(writeFile(
|
|
104
|
+
path.join(turnDir, 'events.jsonl'),
|
|
105
|
+
slice.map((e) => JSON.stringify(e)).join('\n') + '\n',
|
|
106
|
+
{ mode: 0o600 },
|
|
107
|
+
));
|
|
108
|
+
}
|
|
109
|
+
await Promise.all(writes);
|
|
110
|
+
// Append an index line so a single `tail -f .ocp/sessions.jsonl`
|
|
111
|
+
// gives a chronological feed of all turns in this project.
|
|
112
|
+
const indexLine = JSON.stringify({
|
|
113
|
+
ts,
|
|
114
|
+
sessionId: sessionId ?? null,
|
|
115
|
+
promptPreview: (prompt ?? '').slice(0, 80),
|
|
116
|
+
isError: !!meta?.isError,
|
|
117
|
+
durationMs: meta?.durationMs ?? null,
|
|
118
|
+
tools: meta?.tools ?? [],
|
|
119
|
+
}) + '\n';
|
|
120
|
+
await appendFile(path.join(baseDir, 'sessions.jsonl'), indexLine, { mode: 0o600 }).catch(() => {});
|
|
121
|
+
} catch (e) {
|
|
122
|
+
emitOnce(`ocp: session log write failed for ${turnDir} (${e?.code || e?.message})`);
|
|
123
|
+
}
|
|
124
|
+
}
|