Skip to content

Commit eb3bd46

Browse files
author
Ray
committed
feat: history archive + editor integration
History archive: - Add archive/restore/delete API endpoints for history entries - Add Active/Archive tabs with multi-select UX to history page - Archive stored in ~/.claude/history-archived.jsonl Editor integration: - Replace MDC with UEditor in MarkdownViewer (editable toggle) - Add textarea edit mode to CodeViewer - Wire up editing in rules, agents, commands, claude-md, settings, hooks - Skills remain read-only (no PUT endpoint) Security: - Async mutex for concurrent JSONL writes (server/utils/fileLock.ts) - Input validation with size limits (server/utils/validateTimestamps.ts) - Explicit delete_all flag required to empty archive - JSON parse errors surfaced to user instead of silent failure Code quality: - Extract atomicWrite, readJsonlEntries, useMultiSelect, historyFormat - Immutable patterns (spread/destructure instead of mutation) README updated with file-write warning and origin story.
1 parent 966a35e commit eb3bd46

26 files changed

+1047
-154
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Web dashboard for managing Claude Code configuration, sessions, and settings.
44

5+
![CC Hub Demo](https://github.com/user-attachments/assets/15070ade-4789-40cb-aa5d-f3330510365b)
6+
57
> **Requires an active [Claude Pro/Team/Enterprise subscription](https://claude.ai/pricing) with Claude Code access.** The AI assistant feature uses the Claude Agent SDK which authenticates through your Claude Code CLI login.
68
79
## Why?
@@ -61,6 +63,8 @@ cc-hub/
6163
| `~/.claude/trash/` | Archived sessions with `.meta.json` for restore |
6264
| `~/.claude/agents/` | Agent definitions (`.md` with frontmatter) |
6365
| `~/.claude/rules/` | Custom rules |
66+
| `~/.claude/history.jsonl` | CLI command history |
67+
| `~/.claude/history-archived.jsonl` | Archived history entries (created by CC Hub) |
6468
| `~/.claude/settings.json` | Global settings |
6569
| `~/.claude/mcp.json` | MCP server configuration |
6670

@@ -87,10 +91,24 @@ CC Hub **complements** the CLI — it doesn't replace it.
8791

8892
Both read/write the same `~/.claude/` files — naturally in sync.
8993

94+
## Heads Up: This Tool Writes to Real Files
95+
96+
CC Hub reads **and writes** to your `~/.claude/` directory. Edits you make (agents, rules, hooks, settings, CLAUDE.md) are saved directly to disk — there is no sandbox or dry-run mode.
97+
98+
This was a deliberate design choice. The original motivation: Claude Code CLI has no built-in way to delete or archive sessions. The only option was manually removing `.jsonl` files from `~/.claude/projects/`. CC Hub was built to fill that gap, and naturally expanded to cover editing config files too.
99+
100+
**What this means:**
101+
- Archive/delete/restore operations move or remove actual session files
102+
- Editing an agent, rule, or setting overwrites the real file (a `.bak` backup is created first)
103+
- History archive writes to `~/.claude/history-archived.jsonl`
104+
105+
If you want to be cautious, back up `~/.claude/` before first use.
106+
90107
## Security
91108

92109
- All `/api/**` routes require authentication (cookie or Bearer token)
93110
- Path traversal protection via `resolveClaudePath` + `safeJoin` + `assertSafeSegment`
111+
- Concurrent file writes serialized via async mutex (no race conditions)
94112
- Error messages sanitized (no filesystem path leakage)
95113
- `gray-matter` JS eval engine disabled
96114
- Agent SDK env vars restricted to safe allowlist

app/components/CodeViewer.vue

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,97 @@
11
<script setup lang="ts">
2-
const props = defineProps<{
2+
const props = withDefaults(defineProps<{
33
content: string
44
language?: string
5+
can_edit?: boolean
6+
}>(), {
7+
can_edit: false
8+
})
9+
10+
const emit = defineEmits<{
11+
save: [content: string]
512
}>()
613
714
const { copy, copied } = useClipboard()
815
16+
const is_editing = ref(false)
17+
const content_draft = ref('')
18+
919
const lines = computed(() => props.content.split('\n'))
20+
21+
function startEditing() {
22+
content_draft.value = props.content
23+
is_editing.value = true
24+
}
25+
26+
function handleSave() {
27+
emit('save', content_draft.value)
28+
is_editing.value = false
29+
}
30+
31+
function handleCancel() {
32+
is_editing.value = false
33+
}
1034
</script>
1135

1236
<template>
1337
<div class="relative overflow-auto">
14-
<div class="absolute top-2 right-2 z-10">
15-
<UButton
16-
:icon="copied ? 'i-lucide-check' : 'i-lucide-copy'"
17-
color="neutral"
18-
variant="ghost"
19-
size="xs"
20-
@click="copy(content)"
38+
<template v-if="is_editing">
39+
<textarea
40+
v-model="content_draft"
41+
class="w-full p-4 text-sm font-mono bg-transparent border border-(--ui-border) rounded-md resize-y focus:outline-none focus:ring-2 focus:ring-(--ui-primary)"
42+
style="min-height: 300px"
2143
/>
22-
</div>
23-
24-
<div class="p-4 font-mono text-sm">
25-
<table class="border-collapse">
26-
<tbody>
27-
<tr v-for="(line, idx) in lines" :key="idx">
28-
<td class="pr-4 text-right text-dimmed select-none align-top w-1">
29-
{{ idx + 1 }}
30-
</td>
31-
<td class="whitespace-pre-wrap break-words">{{ line }}</td>
32-
</tr>
33-
</tbody>
34-
</table>
35-
</div>
44+
<div class="flex gap-2 mt-2">
45+
<UButton
46+
icon="i-lucide-save"
47+
label="Save"
48+
color="primary"
49+
variant="soft"
50+
size="xs"
51+
@click="handleSave"
52+
/>
53+
<UButton
54+
icon="i-lucide-x"
55+
label="Cancel"
56+
color="neutral"
57+
variant="ghost"
58+
size="xs"
59+
@click="handleCancel"
60+
/>
61+
</div>
62+
</template>
63+
64+
<template v-else>
65+
<div class="absolute top-2 right-2 z-10 flex gap-1">
66+
<UButton
67+
v-if="can_edit"
68+
icon="i-lucide-pencil"
69+
color="neutral"
70+
variant="ghost"
71+
size="xs"
72+
@click="startEditing"
73+
/>
74+
<UButton
75+
:icon="copied ? 'i-lucide-check' : 'i-lucide-copy'"
76+
color="neutral"
77+
variant="ghost"
78+
size="xs"
79+
@click="copy(content)"
80+
/>
81+
</div>
82+
83+
<div class="p-4 font-mono text-sm">
84+
<table class="border-collapse">
85+
<tbody>
86+
<tr v-for="(line, idx) in lines" :key="idx">
87+
<td class="pr-4 text-right text-dimmed select-none align-top w-1">
88+
{{ idx + 1 }}
89+
</td>
90+
<td class="whitespace-pre-wrap break-words">{{ line }}</td>
91+
</tr>
92+
</tbody>
93+
</table>
94+
</div>
95+
</template>
3696
</div>
3797
</template>

app/components/MarkdownViewer.vue

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,78 @@
11
<script setup lang="ts">
2-
defineProps<{
2+
const props = withDefaults(defineProps<{
33
content_raw: string
4+
can_edit?: boolean
5+
}>(), {
6+
can_edit: false
7+
})
8+
9+
const emit = defineEmits<{
10+
save: [content: string]
411
}>()
512
6-
const is_raw = ref(false)
13+
const is_editing = ref(false)
14+
const content_draft = ref('')
15+
16+
const content_model = computed({
17+
get: () => is_editing.value ? content_draft.value : props.content_raw,
18+
set: (val: string) => { content_draft.value = val }
19+
})
20+
21+
function startEditing(): void {
22+
content_draft.value = props.content_raw
23+
is_editing.value = true
24+
}
25+
26+
function handleSave(): void {
27+
emit('save', content_draft.value)
28+
is_editing.value = false
29+
}
30+
31+
function handleCancel(): void {
32+
content_draft.value = props.content_raw
33+
is_editing.value = false
34+
}
735
</script>
836

937
<template>
1038
<div class="relative">
11-
<div class="absolute top-2 right-2 z-10">
39+
<!-- Action buttons -->
40+
<div v-if="can_edit && !is_editing" class="absolute top-2 right-2 z-10">
1241
<UButton
13-
:icon="is_raw ? 'lucide:eye' : 'lucide:code'"
42+
icon="i-lucide-pencil"
1443
color="neutral"
1544
variant="ghost"
1645
size="xs"
17-
@click="is_raw = !is_raw"
46+
@click="startEditing"
1847
/>
1948
</div>
2049

21-
<div v-if="is_raw" class="overflow-auto">
22-
<pre class="p-4 text-sm font-mono whitespace-pre-wrap break-words"><code>{{ content_raw }}</code></pre>
50+
<!-- Editor -->
51+
<UEditor
52+
v-model="content_model"
53+
content-type="markdown"
54+
:editable="is_editing"
55+
class="min-h-[200px]"
56+
/>
57+
58+
<!-- Save / Cancel bar -->
59+
<div v-if="is_editing" class="flex gap-2 mt-2 px-2 pb-2">
60+
<UButton
61+
icon="i-lucide-save"
62+
label="Save"
63+
color="primary"
64+
variant="soft"
65+
size="xs"
66+
@click="handleSave"
67+
/>
68+
<UButton
69+
icon="i-lucide-x"
70+
label="Cancel"
71+
color="neutral"
72+
variant="ghost"
73+
size="xs"
74+
@click="handleCancel"
75+
/>
2376
</div>
24-
<MDC v-else :value="content_raw" tag="article" class="prose prose-sm dark:prose-invert max-w-none p-4" />
2577
</div>
2678
</template>

app/components/agents/AgentDetail.vue

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
defineProps<{
2+
withDefaults(defineProps<{
33
agent: {
44
name_agent: string
55
description: string
@@ -8,6 +8,13 @@ defineProps<{
88
name_file: string
99
content_raw: string
1010
}
11+
can_edit?: boolean
12+
}>(), {
13+
can_edit: false
14+
})
15+
16+
const emit = defineEmits<{
17+
save: [content: string]
1118
}>()
1219
</script>
1320

@@ -39,7 +46,7 @@ defineProps<{
3946
:key="tool"
4047
color="neutral"
4148
variant="subtle"
42-
size="xs"
49+
size="sm"
4350
>
4451
{{ tool }}
4552
</UBadge>
@@ -49,6 +56,8 @@ defineProps<{
4956
<UCard>
5057
<MarkdownViewer
5158
:content_raw="agent.content_raw"
59+
:can_edit="can_edit"
60+
@save="emit('save', $event)"
5261
/>
5362
</UCard>
5463
</div>

0 commit comments

Comments
 (0)