@someonegg/agent-hub 0.0.0-stage → 0.7.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.md +104 -2
- package/README.zh-CN.md +99 -0
- package/README.zh-TW.md +99 -0
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/index-4ejyszaV.css +1 -0
- package/dist/client/assets/index-CKKT4cMK.js +309 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/favicon.svg +6 -0
- package/dist/client/icons/icon-192.png +0 -0
- package/dist/client/icons/icon-512.png +0 -0
- package/dist/client/icons/icon-maskable-512.png +0 -0
- package/dist/client/index.html +22 -0
- package/dist/client/manifest.webmanifest +21 -0
- package/dist/server/server/adapters/claude/claude-session-source.js +220 -0
- package/dist/server/server/adapters/claude/identity.js +28 -0
- package/dist/server/server/adapters/claude/interaction.js +68 -0
- package/dist/server/server/adapters/claude/prompt-readiness.js +12 -0
- package/dist/server/server/adapters/claude/ready-script.js +25 -0
- package/dist/server/server/adapters/claude/session-manager.js +106 -0
- package/dist/server/server/adapters/claude/session-normalizer.js +173 -0
- package/dist/server/server/adapters/claude/user-input-normalizer.js +71 -0
- package/dist/server/server/adapters/codex/catalog-source.js +2 -0
- package/dist/server/server/adapters/codex/codex-session-source.js +250 -0
- package/dist/server/server/adapters/codex/identity-resolver.js +104 -0
- package/dist/server/server/adapters/codex/interaction-parser.js +70 -0
- package/dist/server/server/adapters/codex/io-errors.js +2 -0
- package/dist/server/server/adapters/codex/jsonl-catalog-source.js +84 -0
- package/dist/server/server/adapters/codex/limits.js +2 -0
- package/dist/server/server/adapters/codex/message-normalizer.js +66 -0
- package/dist/server/server/adapters/codex/response-item-parser.js +64 -0
- package/dist/server/server/adapters/codex/rollout-descriptor.js +2 -0
- package/dist/server/server/adapters/codex/session-index-reader.js +101 -0
- package/dist/server/server/adapters/codex/session-manager.js +51 -0
- package/dist/server/server/adapters/codex/session-normalizer.js +175 -0
- package/dist/server/server/adapters/codex/subagent-identity.js +9 -0
- package/dist/server/server/adapters/codex/user-input-normalizer.js +121 -0
- package/dist/server/server/adapters/codex/value-readers.js +4 -0
- package/dist/server/server/adapters/shared/binding-protocol.js +54 -0
- package/dist/server/server/adapters/shared/io-errors.js +13 -0
- package/dist/server/server/adapters/shared/jsonl-decoder.js +263 -0
- package/dist/server/server/adapters/shared/limits.js +4 -0
- package/dist/server/server/adapters/shared/session-management.js +43 -0
- package/dist/server/server/adapters/shared/source-cache.js +23 -0
- package/dist/server/server/api/session-api-mapper.js +65 -0
- package/dist/server/server/application/interaction-session-reader.js +17 -0
- package/dist/server/server/application/session-read-service.js +86 -0
- package/dist/server/server/application/session-reader.js +2 -0
- package/dist/server/server/application/session-response.js +11 -0
- package/dist/server/server/catalog/config.js +4 -0
- package/dist/server/server/catalog/session-catalog-service.js +112 -0
- package/dist/server/server/cli/command-error.js +27 -0
- package/dist/server/server/cli/main.js +42 -0
- package/dist/server/server/cli/output.js +52 -0
- package/dist/server/server/cli/resource.js +104 -0
- package/dist/server/server/cli/session-client.js +64 -0
- package/dist/server/server/cli/session-follow.js +58 -0
- package/dist/server/server/cli/session-output.js +184 -0
- package/dist/server/server/cli/session-wait.js +44 -0
- package/dist/server/server/cli/session.js +318 -0
- package/dist/server/server/cli/skill.js +37 -0
- package/dist/server/server/config.js +196 -0
- package/dist/server/server/create-hub.js +91 -0
- package/dist/server/server/directory-lock.js +65 -0
- package/dist/server/server/domain/project-directory.js +5 -0
- package/dist/server/server/domain/session-domain.js +2 -0
- package/dist/server/server/domain/session-text.js +45 -0
- package/dist/server/server/hosting/diff-store.js +103 -0
- package/dist/server/server/hosting/routes.js +168 -0
- package/dist/server/server/hosting/store.js +162 -0
- package/dist/server/server/hosting/validation-error.js +4 -0
- package/dist/server/server/http/api-route-handler.js +2 -0
- package/dist/server/server/http/api-router.js +54 -0
- package/dist/server/server/http/create-server.js +218 -0
- package/dist/server/server/http/query-params.js +19 -0
- package/dist/server/server/http/request-body.js +48 -0
- package/dist/server/server/http/request-errors.js +59 -0
- package/dist/server/server/http/router.js +16 -0
- package/dist/server/server/http/security.js +19 -0
- package/dist/server/server/http/session-interaction-routes.js +128 -0
- package/dist/server/server/http/session-management-routes.js +105 -0
- package/dist/server/server/http/session-path.js +10 -0
- package/dist/server/server/http/session-read-routes.js +143 -0
- package/dist/server/server/http/terminal-preview-websocket.js +179 -0
- package/dist/server/server/http/upload-routes.js +124 -0
- package/dist/server/server/interaction/interaction-service.js +276 -0
- package/dist/server/server/interaction/message.js +19 -0
- package/dist/server/server/interaction/pane-binding-manager.js +62 -0
- package/dist/server/server/interaction/pane-observation.js +31 -0
- package/dist/server/server/interaction/reconcile-session-state.js +21 -0
- package/dist/server/server/interaction/runtime-capabilities.js +2 -0
- package/dist/server/server/interaction/session-runtime.js +86 -0
- package/dist/server/server/interaction/terminal-activity-service.js +81 -0
- package/dist/server/server/interaction/terminal-preview-diff.js +71 -0
- package/dist/server/server/interaction/terminal-preview-service.js +184 -0
- package/dist/server/server/interaction/tmux-launcher.js +84 -0
- package/dist/server/server/interaction/tmux-preview-listener.js +107 -0
- package/dist/server/server/interaction/tmux-service.js +334 -0
- package/dist/server/server/interaction/tmux-target.js +13 -0
- package/dist/server/server/interaction/transport.js +9 -0
- package/dist/server/server/main.js +54 -0
- package/dist/server/server/management/operation-service.js +307 -0
- package/dist/server/server/management/operation-state.js +3 -0
- package/dist/server/server/management/session-exit-service.js +50 -0
- package/dist/server/server/management/session-launch-executor.js +164 -0
- package/dist/server/server/management/session-lifecycle-executor.js +158 -0
- package/dist/server/server/management/session-manager.js +64 -0
- package/dist/server/server/repository/catalog-snapshot-store.js +318 -0
- package/dist/server/server/repository/diagnostic-logger.js +70 -0
- package/dist/server/server/repository/refresh-coordinator.js +14 -0
- package/dist/server/server/repository/session-queries.js +72 -0
- package/dist/server/server/security/opaque-id.js +17 -0
- package/dist/server/server/source/session-source.js +2 -0
- package/dist/server/server/updates/session-updates-service.js +151 -0
- package/dist/server/server/updates/update-revision.js +22 -0
- package/dist/server/server/uploads/store.js +304 -0
- package/dist/server/server/version.js +21 -0
- package/dist/server/shared/api-contract.js +2 -0
- package/dist/server/shared/diff.js +158 -0
- package/dist/server/shared/domain.js +2 -0
- package/dist/server/shared/interaction.js +22 -0
- package/dist/server/shared/task-name.js +6 -0
- package/dist/server/shared/uploads.js +8 -0
- package/package.json +98 -4
- package/skills/ahub-control/SKILL.md +93 -0
- package/skills/ahub-publish/SKILL.md +54 -0
|
Binary file
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
|
2
|
+
<rect width="512" height="512" fill="#edf4f1"/>
|
|
3
|
+
<rect x="110" y="100" width="290" height="310" rx="60" fill="#f3f8f5" stroke="#69aa9f" stroke-width="18"/>
|
|
4
|
+
<g fill="none" stroke="#28474d" stroke-width="19" stroke-linecap="round" stroke-linejoin="round"><polyline points="211,210 165,256 211,302"/><polyline points="251,315 302,315"/></g>
|
|
5
|
+
<g fill="#69aa9f"><circle cx="350" cy="185" r="12"/><circle cx="350" cy="256" r="12"/><circle cx="350" cy="327" r="12"/></g>
|
|
6
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="color-scheme" content="light" />
|
|
7
|
+
<meta name="description" content="A private, local reader for Codex sessions" />
|
|
8
|
+
<meta name="theme-color" content="#18262d" />
|
|
9
|
+
<meta name="apple-mobile-web-app-title" content="Agent Hub" />
|
|
10
|
+
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
11
|
+
<link rel="icon" href="/favicon.ico" sizes="16x16 32x32 48x48 64x64" />
|
|
12
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" sizes="any" />
|
|
13
|
+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
|
|
14
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
15
|
+
<title>Agent Hub</title>
|
|
16
|
+
<script type="module" crossorigin src="/assets/index-CKKT4cMK.js"></script>
|
|
17
|
+
<link rel="stylesheet" crossorigin href="/assets/index-4ejyszaV.css">
|
|
18
|
+
</head>
|
|
19
|
+
<body>
|
|
20
|
+
<div id="root"></div>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "/",
|
|
3
|
+
"name": "Agent Hub",
|
|
4
|
+
"short_name": "Agent Hub",
|
|
5
|
+
"description": "A private, local reader for Codex sessions",
|
|
6
|
+
"start_url": "/",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"background_color": "#f7fafa",
|
|
10
|
+
"theme_color": "#18262d",
|
|
11
|
+
"icons": [
|
|
12
|
+
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
|
13
|
+
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
|
14
|
+
{
|
|
15
|
+
"src": "/icons/icon-maskable-512.png",
|
|
16
|
+
"sizes": "512x512",
|
|
17
|
+
"type": "image/png",
|
|
18
|
+
"purpose": "maskable"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import { sessionTitleFromMarkdown } from "../../domain/session-text.js";
|
|
4
|
+
import { isExpectedIoError as expectedIo } from "../shared/io-errors.js";
|
|
5
|
+
import { CheckpointedJsonlDecoder } from "../shared/jsonl-decoder.js";
|
|
6
|
+
import { MAX_SESSION_DIAGNOSTICS } from "../shared/limits.js";
|
|
7
|
+
import { acceptedPathOrder, mutableTelemetry, sameFingerprint, } from "../shared/source-cache.js";
|
|
8
|
+
import { claudeInteraction, claudeRecordBindings, validClaudeId } from "./interaction.js";
|
|
9
|
+
import { appendClaudeRecords, claudeDecoderDiagnostics, createClaudeState, } from "./session-normalizer.js";
|
|
10
|
+
export class ClaudeSessionSource {
|
|
11
|
+
directory;
|
|
12
|
+
descriptor;
|
|
13
|
+
#decoder = new CheckpointedJsonlDecoder();
|
|
14
|
+
#snapshot = null;
|
|
15
|
+
#telemetry = mutableTelemetry();
|
|
16
|
+
lastRefreshTelemetry() {
|
|
17
|
+
return { ...this.#telemetry };
|
|
18
|
+
}
|
|
19
|
+
#cache = new Map();
|
|
20
|
+
#rejected = new Map();
|
|
21
|
+
constructor(directory, instanceKey, { agent = "claude" } = {}) {
|
|
22
|
+
this.directory = directory;
|
|
23
|
+
this.descriptor = {
|
|
24
|
+
sourceType: "claude-jsonl",
|
|
25
|
+
instanceKey,
|
|
26
|
+
agent,
|
|
27
|
+
displayName: "Claude Code",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async refresh() {
|
|
31
|
+
const diagnostics = [];
|
|
32
|
+
const bindings = [];
|
|
33
|
+
this.#telemetry = mutableTelemetry();
|
|
34
|
+
const warn = (...items) => {
|
|
35
|
+
diagnostics.push(...items.slice(0, Math.max(0, MAX_SESSION_DIAGNOSTICS - diagnostics.length)));
|
|
36
|
+
};
|
|
37
|
+
const next = new Map();
|
|
38
|
+
const rejected = new Map();
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const projects = join(this.directory, "projects");
|
|
41
|
+
try {
|
|
42
|
+
const candidates = await discoverClaudeFiles(projects, warn);
|
|
43
|
+
candidates.sort((a, b) => acceptedPathOrder(this.#cache, relative(this.directory, a.path), relative(this.directory, b.path)));
|
|
44
|
+
for (const { path, nativeId } of candidates) {
|
|
45
|
+
if (seen.has(nativeId)) {
|
|
46
|
+
warn({
|
|
47
|
+
code: "duplicate_native_session_id",
|
|
48
|
+
severity: "warning",
|
|
49
|
+
message: "Duplicate Claude session file ignored",
|
|
50
|
+
ordinal: null,
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const key = relative(this.directory, path);
|
|
55
|
+
const old = this.#cache.get(key) ?? this.#rejected.get(key);
|
|
56
|
+
let decoded;
|
|
57
|
+
let fingerprint;
|
|
58
|
+
try {
|
|
59
|
+
const info = await stat(path);
|
|
60
|
+
fingerprint = { sourceRelativePath: key, size: info.size, mtimeMs: info.mtimeMs };
|
|
61
|
+
if (old && sameFingerprint(old.fingerprint, fingerprint)) {
|
|
62
|
+
if (old.entry !== undefined) {
|
|
63
|
+
next.set(key, old);
|
|
64
|
+
seen.add(nativeId);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
rejected.set(key, old);
|
|
68
|
+
warn(...old.state.diagnostics);
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const started = performance.now();
|
|
73
|
+
decoded = await this.#decoder.decode({ path }, old?.checkpoint);
|
|
74
|
+
this.#telemetry.decodeMs += performance.now() - started;
|
|
75
|
+
this.#telemetry[decoded.mode === "append" ? "appendFiles" : "fullFiles"]++;
|
|
76
|
+
this.#telemetry.probeBytes += decoded.telemetry.probeBytes;
|
|
77
|
+
this.#telemetry.decodeBytes += decoded.telemetry.decodeBytes;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!expectedIo(error))
|
|
81
|
+
throw error;
|
|
82
|
+
warn({
|
|
83
|
+
code: "session_unavailable",
|
|
84
|
+
severity: "warning",
|
|
85
|
+
message: "Claude session could not be read",
|
|
86
|
+
ordinal: null,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const append = decoded.mode === "append" && !!old;
|
|
91
|
+
const base = append ? old.state : createClaudeState(nativeId);
|
|
92
|
+
const started = performance.now();
|
|
93
|
+
const bindingEvents = decoded.records.flatMap(claudeRecordBindings);
|
|
94
|
+
const state = appendClaudeRecords(base, decoded.records, claudeDecoderDiagnostics(decoded.diagnostics), bindingEvents);
|
|
95
|
+
this.#telemetry.normalizeMs += performance.now() - started;
|
|
96
|
+
if (state.identityConflict) {
|
|
97
|
+
warn(...state.diagnostics);
|
|
98
|
+
rejected.set(key, { fingerprint, checkpoint: decoded.checkpoint, state });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const entry = old?.entry !== undefined && old.state === state
|
|
102
|
+
? old.entry
|
|
103
|
+
: materializeClaudeSession(state, nativeId, this.descriptor);
|
|
104
|
+
const localId = entry.localId;
|
|
105
|
+
next.set(key, {
|
|
106
|
+
fingerprint,
|
|
107
|
+
checkpoint: decoded.checkpoint,
|
|
108
|
+
state,
|
|
109
|
+
entry,
|
|
110
|
+
});
|
|
111
|
+
seen.add(nativeId);
|
|
112
|
+
bindings.push(...bindingEvents.map((event) => ({ ...event, localId })));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (!expectedIo(error))
|
|
117
|
+
throw error;
|
|
118
|
+
if (error.code !== "ENOENT")
|
|
119
|
+
warn({
|
|
120
|
+
code: "source_unavailable",
|
|
121
|
+
severity: "warning",
|
|
122
|
+
message: "Claude projects directory could not be read",
|
|
123
|
+
ordinal: null,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
this.#cache = next;
|
|
127
|
+
this.#rejected = rejected;
|
|
128
|
+
const sessions = [...next.values()].map((cached) => cached.entry);
|
|
129
|
+
const snapshot = {
|
|
130
|
+
bindings,
|
|
131
|
+
sessions,
|
|
132
|
+
diagnostics,
|
|
133
|
+
signature: JSON.stringify([
|
|
134
|
+
[...next].map(([path, cached]) => [path, cached.fingerprint, cached.checkpoint]),
|
|
135
|
+
diagnostics,
|
|
136
|
+
]),
|
|
137
|
+
};
|
|
138
|
+
if (this.#snapshot?.signature === snapshot.signature)
|
|
139
|
+
return this.#snapshot;
|
|
140
|
+
this.#snapshot = snapshot;
|
|
141
|
+
return snapshot;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export async function createClaudeSessionSource(configDirectory, options = {}) {
|
|
145
|
+
const directory = resolve(configDirectory);
|
|
146
|
+
return new ClaudeSessionSource(directory, `claude-jsonl\0${directory}`, options);
|
|
147
|
+
}
|
|
148
|
+
async function discoverClaudeFiles(projects, warn) {
|
|
149
|
+
const candidates = [];
|
|
150
|
+
for (const project of (await readdir(projects, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
151
|
+
if (!project.isDirectory())
|
|
152
|
+
continue;
|
|
153
|
+
let files;
|
|
154
|
+
try {
|
|
155
|
+
files = await readdir(join(projects, project.name), { withFileTypes: true });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (!expectedIo(error))
|
|
159
|
+
throw error;
|
|
160
|
+
warn({
|
|
161
|
+
code: "project_unavailable",
|
|
162
|
+
severity: "warning",
|
|
163
|
+
message: "Claude project directory could not be read",
|
|
164
|
+
ordinal: null,
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const file of files.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
169
|
+
if (!file.isFile() || !file.name.endsWith(".jsonl"))
|
|
170
|
+
continue;
|
|
171
|
+
const nativeId = file.name.slice(0, -6);
|
|
172
|
+
if (!validClaudeId(nativeId))
|
|
173
|
+
continue;
|
|
174
|
+
candidates.push({ path: join(projects, project.name, file.name), nativeId });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return candidates;
|
|
178
|
+
}
|
|
179
|
+
function materializeClaudeSession(state, nativeId, descriptor) {
|
|
180
|
+
const projectDirectory = state.project ?? state.firstCwd;
|
|
181
|
+
const origin = {
|
|
182
|
+
sourceType: descriptor.sourceType,
|
|
183
|
+
agent: descriptor.agent,
|
|
184
|
+
runtimeName: descriptor.displayName,
|
|
185
|
+
runtimeVersion: state.version,
|
|
186
|
+
};
|
|
187
|
+
const localId = `thread:${nativeId}`;
|
|
188
|
+
const normalized = {
|
|
189
|
+
session: {
|
|
190
|
+
id: localId,
|
|
191
|
+
sourceId: nativeId,
|
|
192
|
+
origin,
|
|
193
|
+
title: state.customTitle ??
|
|
194
|
+
state.aiTitle ??
|
|
195
|
+
(state.firstUser ? sessionTitleFromMarkdown(state.firstUser) : null) ??
|
|
196
|
+
"Untitled session",
|
|
197
|
+
workingDirectory: state.cwd,
|
|
198
|
+
projectDirectory,
|
|
199
|
+
taskName: state.taskName,
|
|
200
|
+
createdAt: state.createdAt,
|
|
201
|
+
updatedAt: state.updatedAt,
|
|
202
|
+
archived: false,
|
|
203
|
+
parentId: null,
|
|
204
|
+
childIds: [],
|
|
205
|
+
subagent: null,
|
|
206
|
+
warningCount: state.diagnostics.length,
|
|
207
|
+
diagnostics: state.diagnostics,
|
|
208
|
+
},
|
|
209
|
+
timeline: state.timeline,
|
|
210
|
+
interaction: claudeInteraction(projectDirectory, state.taskName),
|
|
211
|
+
};
|
|
212
|
+
return {
|
|
213
|
+
localId,
|
|
214
|
+
nativeSessionId: nativeId,
|
|
215
|
+
parentNativeSessionId: null,
|
|
216
|
+
origin,
|
|
217
|
+
normalized,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=claude-session-source.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { validProjectDirectory } from "../../domain/project-directory.js";
|
|
2
|
+
const string = (value) => (typeof value === "string" ? value : null);
|
|
3
|
+
/** Native identity and directory/version/time evidence is accumulated separately from content. */
|
|
4
|
+
export function appendClaudeIdentity(state, row, ordinal, retainDiagnostic) {
|
|
5
|
+
const identities = [string(row.sessionId), string(row.session_id)].filter(Boolean);
|
|
6
|
+
if (identities.some((identity) => identity !== state.nativeId)) {
|
|
7
|
+
state.identityConflict = true;
|
|
8
|
+
retainDiagnostic({
|
|
9
|
+
code: "conflicting_native_session_id",
|
|
10
|
+
severity: "warning",
|
|
11
|
+
message: "Claude record identity conflicts with its session file",
|
|
12
|
+
ordinal: ordinal,
|
|
13
|
+
});
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const timestamp = string(row.timestamp);
|
|
17
|
+
if (timestamp && Number.isFinite(Date.parse(timestamp))) {
|
|
18
|
+
state.createdAt ??= timestamp;
|
|
19
|
+
state.updatedAt = timestamp;
|
|
20
|
+
}
|
|
21
|
+
if (validProjectDirectory(row.cwd)) {
|
|
22
|
+
state.firstCwd ??= row.cwd;
|
|
23
|
+
state.cwd = row.cwd;
|
|
24
|
+
}
|
|
25
|
+
state.version = string(row.version) ?? state.version;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { validTaskName } from "../../../shared/task-name.js";
|
|
2
|
+
import { validProjectDirectory } from "../../domain/project-directory.js";
|
|
3
|
+
import { shellQuote } from "../../interaction/tmux-launcher.js";
|
|
4
|
+
import { bindingAttempts } from "../shared/binding-protocol.js";
|
|
5
|
+
import { DEFAULT_KEY_BINDINGS, interruptSteps } from "../shared/session-management.js";
|
|
6
|
+
export const validClaudeId = (id) => /^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(id);
|
|
7
|
+
export function claudeBindingCommand(operationId = "", projectDirectory, taskName = null) {
|
|
8
|
+
if (operationId && !validClaudeId(operationId))
|
|
9
|
+
throw new Error("Invalid operation ID");
|
|
10
|
+
if (taskName !== null && !validTaskName(taskName))
|
|
11
|
+
throw new Error("Invalid task name");
|
|
12
|
+
// Claude worktree isolation rejects tmux commands; the launcher supplies its known name.
|
|
13
|
+
return `! printf 'AHUB_TMUX_BIND_V3\\n%s\\n%s\\n%s\\n%s\\n%s\\n' "\${TMUX%%,*}" "$AHUB_TMUX_SESSION_NAME" ${shellQuote(operationId)} ${shellQuote(validProjectDirectory(projectDirectory) ? projectDirectory : "")} ${shellQuote(JSON.stringify(taskName))}`;
|
|
14
|
+
}
|
|
15
|
+
/** Only call with the main user record's native bash stdout wrapper. */
|
|
16
|
+
export function claudeBindingAttempts(text, ordinal) {
|
|
17
|
+
const found = [];
|
|
18
|
+
for (const match of text.matchAll(/<bash-stdout>([\s\S]*?)<\/bash-stdout>/g)) {
|
|
19
|
+
const lines = match[1].replace(/\r\n?/g, "\n").split("\n");
|
|
20
|
+
found.push(...bindingAttempts(lines, ordinal, validClaudeId));
|
|
21
|
+
}
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
export function claudeInteraction(projectDirectory, taskName) {
|
|
25
|
+
return {
|
|
26
|
+
transport: "tmux",
|
|
27
|
+
bindingCommand: claudeBindingCommand("", projectDirectory, taskName),
|
|
28
|
+
interruptSteps,
|
|
29
|
+
keyBindings: DEFAULT_KEY_BINDINGS,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Only native main-session user shell wrappers can supply binding evidence. */
|
|
33
|
+
export function claudeRecordBindings(record) {
|
|
34
|
+
const row = record.value;
|
|
35
|
+
if (row.type !== "user" ||
|
|
36
|
+
row.isSidechain === true ||
|
|
37
|
+
row.parent_tool_use_id != null ||
|
|
38
|
+
row.isMeta === true ||
|
|
39
|
+
row.isCompactSummary === true ||
|
|
40
|
+
row.subtype === "subagent")
|
|
41
|
+
return [];
|
|
42
|
+
const message = row.message;
|
|
43
|
+
if (!message ||
|
|
44
|
+
typeof message !== "object" ||
|
|
45
|
+
!("role" in message) ||
|
|
46
|
+
message.role !== "user" ||
|
|
47
|
+
!("content" in message))
|
|
48
|
+
return [];
|
|
49
|
+
const content = typeof message.content === "string"
|
|
50
|
+
? [{ type: "text", text: message.content }]
|
|
51
|
+
: message.content;
|
|
52
|
+
if (!Array.isArray(content))
|
|
53
|
+
return [];
|
|
54
|
+
const attempts = content.flatMap((block) => block &&
|
|
55
|
+
block.type === "text" &&
|
|
56
|
+
typeof block.text === "string" &&
|
|
57
|
+
/^\s*<bash-(?:input|stdout|stderr)>/.test(block.text)
|
|
58
|
+
? claudeBindingAttempts(block.text, record.ordinal)
|
|
59
|
+
: []);
|
|
60
|
+
const time = typeof row.timestamp === "string" ? Date.parse(row.timestamp) : NaN;
|
|
61
|
+
return attempts.map((attempt, position) => ({
|
|
62
|
+
time: Number.isFinite(time) ? time : 0,
|
|
63
|
+
ordinal: record.ordinal,
|
|
64
|
+
position,
|
|
65
|
+
attempt,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=interaction.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Allow cursor drift within columns 1–5 and two rows of the prompt. */
|
|
2
|
+
export function claudePromptPosition(snapshot) {
|
|
3
|
+
if (!snapshot || snapshot.cursorX < 1 || snapshot.cursorX > 5)
|
|
4
|
+
return null;
|
|
5
|
+
const lines = snapshot.content.split("\n");
|
|
6
|
+
for (let row = Math.max(0, snapshot.cursorY - 2); row <= snapshot.cursorY + 2; row++) {
|
|
7
|
+
if (/^❯[^\S\r\n\u2028\u2029]*$/u.test(lines[row] ?? ""))
|
|
8
|
+
return row;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=prompt-readiness.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
/** Context is opaque text scoped to a single launch attempt. */
|
|
3
|
+
export async function checkClaudeReadyScript(script, current, context, timeout) {
|
|
4
|
+
const stdout = await new Promise((resolve, reject) => {
|
|
5
|
+
const child = execFile(script, [], { timeout: Math.max(1, timeout), maxBuffer: 1024 * 1024, killSignal: "SIGKILL" }, (error, stdout) => {
|
|
6
|
+
if (error)
|
|
7
|
+
reject(error);
|
|
8
|
+
else
|
|
9
|
+
resolve(stdout);
|
|
10
|
+
});
|
|
11
|
+
child.stdin?.on("error", reject);
|
|
12
|
+
child.stdin?.end(JSON.stringify({ current, context }));
|
|
13
|
+
});
|
|
14
|
+
const result = JSON.parse(stdout);
|
|
15
|
+
if (!result ||
|
|
16
|
+
typeof result !== "object" ||
|
|
17
|
+
!("ready" in result) ||
|
|
18
|
+
typeof result.ready !== "boolean" ||
|
|
19
|
+
!("context" in result) ||
|
|
20
|
+
typeof result.context !== "string") {
|
|
21
|
+
throw new Error("Claude readiness script must return { ready: boolean, context: string }");
|
|
22
|
+
}
|
|
23
|
+
return { ready: result.ready, context: result.context };
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=ready-script.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { lstat, readdir, rm, rmdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
+
import { attachCommand, TmuxLauncher } from "../../interaction/tmux-launcher.js";
|
|
5
|
+
import { SessionLaunchError, } from "../../management/session-manager.js";
|
|
6
|
+
import { exitSteps, resolveDirectory } from "../shared/session-management.js";
|
|
7
|
+
import { claudeBindingCommand, validClaudeId } from "./interaction.js";
|
|
8
|
+
import { claudePromptPosition } from "./prompt-readiness.js";
|
|
9
|
+
import { checkClaudeReadyScript } from "./ready-script.js";
|
|
10
|
+
/** Never deliver bootstrap input unless the native prompt is confirmed ready. */
|
|
11
|
+
export const BINDING_PROMPT_WAIT_MS = 15_000;
|
|
12
|
+
export class ClaudeSessionManager {
|
|
13
|
+
configDirectory;
|
|
14
|
+
tmux;
|
|
15
|
+
readyScript;
|
|
16
|
+
constructor(configDirectory, tmux = new TmuxLauncher(), readyScript) {
|
|
17
|
+
this.configDirectory = configDirectory;
|
|
18
|
+
this.tmux = tmux;
|
|
19
|
+
this.readyScript = readyScript;
|
|
20
|
+
}
|
|
21
|
+
interruptAfterBinding = true;
|
|
22
|
+
isValidNativeId = validClaudeId;
|
|
23
|
+
resolveDirectory = resolveDirectory;
|
|
24
|
+
async launch(options) {
|
|
25
|
+
if (options.resumeNativeId !== undefined && !this.isValidNativeId(options.resumeNativeId))
|
|
26
|
+
throw new Error("Invalid resume identity");
|
|
27
|
+
const target = await this.tmux.launch(options.operationId, options.workingDirectory, [
|
|
28
|
+
"env",
|
|
29
|
+
`CLAUDE_CONFIG_DIR=${this.configDirectory}`,
|
|
30
|
+
"claude",
|
|
31
|
+
...(options.resumeNativeId ? ["--resume", options.resumeNativeId] : []),
|
|
32
|
+
...(options.worktree ? ["--worktree"] : []),
|
|
33
|
+
...(options.yolo ? ["--dangerously-skip-permissions"] : []),
|
|
34
|
+
]);
|
|
35
|
+
try {
|
|
36
|
+
const deadline = Date.now() + BINDING_PROMPT_WAIT_MS;
|
|
37
|
+
let context = "";
|
|
38
|
+
let ready = false;
|
|
39
|
+
while (Date.now() < deadline) {
|
|
40
|
+
const snapshot = await this.tmux.transport.captureTerminalWithCursor(target);
|
|
41
|
+
if (Date.now() >= deadline)
|
|
42
|
+
break;
|
|
43
|
+
if (snapshot) {
|
|
44
|
+
const result = this.readyScript
|
|
45
|
+
? await checkClaudeReadyScript(this.readyScript, snapshot, context, deadline - Date.now())
|
|
46
|
+
: { ready: claudePromptPosition(snapshot) !== null, context: "" };
|
|
47
|
+
if (Date.now() >= deadline)
|
|
48
|
+
break;
|
|
49
|
+
context = result.context;
|
|
50
|
+
if (result.ready) {
|
|
51
|
+
ready = true;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
await delay(Math.min(1_000, Math.max(0, deadline - Date.now())));
|
|
56
|
+
}
|
|
57
|
+
if (!ready)
|
|
58
|
+
throw new Error("Claude Code prompt readiness was not confirmed before timeout. Attach to tmux to check the session.");
|
|
59
|
+
await this.tmux.transport.sendMessage(target, claudeBindingCommand(options.operationId, options.projectDirectory, options.taskName ?? null));
|
|
60
|
+
return target;
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new SessionLaunchError("unknown", error instanceof Error ? error.message : String(error), attachCommand(target.socketPath, `ahub-${options.operationId}`), target);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async delete(nativeId) {
|
|
67
|
+
if (!this.isValidNativeId(nativeId))
|
|
68
|
+
throw new Error("Invalid Claude session identity");
|
|
69
|
+
const projects = join(this.configDirectory, "projects");
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await readdir(projects, { withFileTypes: true });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error.code === "ENOENT")
|
|
76
|
+
return;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
for (const project of entries) {
|
|
80
|
+
if (!project.isDirectory())
|
|
81
|
+
continue;
|
|
82
|
+
const directory = join(projects, project.name);
|
|
83
|
+
const session = join(directory, nativeId);
|
|
84
|
+
try {
|
|
85
|
+
if ((await lstat(session)).isDirectory()) {
|
|
86
|
+
await rm(join(session, "subagents"), { recursive: true, force: true });
|
|
87
|
+
try {
|
|
88
|
+
await rmdir(session);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (!["ENOENT", "ENOTEMPTY", "EEXIST"].includes(error.code ?? ""))
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error.code !== "ENOENT")
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
await rm(join(directory, `${nativeId}.jsonl`), { force: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exitSteps = exitSteps;
|
|
104
|
+
}
|
|
105
|
+
export const createClaudeSessionManager = (configDirectory, socket, readyScript) => new ClaudeSessionManager(configDirectory, new TmuxLauncher(socket), readyScript);
|
|
106
|
+
//# sourceMappingURL=session-manager.js.map
|