hammadev 0.1.0-alpha.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.
@@ -0,0 +1,21 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export function defaultCodexHome() {
4
+ return path.join(os.homedir(), ".codex");
5
+ }
6
+ export function codexSessionsGlob(codexHome = defaultCodexHome()) {
7
+ return path.join(codexHome, "sessions", "*", "*", "*", "rollout-*.jsonl");
8
+ }
9
+ const ROLLOUT_RE = /^rollout-(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2})-(.+)\.jsonl$/;
10
+ export function parseRolloutFilename(filePath) {
11
+ const base = path.basename(filePath);
12
+ const match = base.match(ROLLOUT_RE);
13
+ if (!match)
14
+ return null;
15
+ const [, timestampRaw, conversationId] = match;
16
+ return {
17
+ timestampRaw,
18
+ conversationId,
19
+ startedAt: timestampRaw.replace(/T(\d{2})-(\d{2})-(\d{2})$/, "T$1:$2:$3")
20
+ };
21
+ }
@@ -0,0 +1,59 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { discoverCodexSessions } from "./discover.js";
4
+ const CODEX_PREFIX = "codex:";
5
+ export async function resolveCodexTarget(target, options = {}) {
6
+ if (target.startsWith(CODEX_PREFIX)) {
7
+ const rest = target.slice(CODEX_PREFIX.length);
8
+ if (!rest) {
9
+ throw new Error(`Invalid Codex target '${target}'. Expected 'codex:last', 'codex:<conversationId>', or a rollout file path.`);
10
+ }
11
+ const sessions = await discoverCodexSessions(options.codexHome);
12
+ if (rest === "last") {
13
+ const latest = sessions[0];
14
+ if (!latest)
15
+ throw new Error("No Codex sessions found.");
16
+ return latest.path;
17
+ }
18
+ return resolveByConversationId(rest, sessions);
19
+ }
20
+ return resolveByFilePath(target);
21
+ }
22
+ function resolveByConversationId(id, sessions) {
23
+ const exact = sessions.find((s) => s.conversationId === id);
24
+ if (exact)
25
+ return exact.path;
26
+ const prefixMatches = sessions.filter((s) => s.conversationId.startsWith(id));
27
+ if (prefixMatches.length === 1)
28
+ return prefixMatches[0].path;
29
+ if (prefixMatches.length > 1) {
30
+ const list = prefixMatches
31
+ .map((s) => ` - ${s.conversationId} (${s.path})`)
32
+ .join("\n");
33
+ throw new Error(`Ambiguous Codex conversationId prefix '${id}'. Matches ${prefixMatches.length} sessions:\n${list}`);
34
+ }
35
+ throw new Error(`No Codex session found with conversationId matching '${id}'.`);
36
+ }
37
+ async function resolveByFilePath(target) {
38
+ const abs = path.resolve(target);
39
+ const base = path.basename(abs);
40
+ if (!abs.endsWith(".jsonl")) {
41
+ throw new Error(`Rollout file must have a .jsonl extension: ${abs}`);
42
+ }
43
+ if (!base.startsWith("rollout-")) {
44
+ throw new Error(`Rollout file basename must start with 'rollout-': ${base}`);
45
+ }
46
+ try {
47
+ const stat = await fs.stat(abs);
48
+ if (!stat.isFile()) {
49
+ throw new Error(`Rollout path is not a regular file: ${abs}`);
50
+ }
51
+ }
52
+ catch (err) {
53
+ if (err.code === "ENOENT") {
54
+ throw new Error(`Rollout file does not exist: ${abs}`);
55
+ }
56
+ throw err;
57
+ }
58
+ return abs;
59
+ }
@@ -0,0 +1,214 @@
1
+ import fs from "node:fs";
2
+ import readline from "node:readline";
3
+ import { redactText } from "../../core/redact.js";
4
+ import { parseRolloutFilename } from "./paths.js";
5
+ function stringifySafe(value) {
6
+ if (typeof value === "string")
7
+ return value;
8
+ try {
9
+ return JSON.stringify(value, null, 2);
10
+ }
11
+ catch {
12
+ return String(value);
13
+ }
14
+ }
15
+ function extractContentText(content) {
16
+ if (!content)
17
+ return undefined;
18
+ if (typeof content === "string")
19
+ return content;
20
+ if (Array.isArray(content)) {
21
+ const parts = content
22
+ .map((item) => {
23
+ if (typeof item === "string")
24
+ return item;
25
+ if (typeof item?.text === "string")
26
+ return item.text;
27
+ if (typeof item?.content === "string")
28
+ return item.content;
29
+ return "";
30
+ })
31
+ .filter(Boolean);
32
+ return parts.length > 0 ? parts.join("\n") : undefined;
33
+ }
34
+ if (typeof content === "object") {
35
+ const obj = content;
36
+ if (typeof obj.text === "string")
37
+ return obj.text;
38
+ if (typeof obj.content === "string")
39
+ return obj.content;
40
+ if (typeof obj.message === "string")
41
+ return obj.message;
42
+ }
43
+ return undefined;
44
+ }
45
+ function extractPayloadText(payload) {
46
+ if (!payload)
47
+ return undefined;
48
+ if (typeof payload.message === "string")
49
+ return payload.message;
50
+ if (typeof payload.text === "string")
51
+ return payload.text;
52
+ if (typeof payload.content === "string")
53
+ return payload.content;
54
+ const fromContent = extractContentText(payload.content);
55
+ if (fromContent)
56
+ return fromContent;
57
+ return undefined;
58
+ }
59
+ function redactIntoSession(session, text) {
60
+ const redacted = redactText(text);
61
+ session.security.redactionCount += redacted.count;
62
+ if (redacted.count > 0)
63
+ session.security.redacted = true;
64
+ return redacted.text;
65
+ }
66
+ export async function parseCodexRollout(rolloutPath) {
67
+ try {
68
+ await fs.promises.access(rolloutPath, fs.constants.R_OK);
69
+ }
70
+ catch (err) {
71
+ throw new Error(`Rollout file is missing or not readable: ${rolloutPath}`);
72
+ }
73
+ const parsedFile = parseRolloutFilename(rolloutPath);
74
+ const session = {
75
+ meta: {
76
+ sourceCli: "codex",
77
+ sourceSessionId: parsedFile?.conversationId ?? "",
78
+ startedAt: parsedFile?.startedAt,
79
+ sourcePath: rolloutPath
80
+ },
81
+ messages: [],
82
+ shellCommands: [],
83
+ parserWarnings: [],
84
+ security: {
85
+ redacted: false,
86
+ redactionCount: 0,
87
+ warnings: []
88
+ }
89
+ };
90
+ const fileStream = fs.createReadStream(rolloutPath);
91
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
92
+ const toolCalls = new Map();
93
+ for await (const line of rl) {
94
+ if (!line.trim())
95
+ continue;
96
+ let item;
97
+ try {
98
+ item = JSON.parse(line);
99
+ }
100
+ catch {
101
+ session.parserWarnings.push("Skipped malformed JSONL line.");
102
+ continue;
103
+ }
104
+ const topType = String(item.type ?? "");
105
+ const payload = item.payload ?? {};
106
+ const payloadType = String(payload.type ?? "");
107
+ const timestamp = item.timestamp ? String(item.timestamp) : undefined;
108
+ // session_meta line
109
+ if (topType === "session_meta") {
110
+ const id = payload.session_id ?? payload.id;
111
+ const cwd = payload.cwd;
112
+ if (id && !session.meta.sourceSessionId)
113
+ session.meta.sourceSessionId = String(id);
114
+ if (cwd)
115
+ session.meta.projectPath = String(cwd);
116
+ if (payload.timestamp && !session.meta.startedAt) {
117
+ session.meta.startedAt = String(payload.timestamp);
118
+ }
119
+ continue;
120
+ }
121
+ // turn_context contains cwd/model/git-ish context
122
+ if (topType === "turn_context") {
123
+ if (payload.cwd)
124
+ session.meta.projectPath = String(payload.cwd);
125
+ continue;
126
+ }
127
+ // event_msg: user visible events
128
+ if (topType === "event_msg") {
129
+ if (payloadType === "user_message") {
130
+ const raw = extractPayloadText(payload);
131
+ if (raw) {
132
+ session.messages.push({
133
+ role: "user",
134
+ content: redactIntoSession(session, raw),
135
+ timestamp
136
+ });
137
+ }
138
+ continue;
139
+ }
140
+ if (payloadType === "agent_message") {
141
+ const raw = extractPayloadText(payload);
142
+ if (raw) {
143
+ session.messages.push({
144
+ role: "assistant",
145
+ content: redactIntoSession(session, raw),
146
+ timestamp
147
+ });
148
+ }
149
+ continue;
150
+ }
151
+ // Some command/tool completions may appear here.
152
+ if (payloadType === "mcp_tool_call_end") {
153
+ const invocation = payload.invocation;
154
+ const result = payload.result;
155
+ const command = invocation?.arguments?.cmd ??
156
+ invocation?.arguments?.command ??
157
+ invocation?.input?.cmd ??
158
+ invocation?.input?.command;
159
+ if (command) {
160
+ session.shellCommands.push({
161
+ command: redactIntoSession(session, stringifySafe(command)),
162
+ output: result ? redactIntoSession(session, stringifySafe(result)) : undefined,
163
+ endedAt: timestamp
164
+ });
165
+ }
166
+ continue;
167
+ }
168
+ continue;
169
+ }
170
+ // response_item: model/tool transcript items
171
+ if (topType === "response_item") {
172
+ if (payloadType === "function_call" || payloadType === "custom_tool_call") {
173
+ const callId = payload.call_id;
174
+ const name = payload.name ?? "tool_call";
175
+ if (callId) {
176
+ toolCalls.set(String(callId), {
177
+ name: String(name),
178
+ input: payload.arguments ?? payload.input
179
+ });
180
+ }
181
+ // Shell commands are often represented as custom tool calls.
182
+ const input = payload.arguments ?? payload.input;
183
+ const possibleCommand = input?.cmd ??
184
+ input?.command ??
185
+ input?.argv?.join?.(" ") ??
186
+ (name === "shell" || name === "exec" || name === "bash" ? input : undefined);
187
+ if (possibleCommand) {
188
+ session.shellCommands.push({
189
+ command: redactIntoSession(session, stringifySafe(possibleCommand)),
190
+ startedAt: timestamp
191
+ });
192
+ }
193
+ continue;
194
+ }
195
+ if (payloadType === "function_call_output" || payloadType === "custom_tool_call_output") {
196
+ const rawOutput = payload.output;
197
+ if (rawOutput !== undefined) {
198
+ const output = redactIntoSession(session, stringifySafe(rawOutput));
199
+ // Attach to latest shell command if possible.
200
+ const last = session.shellCommands[session.shellCommands.length - 1];
201
+ if (last && !last.output) {
202
+ last.output = output;
203
+ last.endedAt = timestamp;
204
+ }
205
+ }
206
+ continue;
207
+ }
208
+ // Ignore reasoning/token_count for v0.1.
209
+ continue;
210
+ }
211
+ }
212
+ session.meta.lastUpdatedAt = new Date().toISOString();
213
+ return session;
214
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { Command } from "commander";
4
+ import pc from "picocolors";
5
+ import { CodexAdapter } from "./adapters/codex/index.js";
6
+ import { ClaudeAdapter } from "./adapters/claude/index.js";
7
+ import { createHandoff } from "./core/handoff.js";
8
+ import { formatHandoffLog, listHandoffs, readHandoff } from "./core/history.js";
9
+ import { formatProjectStatus, getProjectStatus } from "./core/project-status.js";
10
+ import { runDoctor } from "./core/doctor.js";
11
+ import { loadSession, resolveSessionTarget } from "./session-loader.js";
12
+ function truncate(s, max) {
13
+ if (!s)
14
+ return s;
15
+ return s.length > max ? s.slice(0, max) + "..." : s;
16
+ }
17
+ function renderSession(session, summary) {
18
+ if (!summary)
19
+ return JSON.stringify(session, null, 2);
20
+ const view = {
21
+ meta: session.meta,
22
+ messageCount: session.messages.length,
23
+ shellCommandCount: session.shellCommands.length,
24
+ parserWarningsCount: session.parserWarnings.length,
25
+ redactionCount: session.security.redactionCount,
26
+ firstMessages: session.messages.slice(0, 5).map((m) => ({
27
+ ...m,
28
+ content: truncate(m.content, 300)
29
+ })),
30
+ lastMessages: session.messages.slice(-5).map((m) => ({
31
+ ...m,
32
+ content: truncate(m.content, 300)
33
+ })),
34
+ lastShellCommands: session.shellCommands.slice(-10).map((c) => ({
35
+ ...c,
36
+ command: truncate(c.command, 200),
37
+ output: c.output !== undefined ? "<omitted>" : undefined
38
+ }))
39
+ };
40
+ return JSON.stringify(view, null, 2);
41
+ }
42
+ function printCountMap(label, counts) {
43
+ const entries = Object.entries(counts).sort((a, b) => b[1] - a[1]);
44
+ if (entries.length === 0) {
45
+ console.log(`${label}: (none)`);
46
+ return;
47
+ }
48
+ console.log(`${label}:`);
49
+ for (const [k, v] of entries)
50
+ console.log(` ${k}: ${v}`);
51
+ }
52
+ function printClaudeShapeReport(report) {
53
+ console.log(pc.bold("Claude session shape inspection"));
54
+ console.log(pc.yellow("(read-only — no message content, tool inputs, or outputs are printed)"));
55
+ console.log("");
56
+ console.log(`File: ${report.path}`);
57
+ console.log(`Size: ${report.sizeBytes} bytes`);
58
+ console.log(`Total non-empty lines: ${report.totalLines}`);
59
+ console.log(`Parsed JSON lines: ${report.parsedLines}`);
60
+ console.log(`Malformed lines: ${report.malformedLines}`);
61
+ console.log("");
62
+ printCountMap("Top-level key frequency", report.topLevelKeyFrequency);
63
+ console.log("");
64
+ printCountMap("Type field values", report.typeCounts);
65
+ console.log("");
66
+ printCountMap("Role values", report.roleCounts);
67
+ console.log("");
68
+ const typeShapes = Object.entries(report.shapeByType).sort((a, b) => a[0].localeCompare(b[0]));
69
+ if (typeShapes.length === 0) {
70
+ console.log("Shape by type: (none)");
71
+ }
72
+ else {
73
+ console.log("Shape by type:");
74
+ for (const [t, shape] of typeShapes) {
75
+ console.log(` ${t}:`);
76
+ const keys = Object.keys(shape).sort();
77
+ for (const k of keys)
78
+ console.log(` ${k}: ${shape[k]}`);
79
+ }
80
+ }
81
+ console.log("");
82
+ if (report.cwdValues.length === 0) {
83
+ console.log("Detected cwd values: (none)");
84
+ }
85
+ else {
86
+ console.log("Detected cwd values:");
87
+ for (const v of report.cwdValues)
88
+ console.log(` ${v}`);
89
+ }
90
+ if (report.projectPathValues.length === 0) {
91
+ console.log("Detected projectPath values: (none)");
92
+ }
93
+ else {
94
+ console.log("Detected projectPath values:");
95
+ for (const v of report.projectPathValues)
96
+ console.log(` ${v}`);
97
+ }
98
+ }
99
+ const program = new Command();
100
+ program
101
+ .name("hamma")
102
+ .description("Shared memory and handoff layer for agentic coding CLIs")
103
+ .version("0.1.0-alpha.0");
104
+ program
105
+ .command("list")
106
+ .argument("<source>", "source CLI: codex | claude")
107
+ .description("List sessions from a source CLI")
108
+ .action(async (source) => {
109
+ if (source === "codex") {
110
+ const sessions = await CodexAdapter.list();
111
+ if (sessions.length === 0) {
112
+ console.log(pc.yellow("No Codex sessions found."));
113
+ return;
114
+ }
115
+ console.log(pc.bold(`Codex sessions found: ${sessions.length}\n`));
116
+ sessions.slice(0, 20).forEach((s, i) => {
117
+ console.log(`${i + 1}. ${pc.cyan(s.startedAt ?? "unknown-time")} ${s.conversationId}`);
118
+ console.log(` ${s.path}`);
119
+ console.log(` updated: ${s.lastUpdatedAt ?? "unknown"} size: ${s.sizeBytes ?? 0} bytes`);
120
+ });
121
+ return;
122
+ }
123
+ if (source === "claude") {
124
+ const sessions = await ClaudeAdapter.list();
125
+ console.log(pc.yellow("Claude Code discovery is read-only and experimental — no files are modified."));
126
+ if (sessions.length === 0) {
127
+ console.log(pc.yellow("\nNo Claude Code session files found."));
128
+ console.log("Looked under ~/.claude, ~/.config/claude, and ~/.local/share/claude for projects/**/*.jsonl, sessions/**/*.jsonl, and history/**/*.jsonl.");
129
+ console.log("If Claude Code is installed elsewhere on this machine, this is expected.");
130
+ return;
131
+ }
132
+ console.log(pc.bold(`\nCandidate Claude session files: ${sessions.length}\n`));
133
+ sessions.slice(0, 20).forEach((s, i) => {
134
+ const idLabel = s.sessionId ?? "unknown-session-id";
135
+ console.log(`${i + 1}. ${pc.cyan(s.lastUpdatedAt)} ${idLabel}`);
136
+ console.log(` ${s.path}`);
137
+ console.log(` size: ${s.sizeBytes} bytes home: ${s.claudeHome}${s.projectPathHint ? ` cwd hint: ${s.projectPathHint}` : ""}`);
138
+ });
139
+ if (sessions.length > 20) {
140
+ console.log(pc.dim(`\n… ${sessions.length - 20} more not shown.`));
141
+ }
142
+ return;
143
+ }
144
+ console.error(pc.red(`Error: Unsupported source '${source}'. Supported: 'codex', 'claude'.`));
145
+ process.exit(1);
146
+ });
147
+ program
148
+ .command("inspect")
149
+ .argument("<target>", "codex:last | codex:<conversationId> | claude:last | claude:<sessionId> | session JSONL path")
150
+ .option("--summary", "Print a summarized view (meta, counts, head/tail messages)")
151
+ .option("--shape", "Read-only shape report for Claude targets — no message content is printed")
152
+ .description("Inspect one session")
153
+ .action(async (target, options) => {
154
+ if (options.shape) {
155
+ try {
156
+ const resolved = await resolveSessionTarget(target);
157
+ if (resolved.sourceCli !== "claude") {
158
+ throw new Error("--shape is only supported for Claude sessions.");
159
+ }
160
+ const report = await ClaudeAdapter.inspectShape(resolved.sessionPath);
161
+ printClaudeShapeReport(report);
162
+ return;
163
+ }
164
+ catch (err) {
165
+ console.error(pc.red(`Error inspecting Claude session shape: ${err.message}`));
166
+ process.exit(1);
167
+ }
168
+ }
169
+ try {
170
+ const session = await loadSession(target);
171
+ console.log(renderSession(session, Boolean(options.summary)));
172
+ }
173
+ catch (err) {
174
+ console.error(pc.red(`Error inspecting session: ${err.message}`));
175
+ process.exit(1);
176
+ }
177
+ });
178
+ program
179
+ .command("handoff")
180
+ .argument("<target>", "codex:last | codex:<conversationId> | claude:last | claude:<sessionId> | session JSONL path")
181
+ .requiredOption("--to <agent>", "Target CLI (e.g. claude or codex)")
182
+ .option("--no-gitignore", "Do not modify .gitignore")
183
+ .description("Create a handoff package for another agent")
184
+ .action(async (target, options) => {
185
+ try {
186
+ const session = await loadSession(target);
187
+ await createHandoff(session, options.to, options.gitignore);
188
+ }
189
+ catch (err) {
190
+ console.error(pc.red(`Error processing handoff: ${err.message}`));
191
+ process.exit(1);
192
+ }
193
+ });
194
+ program
195
+ .command("status")
196
+ .option("--project <path>", "Project directory to inspect")
197
+ .description("Show a read-only project and local session overview")
198
+ .action(async (options) => {
199
+ const projectPath = path.resolve(options.project ?? process.cwd());
200
+ try {
201
+ console.log(formatProjectStatus(await getProjectStatus(projectPath)));
202
+ }
203
+ catch (err) {
204
+ console.error(pc.red(`Error reading project status: ${err.message}`));
205
+ process.exit(1);
206
+ }
207
+ });
208
+ program
209
+ .command("log")
210
+ .option("--project <path>", "Project whose local handoff history should be listed")
211
+ .description("List local handoffs for a project, newest first")
212
+ .action(async (options) => {
213
+ const projectPath = path.resolve(options.project ?? process.cwd());
214
+ try {
215
+ const handoffs = await listHandoffs(projectPath);
216
+ if (handoffs.length === 0) {
217
+ console.log(pc.yellow(`No handoffs found in ${path.join(projectPath, ".hamma", "tasks")}.`));
218
+ return;
219
+ }
220
+ console.log(formatHandoffLog(handoffs));
221
+ }
222
+ catch (err) {
223
+ console.error(pc.red(`Error reading handoff history: ${err.message}`));
224
+ process.exit(1);
225
+ }
226
+ });
227
+ program
228
+ .command("show")
229
+ .argument("<task-id>", "Handoff task id or 'latest'")
230
+ .description("Print a local handoff brief")
231
+ .action(async (taskId) => {
232
+ try {
233
+ const markdown = await readHandoff(process.cwd(), taskId);
234
+ process.stdout.write(markdown.endsWith("\n") ? markdown : markdown + "\n");
235
+ }
236
+ catch (err) {
237
+ console.error(pc.red(`Error reading handoff: ${err.message}`));
238
+ process.exit(1);
239
+ }
240
+ });
241
+ program
242
+ .command("doctor")
243
+ .description("Validate environment, Codex availability, and .gitignore safety")
244
+ .action(async () => {
245
+ const code = await runDoctor();
246
+ process.exit(code);
247
+ });
248
+ program.parseAsync();