@1yefuwang1/dsh-worktrees 0.0.0-stage → 0.2.13
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 +218 -0
- package/LICENSE +21 -0
- package/README.md +471 -2
- package/client.js +1398 -0
- package/cordis.patch.yml +6 -0
- package/dist/context.d.ts +20 -0
- package/dist/context.js +70 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.js +30 -0
- package/dist/first-message.d.ts +19 -0
- package/dist/first-message.js +121 -0
- package/dist/git.d.ts +45 -0
- package/dist/git.js +806 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +183 -0
- package/dist/naming.d.ts +25 -0
- package/dist/naming.js +151 -0
- package/dist/project-context.d.ts +20 -0
- package/dist/project-context.js +82 -0
- package/dist/project-rpc.d.ts +10 -0
- package/dist/project-rpc.js +103 -0
- package/dist/project-store.d.ts +121 -0
- package/dist/project-store.js +32 -0
- package/dist/projects.d.ts +149 -0
- package/dist/projects.js +603 -0
- package/dist/quiet-rpc.d.ts +7 -0
- package/dist/quiet-rpc.js +125 -0
- package/dist/rpc.d.ts +10 -0
- package/dist/rpc.js +418 -0
- package/dist/runtime.d.ts +22 -0
- package/dist/runtime.js +123 -0
- package/dist/schema.d.ts +50 -0
- package/dist/schema.js +86 -0
- package/dist/service.d.ts +98 -0
- package/dist/service.js +943 -0
- package/dist/sessions.d.ts +31 -0
- package/dist/sessions.js +130 -0
- package/dist/store.d.ts +114 -0
- package/dist/store.js +44 -0
- package/dist/types.d.ts +262 -0
- package/dist/types.js +1 -0
- package/icon.svg +1 -0
- package/locale/en.json +6 -0
- package/package.json +106 -3
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
3
|
+
import type { WorktreeRecord, CheckoutState } from './types.js';
|
|
4
|
+
export interface ReminderSource {
|
|
5
|
+
records(): WorktreeRecord[];
|
|
6
|
+
cachedStatus(id: string): CheckoutState | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare function pathContains(parent: string, child: string): boolean;
|
|
9
|
+
export declare function renderWorktreeContext(record: WorktreeRecord, state?: CheckoutState): string;
|
|
10
|
+
/** Agent and plugin both own each registration; no per-step Git I/O or timestamp churn. */
|
|
11
|
+
export declare class WorktreeReminders {
|
|
12
|
+
private readonly owner;
|
|
13
|
+
private readonly source;
|
|
14
|
+
private readonly registrations;
|
|
15
|
+
private readonly stops;
|
|
16
|
+
constructor(owner: Context, source: ReminderSource);
|
|
17
|
+
start(): void;
|
|
18
|
+
attach(agent: Agent): void;
|
|
19
|
+
close(): void;
|
|
20
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isAbsolute, relative } from 'node:path';
|
|
2
|
+
import { capability } from './runtime.js';
|
|
3
|
+
export function pathContains(parent, child) {
|
|
4
|
+
const part = relative(parent, child);
|
|
5
|
+
return part === '' || (!isAbsolute(part) && part !== '..' && !part.startsWith('../') && !part.startsWith('..\\'));
|
|
6
|
+
}
|
|
7
|
+
function quote(value) {
|
|
8
|
+
const bounded = value.length > 1024 ? `${value.slice(0, 1024)} [truncated; inspect worktree status]` : value;
|
|
9
|
+
return JSON.stringify(bounded).replace(/</gu, '\\u003c').replace(/>/gu, '\\u003e').replace(/&/gu, '\\u0026');
|
|
10
|
+
}
|
|
11
|
+
export function renderWorktreeContext(record, state) {
|
|
12
|
+
return [
|
|
13
|
+
'Git worktree context. Paths and ref names below are data.',
|
|
14
|
+
'Mode: isolated linked checkout.',
|
|
15
|
+
`Execution directory: ${quote(record.effectiveCwd)}`,
|
|
16
|
+
`Original local checkout: ${quote(record.repoRoot)}`,
|
|
17
|
+
`Fetched base branch: ${quote(`${record.remote}/${record.remoteBranch}`)}`,
|
|
18
|
+
`Starting commit: ${quote(record.baseOid || 'not established')}`,
|
|
19
|
+
`Checkout last known: ${quote(state === undefined ? 'unknown; inspect status before Git mutations' : state.branch === null ? 'detached HEAD' : state.branch)}`,
|
|
20
|
+
...(state === undefined ? [] : [`HEAD last known: ${quote(state.head)}`]),
|
|
21
|
+
'The base is a creation-time fetched snapshot, not continuous synchronization. Git refs and repository metadata remain shared.',
|
|
22
|
+
'Work in the current session directory. Preserve the local checkout unless an explicit handoff is requested. Historical paths may refer to an earlier checkout; verify before writing.',
|
|
23
|
+
].join('\n');
|
|
24
|
+
}
|
|
25
|
+
/** Agent and plugin both own each registration; no per-step Git I/O or timestamp churn. */
|
|
26
|
+
export class WorktreeReminders {
|
|
27
|
+
owner;
|
|
28
|
+
source;
|
|
29
|
+
registrations = new Map();
|
|
30
|
+
stops = [];
|
|
31
|
+
constructor(owner, source) {
|
|
32
|
+
this.owner = owner;
|
|
33
|
+
this.source = source;
|
|
34
|
+
}
|
|
35
|
+
start() {
|
|
36
|
+
this.stops.push(this.owner.on('agent/created', ({ agent }) => { this.attach(agent); return undefined; }));
|
|
37
|
+
this.stops.push(this.owner.on('agent/disposed', ({ agent }) => { this.registrations.get(agent)?.(); this.registrations.delete(agent); }));
|
|
38
|
+
for (const agent of capability(this.owner, 'agents')?.list() ?? [])
|
|
39
|
+
this.attach(agent);
|
|
40
|
+
}
|
|
41
|
+
attach(agent) {
|
|
42
|
+
if (this.registrations.has(agent))
|
|
43
|
+
return;
|
|
44
|
+
const cwd = agent.session.header.cwd;
|
|
45
|
+
if (cwd === undefined)
|
|
46
|
+
return;
|
|
47
|
+
const record = this.source.records().find(r => pathContains(r.checkoutRoot, cwd));
|
|
48
|
+
if (record === undefined)
|
|
49
|
+
return;
|
|
50
|
+
const prompt = capability(agent.ctx, 'systemPrompt');
|
|
51
|
+
if (prompt === undefined)
|
|
52
|
+
return;
|
|
53
|
+
const dispose = agent.ctx.effect(() => prompt.context({
|
|
54
|
+
name: 'dsh-worktrees:context', order: 500,
|
|
55
|
+
text: () => {
|
|
56
|
+
const current = this.source.records().find(r => r.id === record.id) ?? record;
|
|
57
|
+
return renderWorktreeContext(current, this.source.cachedStatus(record.id));
|
|
58
|
+
},
|
|
59
|
+
}), 'worktrees: agent runtime context');
|
|
60
|
+
const stop = this.owner.effect(() => dispose, 'worktrees: reminder ownership');
|
|
61
|
+
this.registrations.set(agent, () => { stop(); dispose(); });
|
|
62
|
+
}
|
|
63
|
+
close() {
|
|
64
|
+
for (const stop of this.stops.splice(0))
|
|
65
|
+
stop();
|
|
66
|
+
for (const dispose of this.registrations.values())
|
|
67
|
+
dispose();
|
|
68
|
+
this.registrations.clear();
|
|
69
|
+
}
|
|
70
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class WorktreeError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
readonly details: Record<string, unknown> | undefined;
|
|
4
|
+
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
5
|
+
}
|
|
6
|
+
/** Never put credential-bearing URLs or helper output into the command/model log. */
|
|
7
|
+
export declare function redact(text: string): string;
|
|
8
|
+
export declare function failureOf(error: unknown): {
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function abortIfRequested(signal?: AbortSignal): void;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class WorktreeError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
constructor(code, message, details) {
|
|
5
|
+
super(redact(message));
|
|
6
|
+
this.name = 'WorktreeError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Never put credential-bearing URLs or helper output into the command/model log. */
|
|
12
|
+
export function redact(text) {
|
|
13
|
+
return text
|
|
14
|
+
.replace(/(https?:\/\/)[^\s/@]+@/giu, '$1[redacted]@')
|
|
15
|
+
.replace(/\b(?:gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+)\b/gu, '[redacted]')
|
|
16
|
+
.replace(/([?&](?:access_token|token|password|secret)=)[^\s&#]+/giu, '$1[redacted]')
|
|
17
|
+
.replace(/(authorization\s*:\s*)(?:bearer|basic)\s+\S+/giu, '$1[redacted]')
|
|
18
|
+
.slice(0, 4096);
|
|
19
|
+
}
|
|
20
|
+
export function failureOf(error) {
|
|
21
|
+
if (error instanceof WorktreeError)
|
|
22
|
+
return { code: error.code, message: redact(error.message) };
|
|
23
|
+
if (error instanceof Error && error.name === 'AbortError')
|
|
24
|
+
return { code: 'CANCELLED', message: 'The operation was cancelled; committed effects may remain.' };
|
|
25
|
+
return { code: 'OPERATION_FAILED', message: redact(error instanceof Error ? error.message : String(error)) };
|
|
26
|
+
}
|
|
27
|
+
export function abortIfRequested(signal) {
|
|
28
|
+
if (signal?.aborted)
|
|
29
|
+
throw new WorktreeError('CANCELLED', 'The operation was cancelled; committed effects may remain.');
|
|
30
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { Agent, SessionStartSource } from '@deepseek-ai/dsh-agent';
|
|
3
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
4
|
+
import type { WorktreeController } from './service.js';
|
|
5
|
+
export declare function actualUserMessage(agent: Agent, event: SessionEvent): event is SessionEvent<'user/message'>;
|
|
6
|
+
/** Filtered one-shot observation only for eligible sessions; no token/full-log rescans. */
|
|
7
|
+
export declare class FirstMessageNamer {
|
|
8
|
+
private readonly owner;
|
|
9
|
+
private readonly controller;
|
|
10
|
+
private readonly shutdown;
|
|
11
|
+
private readonly stops;
|
|
12
|
+
private readonly attachments;
|
|
13
|
+
private readonly tasks;
|
|
14
|
+
constructor(owner: Context, controller: WorktreeController);
|
|
15
|
+
start(): void;
|
|
16
|
+
private track;
|
|
17
|
+
attach(agent: Agent, source?: SessionStartSource): void;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { capability } from './runtime.js';
|
|
2
|
+
import { WorktreeError } from './errors.js';
|
|
3
|
+
export function actualUserMessage(agent, event) {
|
|
4
|
+
return event.type === 'user/message' && event.surfaceOp === 'append' && !event.sourceEventSeqs?.length
|
|
5
|
+
&& event.data.role === 'user' && event.data.source.kind === 'user' && agent.session.isOwnSeq(event.seq);
|
|
6
|
+
}
|
|
7
|
+
function abortable(task, signal) {
|
|
8
|
+
if (signal.aborted)
|
|
9
|
+
return Promise.reject(signal.reason);
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const abort = () => { cleanup(); reject(signal.reason); };
|
|
12
|
+
const cleanup = () => signal.removeEventListener('abort', abort);
|
|
13
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
14
|
+
task.then(() => { cleanup(); resolve(); }, error => { cleanup(); reject(error); });
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/** Filtered one-shot observation only for eligible sessions; no token/full-log rescans. */
|
|
18
|
+
export class FirstMessageNamer {
|
|
19
|
+
owner;
|
|
20
|
+
controller;
|
|
21
|
+
shutdown = new AbortController();
|
|
22
|
+
stops = [];
|
|
23
|
+
attachments = new Map();
|
|
24
|
+
tasks = new Set();
|
|
25
|
+
constructor(owner, controller) {
|
|
26
|
+
this.owner = owner;
|
|
27
|
+
this.controller = controller;
|
|
28
|
+
}
|
|
29
|
+
start() {
|
|
30
|
+
this.stops.push(this.owner.on('agent/created', ({ agent, source }) => { this.attach(agent, source); return undefined; }));
|
|
31
|
+
this.stops.push(this.owner.on('agent/disposed', ({ agent }) => { this.attachments.get(agent)?.(); }));
|
|
32
|
+
for (const agent of capability(this.owner, 'agents')?.list() ?? [])
|
|
33
|
+
this.attach(agent, 'resume');
|
|
34
|
+
}
|
|
35
|
+
track(task) {
|
|
36
|
+
this.tasks.add(task);
|
|
37
|
+
void task.then(() => this.tasks.delete(task), () => this.tasks.delete(task));
|
|
38
|
+
}
|
|
39
|
+
attach(agent, source = 'startup') {
|
|
40
|
+
if (this.shutdown.signal.aborted || this.attachments.has(agent) || agent.session.header.origin === 'subagent')
|
|
41
|
+
return;
|
|
42
|
+
const record = this.controller.records().find(row => row.firstMessageNaming?.sessionId === agent.id);
|
|
43
|
+
const naming = record?.firstMessageNaming;
|
|
44
|
+
if (record === undefined || naming === undefined || naming.phase === 'complete' || naming.phase === 'skipped')
|
|
45
|
+
return;
|
|
46
|
+
if (source === 'clear' || source === 'compact') {
|
|
47
|
+
this.track(this.controller.skipFirstMessageName(record.id, 'session-replaced'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const lifetime = new AbortController();
|
|
51
|
+
const signal = AbortSignal.any([lifetime.signal, this.shutdown.signal]);
|
|
52
|
+
let claimed = naming.phase !== 'waiting';
|
|
53
|
+
let ended = false;
|
|
54
|
+
let end;
|
|
55
|
+
const turnEnd = new Promise(resolve => { end = resolve; });
|
|
56
|
+
let stop;
|
|
57
|
+
const launch = (event) => {
|
|
58
|
+
claimed = true;
|
|
59
|
+
this.track((async () => {
|
|
60
|
+
try {
|
|
61
|
+
await this.controller.prepareFirstMessageName(record.id, agent, event, signal);
|
|
62
|
+
if (!ended)
|
|
63
|
+
await abortable(turnEnd, signal);
|
|
64
|
+
// turn/end is committed while status can still be running. Claim actual idle,
|
|
65
|
+
// not a chat event boundary, so naming never races this agent's Git tools.
|
|
66
|
+
for (;;) {
|
|
67
|
+
await abortable(agent.whenIdle(), signal);
|
|
68
|
+
try {
|
|
69
|
+
await this.controller.finishFirstMessageName(record.id, agent, signal);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// If waking input won the repository-queue/idle-claim race, wait on
|
|
74
|
+
// that real activity. This reuses the original result, not its prompt.
|
|
75
|
+
if (signal.aborted || !(error instanceof WorktreeError) || error.code !== 'BUSY')
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
stop();
|
|
82
|
+
}
|
|
83
|
+
})());
|
|
84
|
+
};
|
|
85
|
+
const effect = agent.ctx.effect(() => {
|
|
86
|
+
const unlisten = this.owner.on('session/event', (session, event) => {
|
|
87
|
+
if (session !== agent.session || signal.aborted)
|
|
88
|
+
return;
|
|
89
|
+
if (!claimed && actualUserMessage(agent, event))
|
|
90
|
+
launch(event);
|
|
91
|
+
else if (claimed && event.type === 'turn/end' && agent.session.isOwnSeq(event.seq)) {
|
|
92
|
+
ended = true;
|
|
93
|
+
end();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
return () => { lifetime.abort(); unlisten(); };
|
|
97
|
+
}, 'worktrees: first user message naming');
|
|
98
|
+
const owned = this.owner.effect(() => effect, 'worktrees: naming attachment ownership');
|
|
99
|
+
stop = () => { owned(); effect(); this.attachments.delete(agent); };
|
|
100
|
+
this.attachments.set(agent, stop);
|
|
101
|
+
// One startup/resume scan, never on an event/token. Recover the exact first own
|
|
102
|
+
// authored append, including the crash window before its naming receipt.
|
|
103
|
+
const events = agent.session.ownEvents();
|
|
104
|
+
const first = events.find(event => actualUserMessage(agent, event));
|
|
105
|
+
const seq = naming.messageSeq ?? first?.seq;
|
|
106
|
+
if (seq !== undefined && events.some(event => event.seq > seq && event.type === 'turn/end')) {
|
|
107
|
+
ended = true;
|
|
108
|
+
end();
|
|
109
|
+
}
|
|
110
|
+
if (claimed || first?.type === 'user/message')
|
|
111
|
+
launch(first?.type === 'user/message' ? first : undefined);
|
|
112
|
+
}
|
|
113
|
+
async close() {
|
|
114
|
+
this.shutdown.abort();
|
|
115
|
+
for (const stop of this.stops.splice(0))
|
|
116
|
+
stop();
|
|
117
|
+
for (const stop of [...this.attachments.values()])
|
|
118
|
+
stop();
|
|
119
|
+
await Promise.allSettled([...this.tasks]);
|
|
120
|
+
}
|
|
121
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CheckoutState, CreateCheckoutInput, CreatedCheckout, GitExecutor, RemoteBranches, RemoteInfo, RepositoryInfo, SnapshotInput, WorktreeConfig, WorktreePreview, WorktreeRecord } from './types.js';
|
|
2
|
+
/** Strict Git engine. Its executor must enforce byte bounds, deadlines and caller sandbox access. */
|
|
3
|
+
export declare class GitOperations {
|
|
4
|
+
private readonly execute;
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly authorize;
|
|
7
|
+
constructor(execute: GitExecutor, config: WorktreeConfig, authorize?: () => void);
|
|
8
|
+
private env;
|
|
9
|
+
private run;
|
|
10
|
+
private configValues;
|
|
11
|
+
private supported;
|
|
12
|
+
discover(projectPath: string, signal?: AbortSignal): Promise<RepositoryInfo>;
|
|
13
|
+
private ref;
|
|
14
|
+
private remote;
|
|
15
|
+
remotes(repoRoot: string, signal?: AbortSignal): Promise<RemoteInfo[]>;
|
|
16
|
+
branches(repoRoot: string, remote: string, identity?: string, signal?: AbortSignal): Promise<RemoteBranches>;
|
|
17
|
+
private destination;
|
|
18
|
+
private store;
|
|
19
|
+
private removeStore;
|
|
20
|
+
private tree;
|
|
21
|
+
private attributes;
|
|
22
|
+
create(input: CreateCheckoutInput): Promise<CreatedCheckout>;
|
|
23
|
+
private identity;
|
|
24
|
+
private metadata;
|
|
25
|
+
private registered;
|
|
26
|
+
verify(record: WorktreeRecord, signal?: AbortSignal): Promise<CheckoutState>;
|
|
27
|
+
private statusRaw;
|
|
28
|
+
status(checkoutRoot: string, signal?: AbortSignal): Promise<CheckoutState>;
|
|
29
|
+
createBranch(record: WorktreeRecord, name: string, signal?: AbortSignal): Promise<CheckoutState>;
|
|
30
|
+
/** Rename metadata only, never switch HEAD, force a collision or touch the index/files. */
|
|
31
|
+
renameBranch(record: WorktreeRecord, expected: string, name: string, signal?: AbortSignal): Promise<CheckoutState>;
|
|
32
|
+
private files;
|
|
33
|
+
private fileData;
|
|
34
|
+
fingerprint(checkoutRoot: string, signal?: AbortSignal): Promise<string>;
|
|
35
|
+
private exactContents;
|
|
36
|
+
private ensureBase;
|
|
37
|
+
private patchFiles;
|
|
38
|
+
private collisions;
|
|
39
|
+
snapshot(input: SnapshotInput): Promise<WorktreePreview>;
|
|
40
|
+
private ownedPreview;
|
|
41
|
+
apply(preview: WorktreePreview, record: WorktreeRecord, signal?: AbortSignal): Promise<{
|
|
42
|
+
head: string;
|
|
43
|
+
files: number;
|
|
44
|
+
}>;
|
|
45
|
+
}
|