dsh-prompt-vcs 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/cordis.patch.yml +4 -0
- package/lib/bin.d.ts +2 -0
- package/lib/bin.js +3 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +45 -0
- package/lib/client/index.d.ts +4 -0
- package/lib/client/index.js +16 -0
- package/lib/client/view.d.ts +2 -0
- package/lib/client/view.js +6 -0
- package/lib/diff.d.ts +7 -0
- package/lib/diff.js +72 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +17 -0
- package/lib/routes.d.ts +5 -0
- package/lib/routes.js +50 -0
- package/lib/store.d.ts +11 -0
- package/lib/store.js +36 -0
- package/lib/types.d.ts +23 -0
- package/lib/types.js +2 -0
- package/lib/vcs.d.ts +17 -0
- package/lib/vcs.js +80 -0
- package/lib/vcs.web.js +26 -0
- package/package.json +45 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hj01857655
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# dsh-prompt-vcs
|
|
2
|
+
|
|
3
|
+
Every change to your agent's instructions is recorded with a diff, and any change can be undone.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
dsh plugin --profile web add dsh-prompt-vcs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
- **Watch the instruction surface.** `AGENTS.md`, `CLAUDE.md`, skill files.
|
|
14
|
+
- **Snapshot on change.** Unified diff + attribution (user / plugin / agent) stored in `.promptvcs/history.jsonl`.
|
|
15
|
+
- **Timeline.** `promptvcs log` shows every change: when, what file, who changed it.
|
|
16
|
+
- **Diff.** `promptvcs diff <hash>` shows the unified diff for a specific change.
|
|
17
|
+
- **Rollback.** `promptvcs rollback <hash>` restores a file to its state before a change.
|
|
18
|
+
- **Panel.** Timeline with inline diffs and rollback button.
|
|
19
|
+
|
|
20
|
+
## CLI
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
dsh-prompt-vcs log # show change history
|
|
24
|
+
dsh-prompt-vcs diff <hash> # show diff for a change
|
|
25
|
+
dsh-prompt-vcs rollback <hash> # undo a change
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## License
|
|
29
|
+
|
|
30
|
+
MIT
|
package/cordis.patch.yml
ADDED
package/lib/bin.d.ts
ADDED
package/lib/bin.js
ADDED
package/lib/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function run(argv: string[]): number;
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { PromptVcs } from './vcs.js';
|
|
3
|
+
export function run(argv) {
|
|
4
|
+
const { positionals } = parseArgs({ args: argv, allowPositionals: true });
|
|
5
|
+
const vcs = new PromptVcs(process.cwd());
|
|
6
|
+
const cmd = positionals[0] ?? 'log';
|
|
7
|
+
switch (cmd) {
|
|
8
|
+
case 'log': {
|
|
9
|
+
const tl = vcs.timeline();
|
|
10
|
+
if (tl.length === 0) {
|
|
11
|
+
console.log('No changes recorded.');
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
for (const e of tl) {
|
|
15
|
+
console.log(`${e.hash} ${new Date(e.timestamp).toLocaleString()} ${e.changedBy} ${e.file} +${e.addedLines} -${e.removedLines}`);
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
case 'diff': {
|
|
20
|
+
const hash = positionals[1] ?? '';
|
|
21
|
+
const diff = vcs.getDiff(hash);
|
|
22
|
+
if (diff === null) {
|
|
23
|
+
console.log('Change not found.');
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
console.log(diff);
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
case 'rollback': {
|
|
30
|
+
const hash = positionals[1] ?? '';
|
|
31
|
+
const ok = vcs.rollback(hash);
|
|
32
|
+
if (!ok) {
|
|
33
|
+
console.log('Change not found.');
|
|
34
|
+
return 1;
|
|
35
|
+
}
|
|
36
|
+
console.log(`Rolled back ${hash}.`);
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
case 'help':
|
|
40
|
+
default:
|
|
41
|
+
console.log('Usage: dsh-prompt-vcs <command>');
|
|
42
|
+
console.log('Commands: log, diff <hash>, rollback <hash>');
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { renderPanel } from './view.js';
|
|
2
|
+
export const inject = ['@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-client-connection'];
|
|
3
|
+
export function apply(ctx) {
|
|
4
|
+
ctx.inject(inject, (settings, connection) => {
|
|
5
|
+
const s = settings;
|
|
6
|
+
const c = connection;
|
|
7
|
+
s.section('prompt-vcs', {
|
|
8
|
+
title: 'Prompt VCS',
|
|
9
|
+
render: async () => {
|
|
10
|
+
const res = await c.fetch('/api/vcs.panel');
|
|
11
|
+
const payload = await res.json();
|
|
12
|
+
return renderPanel(payload);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export function renderPanel(payload) {
|
|
2
|
+
const rows = payload.timeline
|
|
3
|
+
.map((e) => `<tr><td>${e.hash}</td><td>${new Date(e.timestamp).toLocaleString()}</td><td>${e.changedBy}</td><td>${e.file}</td><td>+${e.addedLines}</td><td>-${e.removedLines}</td></tr>`)
|
|
4
|
+
.join('');
|
|
5
|
+
return `<div class="vcs-panel"><h2>Prompt VCS</h2>${rows ? `<table><thead><tr><th>Hash</th><th>Date</th><th>By</th><th>File</th><th>+</th><th>-</th></tr></thead><tbody>${rows}</tbody></table>` : '<p>No changes recorded.</p>'}</div>`;
|
|
6
|
+
}
|
package/lib/diff.d.ts
ADDED
package/lib/diff.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** Unified diff generation (line-level). */
|
|
2
|
+
export function unifiedDiff(oldText, newText, oldPath = 'a', newPath = 'b') {
|
|
3
|
+
const oldLines = oldText === '' ? [] : oldText.split('\n');
|
|
4
|
+
const newLines = newText === '' ? [] : newText.split('\n');
|
|
5
|
+
const lines = [];
|
|
6
|
+
let added = 0;
|
|
7
|
+
let removed = 0;
|
|
8
|
+
// Header
|
|
9
|
+
lines.push(`--- ${oldPath}`);
|
|
10
|
+
lines.push(`+++ ${newPath}`);
|
|
11
|
+
// Simple LCS-based diff
|
|
12
|
+
const lcs = computeLCS(oldLines, newLines);
|
|
13
|
+
let oi = 0;
|
|
14
|
+
let ni = 0;
|
|
15
|
+
let li = 0;
|
|
16
|
+
while (oi < oldLines.length || ni < newLines.length) {
|
|
17
|
+
if (li < lcs.length && oi < oldLines.length && ni < newLines.length && oldLines[oi] === lcs[li] && newLines[ni] === lcs[li]) {
|
|
18
|
+
lines.push(` ${lcs[li]}`);
|
|
19
|
+
oi++;
|
|
20
|
+
ni++;
|
|
21
|
+
li++;
|
|
22
|
+
}
|
|
23
|
+
else if (oi < oldLines.length && (li >= lcs.length || oldLines[oi] !== lcs[li])) {
|
|
24
|
+
lines.push(`-${oldLines[oi]}`);
|
|
25
|
+
removed++;
|
|
26
|
+
oi++;
|
|
27
|
+
}
|
|
28
|
+
else if (ni < newLines.length && (li >= lcs.length || newLines[ni] !== lcs[li])) {
|
|
29
|
+
lines.push(`+${newLines[ni]}`);
|
|
30
|
+
added++;
|
|
31
|
+
ni++;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
oi++;
|
|
35
|
+
ni++;
|
|
36
|
+
li++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { diff: lines.join('\n'), added, removed };
|
|
40
|
+
}
|
|
41
|
+
function computeLCS(a, b) {
|
|
42
|
+
const m = a.length;
|
|
43
|
+
const n = b.length;
|
|
44
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
45
|
+
for (let i = m - 1; i >= 0; i--) {
|
|
46
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
47
|
+
if (a[i] === b[j]) {
|
|
48
|
+
dp[i][j] = dp[i + 1][j + 1] + 1;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const result = [];
|
|
56
|
+
let i = 0;
|
|
57
|
+
let j = 0;
|
|
58
|
+
while (i < m && j < n) {
|
|
59
|
+
if (a[i] === b[j]) {
|
|
60
|
+
result.push(a[i]);
|
|
61
|
+
i++;
|
|
62
|
+
j++;
|
|
63
|
+
}
|
|
64
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
j++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { PromptVcs } from './vcs.js';
|
|
3
|
+
export declare const name = "dsh-prompt-vcs";
|
|
4
|
+
export interface VcsService {
|
|
5
|
+
recordChange(file: string, oldContent: string, newContent: string, changedBy?: 'user' | 'plugin' | 'agent', pluginId?: string): ReturnType<PromptVcs['recordChange']>;
|
|
6
|
+
timeline(): ReturnType<PromptVcs['timeline']>;
|
|
7
|
+
getDiff(hash: string): ReturnType<PromptVcs['getDiff']>;
|
|
8
|
+
rollback(hash: string): boolean;
|
|
9
|
+
panel(): ReturnType<PromptVcs['panel']>;
|
|
10
|
+
}
|
|
11
|
+
export declare function apply(ctx: Context): void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { PromptVcs } from './vcs.js';
|
|
3
|
+
import { registerVcsRoutes } from './routes.js';
|
|
4
|
+
export const name = 'dsh-prompt-vcs';
|
|
5
|
+
export function apply(ctx) {
|
|
6
|
+
const root = resolve(process.cwd());
|
|
7
|
+
const vcs = new PromptVcs(root);
|
|
8
|
+
const service = {
|
|
9
|
+
recordChange: (file, oldContent, newContent, changedBy, pluginId) => vcs.recordChange(file, oldContent, newContent, changedBy, pluginId),
|
|
10
|
+
timeline: () => vcs.timeline(),
|
|
11
|
+
getDiff: (hash) => vcs.getDiff(hash),
|
|
12
|
+
rollback: (hash) => vcs.rollback(hash),
|
|
13
|
+
panel: () => vcs.panel(),
|
|
14
|
+
};
|
|
15
|
+
ctx.provide('promptVcs', service);
|
|
16
|
+
registerVcsRoutes(ctx, service);
|
|
17
|
+
}
|
package/lib/routes.d.ts
ADDED
package/lib/routes.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { VCS_PANEL_PATH } from './vcs.js';
|
|
2
|
+
export { VCS_PANEL_PATH };
|
|
3
|
+
export function registerVcsRoutes(ctx, vcs) {
|
|
4
|
+
ctx.inject(['connection'], (connectionCtx) => {
|
|
5
|
+
const connection = connectionCtx.connection;
|
|
6
|
+
connection.fetch.register({
|
|
7
|
+
path: VCS_PANEL_PATH,
|
|
8
|
+
methods: ['GET'],
|
|
9
|
+
requestBody: 'buffered',
|
|
10
|
+
fetch: () => Promise.resolve(Response.json(vcs.panel(), {
|
|
11
|
+
headers: { 'cache-control': 'no-store' },
|
|
12
|
+
})),
|
|
13
|
+
});
|
|
14
|
+
connection.fetch.register({
|
|
15
|
+
path: '/api/vcs.diff',
|
|
16
|
+
methods: ['GET'],
|
|
17
|
+
requestBody: 'buffered',
|
|
18
|
+
fetch: async (request) => {
|
|
19
|
+
const url = new URL(request.url);
|
|
20
|
+
const hash = url.searchParams.get('hash');
|
|
21
|
+
if (!hash)
|
|
22
|
+
return Response.json({ error: 'missing hash' }, { status: 400 });
|
|
23
|
+
const diff = vcs.getDiff(hash);
|
|
24
|
+
if (diff === null)
|
|
25
|
+
return Response.json({ error: 'not found' }, { status: 404 });
|
|
26
|
+
return Response.json({ diff }, { headers: { 'cache-control': 'no-store' } });
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
connection.fetch.register({
|
|
30
|
+
path: '/api/vcs.rollback',
|
|
31
|
+
methods: ['POST'],
|
|
32
|
+
requestBody: 'buffered',
|
|
33
|
+
fetch: async (request) => {
|
|
34
|
+
let body;
|
|
35
|
+
try {
|
|
36
|
+
body = await request.json();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return Response.json({ error: 'invalid JSON' }, { status: 400 });
|
|
40
|
+
}
|
|
41
|
+
if (!body.hash)
|
|
42
|
+
return Response.json({ error: 'missing hash' }, { status: 400 });
|
|
43
|
+
const ok = vcs.rollback(body.hash);
|
|
44
|
+
if (!ok)
|
|
45
|
+
return Response.json({ error: 'not found' }, { status: 404 });
|
|
46
|
+
return Response.json({ ok: true });
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
package/lib/store.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Change } from './types.js';
|
|
2
|
+
export declare class VcsStore {
|
|
3
|
+
private readonly projectDir;
|
|
4
|
+
private readonly vcsDir;
|
|
5
|
+
private readonly historyPath;
|
|
6
|
+
constructor(projectDir: string);
|
|
7
|
+
private ensureDir;
|
|
8
|
+
append(change: Change): void;
|
|
9
|
+
readAll(): Change[];
|
|
10
|
+
static hashChange(file: string, oldContent: string, newContent: string): string;
|
|
11
|
+
}
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, appendFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
export class VcsStore {
|
|
5
|
+
projectDir;
|
|
6
|
+
vcsDir;
|
|
7
|
+
historyPath;
|
|
8
|
+
constructor(projectDir) {
|
|
9
|
+
this.projectDir = projectDir;
|
|
10
|
+
this.vcsDir = join(projectDir, '.promptvcs');
|
|
11
|
+
this.historyPath = join(this.vcsDir, 'history.jsonl');
|
|
12
|
+
}
|
|
13
|
+
ensureDir() {
|
|
14
|
+
if (!existsSync(this.vcsDir))
|
|
15
|
+
mkdirSync(this.vcsDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
append(change) {
|
|
18
|
+
this.ensureDir();
|
|
19
|
+
appendFileSync(this.historyPath, JSON.stringify(change) + '\n', 'utf8');
|
|
20
|
+
}
|
|
21
|
+
readAll() {
|
|
22
|
+
if (!existsSync(this.historyPath))
|
|
23
|
+
return [];
|
|
24
|
+
return readFileSync(this.historyPath, 'utf8')
|
|
25
|
+
.trim()
|
|
26
|
+
.split('\n')
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.map((line) => JSON.parse(line));
|
|
29
|
+
}
|
|
30
|
+
static hashChange(file, oldContent, newContent) {
|
|
31
|
+
return createHash('sha256')
|
|
32
|
+
.update(`${file}:${oldContent}:${newContent}:${Date.now()}`)
|
|
33
|
+
.digest('hex')
|
|
34
|
+
.slice(0, 12);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface Change {
|
|
2
|
+
hash: string;
|
|
3
|
+
timestamp: number;
|
|
4
|
+
file: string;
|
|
5
|
+
changedBy: 'user' | 'plugin' | 'agent';
|
|
6
|
+
pluginId?: string;
|
|
7
|
+
addedLines: number;
|
|
8
|
+
removedLines: number;
|
|
9
|
+
diff: string;
|
|
10
|
+
oldContent: string;
|
|
11
|
+
newContent: string;
|
|
12
|
+
}
|
|
13
|
+
export interface TimelineEntry {
|
|
14
|
+
hash: string;
|
|
15
|
+
timestamp: number;
|
|
16
|
+
file: string;
|
|
17
|
+
changedBy: string;
|
|
18
|
+
addedLines: number;
|
|
19
|
+
removedLines: number;
|
|
20
|
+
}
|
|
21
|
+
export interface PanelPayload {
|
|
22
|
+
timeline: TimelineEntry[];
|
|
23
|
+
}
|
package/lib/types.js
ADDED
package/lib/vcs.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Change, TimelineEntry, PanelPayload } from './types.js';
|
|
2
|
+
export declare const VCS_PANEL_PATH = "/api/vcs.panel";
|
|
3
|
+
export declare class PromptVcs {
|
|
4
|
+
private projectDir;
|
|
5
|
+
private store;
|
|
6
|
+
constructor(projectDir: string);
|
|
7
|
+
/** Record a change to a file. Called when a watched file is modified. */
|
|
8
|
+
recordChange(file: string, oldContent: string, newContent: string, changedBy?: 'user' | 'plugin' | 'agent', pluginId?: string): Change | null;
|
|
9
|
+
/** Snapshot a file's current content, detecting changes since last snapshot. */
|
|
10
|
+
checkFile(file: string, lastKnownContent: string | null): Change | null;
|
|
11
|
+
timeline(): TimelineEntry[];
|
|
12
|
+
getDiff(hash: string): string | null;
|
|
13
|
+
/** Rollback a file to its state before the given change. */
|
|
14
|
+
rollback(hash: string): boolean;
|
|
15
|
+
panel(): PanelPayload;
|
|
16
|
+
static get watchedFiles(): string[];
|
|
17
|
+
}
|
package/lib/vcs.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { VcsStore } from './store.js';
|
|
4
|
+
import { unifiedDiff } from './diff.js';
|
|
5
|
+
export const VCS_PANEL_PATH = '/api/vcs.panel';
|
|
6
|
+
/** Files that shape agent behaviour — the instruction surface. */
|
|
7
|
+
const WATCHED_FILES = ['AGENTS.md', 'CLAUDE.md'];
|
|
8
|
+
export class PromptVcs {
|
|
9
|
+
projectDir;
|
|
10
|
+
store;
|
|
11
|
+
constructor(projectDir) {
|
|
12
|
+
this.projectDir = projectDir;
|
|
13
|
+
this.store = new VcsStore(projectDir);
|
|
14
|
+
}
|
|
15
|
+
/** Record a change to a file. Called when a watched file is modified. */
|
|
16
|
+
recordChange(file, oldContent, newContent, changedBy = 'user', pluginId) {
|
|
17
|
+
if (oldContent === newContent)
|
|
18
|
+
return null;
|
|
19
|
+
const rel = relative(this.projectDir, file);
|
|
20
|
+
const { diff, added, removed } = unifiedDiff(oldContent, newContent, rel, rel);
|
|
21
|
+
const hash = VcsStore.hashChange(file, oldContent, newContent);
|
|
22
|
+
const change = {
|
|
23
|
+
hash,
|
|
24
|
+
timestamp: Date.now(),
|
|
25
|
+
file: rel,
|
|
26
|
+
changedBy,
|
|
27
|
+
addedLines: added,
|
|
28
|
+
removedLines: removed,
|
|
29
|
+
diff,
|
|
30
|
+
oldContent,
|
|
31
|
+
newContent,
|
|
32
|
+
};
|
|
33
|
+
if (pluginId !== undefined)
|
|
34
|
+
change.pluginId = pluginId;
|
|
35
|
+
this.store.append(change);
|
|
36
|
+
return change;
|
|
37
|
+
}
|
|
38
|
+
/** Snapshot a file's current content, detecting changes since last snapshot. */
|
|
39
|
+
checkFile(file, lastKnownContent) {
|
|
40
|
+
const fullPath = join(this.projectDir, file);
|
|
41
|
+
if (!existsSync(fullPath))
|
|
42
|
+
return null;
|
|
43
|
+
const current = readFileSync(fullPath, 'utf8');
|
|
44
|
+
if (lastKnownContent === current)
|
|
45
|
+
return null;
|
|
46
|
+
return this.recordChange(fullPath, lastKnownContent ?? '', current);
|
|
47
|
+
}
|
|
48
|
+
timeline() {
|
|
49
|
+
return this.store.readAll().map((c) => ({
|
|
50
|
+
hash: c.hash,
|
|
51
|
+
timestamp: c.timestamp,
|
|
52
|
+
file: c.file,
|
|
53
|
+
changedBy: c.changedBy,
|
|
54
|
+
addedLines: c.addedLines,
|
|
55
|
+
removedLines: c.removedLines,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
getDiff(hash) {
|
|
59
|
+
const change = this.store.readAll().find((c) => c.hash === hash);
|
|
60
|
+
return change?.diff ?? null;
|
|
61
|
+
}
|
|
62
|
+
/** Rollback a file to its state before the given change. */
|
|
63
|
+
rollback(hash) {
|
|
64
|
+
const changes = this.store.readAll();
|
|
65
|
+
const change = changes.find((c) => c.hash === hash);
|
|
66
|
+
if (!change)
|
|
67
|
+
return false;
|
|
68
|
+
const fullPath = join(this.projectDir, change.file);
|
|
69
|
+
writeFileSync(fullPath, change.oldContent, 'utf8');
|
|
70
|
+
// Record the rollback itself
|
|
71
|
+
this.recordChange(fullPath, change.newContent, change.oldContent, 'user');
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
panel() {
|
|
75
|
+
return { timeline: this.timeline() };
|
|
76
|
+
}
|
|
77
|
+
static get watchedFiles() {
|
|
78
|
+
return WATCHED_FILES;
|
|
79
|
+
}
|
|
80
|
+
}
|
package/lib/vcs.web.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/client/view.tsx
|
|
2
|
+
function renderPanel(payload) {
|
|
3
|
+
const rows = payload.timeline.map((e) => `<tr><td>${e.hash}</td><td>${new Date(e.timestamp).toLocaleString()}</td><td>${e.changedBy}</td><td>${e.file}</td><td>+${e.addedLines}</td><td>-${e.removedLines}</td></tr>`).join("");
|
|
4
|
+
return `<div class="vcs-panel"><h2>Prompt VCS</h2>${rows ? `<table><thead><tr><th>Hash</th><th>Date</th><th>By</th><th>File</th><th>+</th><th>-</th></tr></thead><tbody>${rows}</tbody></table>` : "<p>No changes recorded.</p>"}</div>`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/client/index.tsx
|
|
8
|
+
var inject = ["@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-connection"];
|
|
9
|
+
function apply(ctx) {
|
|
10
|
+
ctx.inject(inject, (settings, connection) => {
|
|
11
|
+
const s = settings;
|
|
12
|
+
const c = connection;
|
|
13
|
+
s.section("prompt-vcs", {
|
|
14
|
+
title: "Prompt VCS",
|
|
15
|
+
render: async () => {
|
|
16
|
+
const res = await c.fetch("/api/vcs.panel");
|
|
17
|
+
const payload = await res.json();
|
|
18
|
+
return renderPanel(payload);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export {
|
|
24
|
+
apply,
|
|
25
|
+
inject
|
|
26
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-prompt-vcs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Every change to your agent's instructions is recorded with a diff, and any change can be undone.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"bin": { "dsh-prompt-vcs": "lib/bin.js" },
|
|
9
|
+
"exports": {
|
|
10
|
+
".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
|
|
11
|
+
"./client": { "types": "./lib/client.d.ts", "default": "./lib/vcs.web.js" },
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": ["lib", "cordis.patch.yml", "README.md", "CHANGELOG.md"],
|
|
15
|
+
"keywords": ["dsh", "dsh-plugin", "deepseek-harness", "prompt", "vcs", "version-control", "diff"],
|
|
16
|
+
"dsh": {
|
|
17
|
+
"bundle": { "patch": "./cordis.patch.yml" },
|
|
18
|
+
"client": {
|
|
19
|
+
"platform": "web",
|
|
20
|
+
"inject": ["@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-connection"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "hj01857655",
|
|
25
|
+
"repository": { "type": "git", "url": "git+https://github.com/hj01857655/dsh-prompt-vcs.git" },
|
|
26
|
+
"bugs": { "url": "https://github.com/hj01857655/dsh-prompt-vcs/issues" },
|
|
27
|
+
"homepage": "https://github.com/hj01857655/dsh-prompt-vcs#readme",
|
|
28
|
+
"engines": { "node": ">=20" },
|
|
29
|
+
"peerDependencies": { "@deepseek-ai/cordis": "^4.0.1" },
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
32
|
+
"@types/node": "^22.20.3",
|
|
33
|
+
"@types/react": "^19.3.0",
|
|
34
|
+
"esbuild": "^0.28.2",
|
|
35
|
+
"react": "^19.3.0",
|
|
36
|
+
"react-dom": "^19.3.0",
|
|
37
|
+
"typescript": "^5.6.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json && node scripts/bundle-client.mjs",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json",
|
|
42
|
+
"test": "tsc -p tsconfig.json && node --test tests/*.test.mjs",
|
|
43
|
+
"prepublishOnly": "npm run build && npm test"
|
|
44
|
+
}
|
|
45
|
+
}
|