@vidge/dsh-agent-hub 0.1.0-rc1

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.
Files changed (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -0
  3. package/README.zh.md +115 -0
  4. package/cordis.patch.yml +22 -0
  5. package/lib/client.js +1494 -0
  6. package/lib/index.js +5045 -0
  7. package/lib/invariant.js +96 -0
  8. package/lib/types/client/LoopEngineComposerSelect.d.ts +73 -0
  9. package/lib/types/client/LoopEngineSection.d.ts +43 -0
  10. package/lib/types/client/engine-rpc.d.ts +74 -0
  11. package/lib/types/client/index.d.ts +28 -0
  12. package/lib/types/client/locales.d.ts +44 -0
  13. package/lib/types/client/session-location.d.ts +63 -0
  14. package/lib/types/client/store.d.ts +58 -0
  15. package/lib/types/commands.d.ts +69 -0
  16. package/lib/types/driver-core/context-files.d.ts +62 -0
  17. package/lib/types/driver-core/ownership.d.ts +40 -0
  18. package/lib/types/driver-core/permission-knobs.d.ts +26 -0
  19. package/lib/types/driver-core/prompt.d.ts +23 -0
  20. package/lib/types/driver-core/skill-inject.d.ts +59 -0
  21. package/lib/types/engine-claude/agent.d.ts +116 -0
  22. package/lib/types/engine-claude/loop.d.ts +99 -0
  23. package/lib/types/engine-claude/mapping.d.ts +84 -0
  24. package/lib/types/engine-claude/permission.d.ts +41 -0
  25. package/lib/types/engine-claude/process.d.ts +59 -0
  26. package/lib/types/engine-claude/provider-env.d.ts +50 -0
  27. package/lib/types/engine-claude/sdk.d.ts +101 -0
  28. package/lib/types/engine-claude/types.d.ts +28 -0
  29. package/lib/types/engine-codex/agent.d.ts +109 -0
  30. package/lib/types/engine-codex/appserver/client.d.ts +49 -0
  31. package/lib/types/engine-codex/appserver/mapping.d.ts +67 -0
  32. package/lib/types/engine-codex/appserver/thread.d.ts +66 -0
  33. package/lib/types/engine-codex/appserver/types.d.ts +215 -0
  34. package/lib/types/engine-codex/loop.d.ts +92 -0
  35. package/lib/types/engine-codex/permission.d.ts +32 -0
  36. package/lib/types/engine-codex/skills.d.ts +29 -0
  37. package/lib/types/engine-codex/types.d.ts +19 -0
  38. package/lib/types/engine-pi/agent.d.ts +125 -0
  39. package/lib/types/engine-pi/loop.d.ts +96 -0
  40. package/lib/types/engine-pi/permission.d.ts +43 -0
  41. package/lib/types/engine-pi/rpc/client.d.ts +105 -0
  42. package/lib/types/engine-pi/rpc/mapping.d.ts +37 -0
  43. package/lib/types/engine-pi/rpc/types.d.ts +235 -0
  44. package/lib/types/engine-pi/skills.d.ts +55 -0
  45. package/lib/types/engine-pi/types.d.ts +27 -0
  46. package/lib/types/engine-record.d.ts +124 -0
  47. package/lib/types/index.d.ts +138 -0
  48. package/lib/types/invariant.d.ts +23 -0
  49. package/lib/types/llm-compat.d.ts +32 -0
  50. package/lib/types/namespace.d.ts +19 -0
  51. package/lib/types/patch-manager.d.ts +78 -0
  52. package/lib/types/router.d.ts +189 -0
  53. package/lib/types/rpc.d.ts +113 -0
  54. package/lib/types/settings.d.ts +29 -0
  55. package/lib/types/skills.d.ts +93 -0
  56. package/package.json +107 -0
@@ -0,0 +1,96 @@
1
+ // src/patch-manager.ts
2
+ var MANAGED_BLOCK_BEGIN = "# -- dsh-loop-engine managed block";
3
+ var MANAGED_BLOCK_END = "# -- /dsh-loop-engine managed block --";
4
+ var END_MARKER_LINE = `${MANAGED_BLOCK_END}
5
+ `;
6
+ function renderManagedBlock() {
7
+ return [
8
+ `${MANAGED_BLOCK_BEGIN} --`,
9
+ "- id: agent-loop",
10
+ " disabled: true",
11
+ END_MARKER_LINE
12
+ ].join("\n");
13
+ }
14
+ function hasManagedBlock(text) {
15
+ return text.includes(MANAGED_BLOCK_BEGIN);
16
+ }
17
+ function managedSpan(text) {
18
+ const begin = text.indexOf(MANAGED_BLOCK_BEGIN);
19
+ if (begin === -1) return { head: text, tail: "", present: false, blankBefore: false };
20
+ const afterBegin = begin + MANAGED_BLOCK_BEGIN.length;
21
+ const endAt = text.indexOf(MANAGED_BLOCK_END, afterBegin);
22
+ const spanEnd = endAt === -1 ? text.length : endAt + END_MARKER_LINE.length;
23
+ const before = text.slice(0, begin);
24
+ const blankBefore = before.endsWith("\n\n");
25
+ return {
26
+ head: blankBefore ? before.slice(0, -1) : before,
27
+ tail: text.slice(spanEnd),
28
+ present: true,
29
+ blankBefore
30
+ };
31
+ }
32
+ function ensureTrailingNewline(text) {
33
+ return text.endsWith("\n") ? text : `${text}
34
+ `;
35
+ }
36
+ var EMPTY_FLOW_SEQ_RE = /^[ \t]*\[\][ \t]*$/;
37
+ function isEmptyFlowSeqDocument(text) {
38
+ let sawEmptySeq = false;
39
+ for (const line of text.split("\n")) {
40
+ const trimmed = line.trim();
41
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
42
+ if (!sawEmptySeq && EMPTY_FLOW_SEQ_RE.test(line)) {
43
+ sawEmptySeq = true;
44
+ continue;
45
+ }
46
+ return false;
47
+ }
48
+ return sawEmptySeq;
49
+ }
50
+ function stripEmptyFlowSeq(text) {
51
+ const lines = text.split("\n");
52
+ const at = lines.findIndex((line) => EMPTY_FLOW_SEQ_RE.test(line));
53
+ lines.splice(at, 1);
54
+ return lines.join("\n");
55
+ }
56
+ function applyManagedBlock(text) {
57
+ const block = renderManagedBlock();
58
+ const span = managedSpan(text);
59
+ if (!span.present) {
60
+ const base = ensureTrailingNewline(
61
+ isEmptyFlowSeqDocument(text) ? stripEmptyFlowSeq(text).trimEnd() : text
62
+ );
63
+ return `${base}
64
+ ${block}`;
65
+ }
66
+ return `${span.head}${span.blankBefore ? "\n" : ""}${block}${span.tail}`;
67
+ }
68
+
69
+ // src/invariant.ts
70
+ var PACKAGE_NAME = "dsh-agent-hub";
71
+ var name = "loop-engine-invariant";
72
+ var inject = ["invariants"];
73
+ var install = (ctx, fail) => {
74
+ void ctx;
75
+ const seed = "# dsh profile patch layer\n";
76
+ const applied = applyManagedBlock(seed);
77
+ if (applyManagedBlock(applied) !== applied) fail("managed-block application is not a fixed point");
78
+ if (!hasManagedBlock(applied)) fail("managed block must be present after application");
79
+ if (!renderManagedBlock().includes("- id: agent-loop")) fail("managed block must disable the base agent-loop row");
80
+ if (!renderManagedBlock().includes("disabled: true")) fail("managed block must disable, not merely target, the base row");
81
+ const legacy = `${seed}
82
+ ${MANAGED_BLOCK_BEGIN}: codex --
83
+ - id: agent-loop
84
+ disabled: true
85
+ ${MANAGED_BLOCK_END}
86
+ `;
87
+ const upgraded = applyManagedBlock(legacy);
88
+ if (upgraded !== applied) fail("a legacy engine-tagged block must upgrade to the permanent block");
89
+ };
90
+ var apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
91
+ export {
92
+ apply,
93
+ inject,
94
+ name
95
+ };
96
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Composer loop-engine control, registered at the `conversation.input.right`
3
+ * seat so it sits immediately left of the model select in the composer's tool
4
+ * row.
5
+ *
6
+ * It shows **this session's actual engine**, read from the node half over the
7
+ * plugin's own RPC channel. That distinction is the whole point of this
8
+ * component. A session's engine is chosen inside `createAgent`, which the
9
+ * harness fires eagerly when a session is *opened* — before a user can click
10
+ * anything — so a control backed by the settings value would name "the last
11
+ * thing picked anywhere" while the session ran something else. That was a real,
12
+ * reported defect: the composer read "In-process engine" while Claude Code
13
+ * answered.
14
+ *
15
+ * The seat never hides itself over a failed read. When the engine cannot be
16
+ * resolved it says so and stays clickable: the picker is the only route to
17
+ * another engine, so removing it would strand the user with no control and no
18
+ * explanation — which is precisely how this looked when the channel silently
19
+ * failed to register.
20
+ *
21
+ * Because the engine is fixed before the control is reachable, picking a
22
+ * different one cannot change this session. It instead **creates a new one**:
23
+ * the seat mints a session id, reserves the engine for it over the channel, and
24
+ * asks the host to create exactly that id — bypassing `connectWorkspace`, which
25
+ * would hand back the current blank session and defeat the purpose. The
26
+ * abandoned blank session is left alone; discarding a session is the user's
27
+ * call, not the picker's.
28
+ *
29
+ * Styling is token-driven inline styles like the section (the client-module
30
+ * bundle is esbuild-built without a CSS loader).
31
+ * @module dsh-agent-hub/client/composer
32
+ */
33
+ import { type JSX } from 'react';
34
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
35
+ import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
36
+ import type { LoopEngineStore, LoopEngineState } from './store.ts';
37
+ import type { EngineRpc } from './engine-rpc.ts';
38
+ import type { LoopEngineId } from '../settings.ts';
39
+ import type { en } from './locales.ts';
40
+ /** Creates a session on a caller-chosen id and brings it to the foreground. */
41
+ export interface SessionSwitcher {
42
+ /**
43
+ * Create a session carrying a specific engine, and open it.
44
+ * @param engine - engine the new session must run on.
45
+ * @returns whether a new session was created and opened.
46
+ */
47
+ startSessionOn(engine: LoopEngineId): Promise<boolean>;
48
+ }
49
+ /** Injected dependencies of {@link LoopEngineComposerSelect} (slot `inject`). */
50
+ export interface LoopEngineComposerSelectInjected {
51
+ /** The settings store, for the composer-visibility toggle and the default engine. */
52
+ controller: LoopEngineStore;
53
+ /** Reads a session's true engine from the node half. */
54
+ rpc: EngineRpc;
55
+ /** Creates and opens a session bound to a chosen engine. */
56
+ switcher: SessionSwitcher;
57
+ hooks: {
58
+ /** Engine snapshot bound by the UI renderer as useSnapshot. */
59
+ snapshot: SnapshotStore<LoopEngineState>;
60
+ };
61
+ /** Composer copy bound to the loop engine dictionaries. */
62
+ t: (key: keyof typeof en) => string;
63
+ }
64
+ /** Props delivered by the slot outlet (the renderer erases the share boundary). */
65
+ export type LoopEngineComposerSelectProps = Partial<InjectFace<LoopEngineComposerSelectInjected>>;
66
+ /**
67
+ * Render the composer's loop-engine seat.
68
+ * @param props - composed slot props.
69
+ * @returns the control naming this session's engine, or null when the settings
70
+ * toggle hides it.
71
+ */
72
+ export declare function LoopEngineComposerSelect(props: LoopEngineComposerSelectProps): JSX.Element | null;
73
+ //# sourceMappingURL=LoopEngineComposerSelect.d.ts.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Loop engine settings section component: one dropdown choosing the **default**
3
+ * agent loop engine, backed by the duplicated settings scope through the inject
4
+ * face.
5
+ *
6
+ * This is deliberately not a switch for "the current engine". A session's
7
+ * engine is fixed inside `createAgent`, which the harness fires eagerly when a
8
+ * session is opened, so nothing set here can reach a session that already
9
+ * exists. It applies to sessions created later that do not reserve an engine of
10
+ * their own — the composer seat is where a session's engine is chosen. That
11
+ * split mirrors the harness's own `agentPreset`: a per-session control, plus a
12
+ * settings entry that only sets the default.
13
+ *
14
+ * Picking commits immediately; there is no confirmation dialog and no
15
+ * `location.reload()`, because every engine is resident behind one router.
16
+ *
17
+ * Styling is token-driven like the rest of the settings shell (`--dsw-*`
18
+ * aliases), with the picker rendered through the shared `Menu` primitive. The
19
+ * client-module bundle is esbuild-built without a CSS loader, so the section
20
+ * shell uses token-based inline styles instead of a CSS module.
21
+ * @module dsh-agent-hub/client
22
+ */
23
+ import { type JSX } from 'react';
24
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
25
+ import type { InjectFace } from '@deepseek-ai/dsh-client-ui-slots';
26
+ import type { LoopEngineStore, LoopEngineState } from './store.ts';
27
+ import type { en } from './locales.ts';
28
+ /** Injected dependencies of {@link LoopEngineSection} (slot `inject`). */
29
+ export interface LoopEngineSectionInjected {
30
+ /** The selection store (loaded on mount, refreshed by scope pushes). */
31
+ controller: LoopEngineStore;
32
+ hooks: {
33
+ /** Section snapshot bound by the UI renderer as useSnapshot. */
34
+ snapshot: SnapshotStore<LoopEngineState>;
35
+ };
36
+ /** Section copy. */
37
+ t: (key: keyof typeof en) => string;
38
+ }
39
+ /** Props delivered by the slot outlet (the renderer erases the share boundary). */
40
+ export type LoopEngineSectionProps = Partial<InjectFace<LoopEngineSectionInjected>>;
41
+ /** Render the engine dropdown plus the binding notice and the composer toggle. */
42
+ export declare function LoopEngineSection(props: LoopEngineSectionProps): JSX.Element;
43
+ //# sourceMappingURL=LoopEngineSection.d.ts.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Browser half of the `/loop-engine` Connection RPC channel.
3
+ *
4
+ * The composer needs two things the rest of the client cannot give it: the
5
+ * engine a session is *actually* running (it lives in a node-side sidecar, not
6
+ * in the log, header, or settings), and a way to claim an engine for a session
7
+ * *before* that session is created (agent creation is eager, so there is no
8
+ * later moment). Both are one `connection.rpc.call` away.
9
+ *
10
+ * The Connection is provided conditionally, so every method degrades to
11
+ * `undefined` rather than throwing when it is absent — the composer then falls
12
+ * back to a read-only label.
13
+ *
14
+ * @module dsh-agent-hub/client/engine-rpc
15
+ */
16
+ import { type LoopEngineId } from '../namespace.ts';
17
+ /** Carrier-neutral result the Connection RPC transport returns. */
18
+ type RpcResult<T> = {
19
+ readonly ok: true;
20
+ readonly value: T;
21
+ } | {
22
+ readonly ok: false;
23
+ readonly error: {
24
+ readonly code: string;
25
+ readonly message: string;
26
+ };
27
+ };
28
+ /**
29
+ * The browser Connection surface this module borrows, declared structurally so
30
+ * the client bundle needs no value import from the connection package.
31
+ */
32
+ export interface ConnectionLike {
33
+ readonly rpc: {
34
+ call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<RpcResult<unknown>>;
35
+ };
36
+ }
37
+ /** Calls the plugin's own channel, tolerating a profile that has no Connection. */
38
+ export declare class EngineRpc {
39
+ private readonly connection;
40
+ /**
41
+ * @param connection - the browser connection, or undefined when the profile has none.
42
+ */
43
+ constructor(connection: ConnectionLike | undefined);
44
+ /** Whether the channel is reachable; false makes the composer read-only. */
45
+ get available(): boolean;
46
+ /**
47
+ * Claim an engine for a session id the caller is about to create.
48
+ *
49
+ * Must be awaited before the session is created: the host resolves the
50
+ * reservation inside `createAgent`, which the harness fires eagerly at
51
+ * session-open.
52
+ *
53
+ * @param sessionId - id the caller will pass to `sessions.create`.
54
+ * @param engine - engine that session must run on.
55
+ * @returns whether the reservation landed.
56
+ */
57
+ bind(sessionId: string, engine: LoopEngineId): Promise<boolean>;
58
+ /**
59
+ * Read the engine a session is actually bound to.
60
+ * @param sessionId - the session to look up.
61
+ * @param signal - abort when the seat unmounts or the session changes.
62
+ * @returns the engine, or undefined when unknown or unreachable.
63
+ */
64
+ resolve(sessionId: string, signal?: AbortSignal): Promise<LoopEngineId | undefined>;
65
+ /**
66
+ * Issue one call, folding transport and endpoint failures into `undefined`.
67
+ *
68
+ * A failure here is never worth breaking the composer over: the engine is a
69
+ * label and a convenience, and the session works regardless.
70
+ */
71
+ private call;
72
+ }
73
+ export {};
74
+ //# sourceMappingURL=engine-rpc.d.ts.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Loop engine settings plugin, browser half. Registers the "Loop engine"
3
+ * page under the settings section slot once the settings shell declares it,
4
+ * binding one store to the duplicated `agent-loop-engine` settings scope.
5
+ * Export discipline: packages/client/AGENTS.md.
6
+ * @module dsh-agent-hub/client
7
+ */
8
+ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
9
+ import { type LoopEngineKey } from './locales.ts';
10
+ export type { LoopEngineSectionInjected, LoopEngineSectionProps } from './LoopEngineSection.tsx';
11
+ export type { LoopEngineComposerSelectInjected, LoopEngineComposerSelectProps, SessionSwitcher } from './LoopEngineComposerSelect.tsx';
12
+ export type { LoopEngineState } from './store.ts';
13
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
14
+ interface LocaleNamespaceMap {
15
+ /** The Loop engine settings page copy. */
16
+ 'settings.loop-engine': LoopEngineKey;
17
+ }
18
+ }
19
+ /** Required services (cordis fiber inject). The target slot is declared by
20
+ * ui-settings' apply; registration depends on it through `slots.inject()`. */
21
+ export declare const inject: string[];
22
+ /**
23
+ * Register the Loop engine section once the `settings.section` declaration is
24
+ * on the ledger and bind its store to the duplicated settings scope.
25
+ * @param ctx - client root context.
26
+ */
27
+ export declare function apply(ctx: ClientContext): void;
28
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Loop engine settings page copy (Chinese product copy; comments in English).
3
+ * @module dsh-agent-hub/client/locales
4
+ */
5
+ /** Copy keys of the loop engine settings page. */
6
+ export interface LoopEngineKey {
7
+ /** Settings navigation label. */
8
+ nav: string;
9
+ /** Panel description under the title. */
10
+ description: string;
11
+ /** Option label: the default in-process loop driver. */
12
+ engineInProcess: string;
13
+ /** Option label: the Claude Code CLI driver. */
14
+ engineClaudeCode: string;
15
+ /** Option label: the Codex CLI driver. */
16
+ engineCodex: string;
17
+ /** Option label: the Pi CLI driver. */
18
+ enginePi: string;
19
+ /** Settings toggle: show the engine picker in the chat page composer. */
20
+ showInComposerLabel: string;
21
+ /** Unavailable-state message. */
22
+ unavailable: string;
23
+ /** Notice explaining that the choice binds at session creation. */
24
+ switchNotice: string;
25
+ /** Composer tooltip: picking a different engine starts a new session. */
26
+ switchCreatesSession: string;
27
+ /** Tooltip of the read-only composer seat: this session's engine is already fixed. */
28
+ boundNotice: string;
29
+ /** Composer label while this session's engine is still being read. */
30
+ engineResolving: string;
31
+ /** Composer label when this session's engine could not be read. */
32
+ engineUnknown: string;
33
+ /** Composer tooltip when the engine could not be read: picking still starts a session. */
34
+ engineUnknownNotice: string;
35
+ /** Saving state label. */
36
+ saving: string;
37
+ /** Notice shown while the Claude Code engine owns the slot: model selection is native. */
38
+ claudeModelNotice: string;
39
+ }
40
+ /** Simplified Chinese copy. */
41
+ export declare const zh: Record<keyof LoopEngineKey, string>;
42
+ /** English copy. */
43
+ export declare const en: Record<keyof LoopEngineKey, string>;
44
+ //# sourceMappingURL=locales.d.ts.map
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Where a newly created session should be placed.
3
+ *
4
+ * Split out of the client entry because the entry cannot be imported under
5
+ * vitest — it pulls the composer components, which expect the browser module
6
+ * loader — while this decision is pure and worth a regression test of its own.
7
+ * It has no imports so the browser bundle can take it as-is.
8
+ *
9
+ * @module dsh-agent-hub/client/session-location
10
+ */
11
+ /** The subset of a session summary this decision reads. */
12
+ export interface SessionSummaryLike {
13
+ readonly cwd?: string;
14
+ }
15
+ /** The subset of a workspace view this decision reads. */
16
+ export interface WorkspaceViewLike {
17
+ readonly workspaceId: string;
18
+ readonly sessionIds: readonly string[];
19
+ }
20
+ /** The `sessions.list` snapshot fields this decision reads. */
21
+ export interface SessionListLike {
22
+ readonly current?: string;
23
+ readonly byId: Record<string, SessionSummaryLike | undefined>;
24
+ }
25
+ /**
26
+ * Location argument for `sessions.create`.
27
+ *
28
+ * The two fields are mutually exclusive by host contract, not by style: the
29
+ * create command rejects a request carrying both with `gateway/bad-request`
30
+ * (`api/session-controller/src/commands.ts:72-74`). Hence a union of two
31
+ * one-field shapes rather than an object with both optional.
32
+ */
33
+ export type SessionLocation = {
34
+ readonly workspaceId: string;
35
+ } | {
36
+ readonly cwd: string;
37
+ } | Record<string, never>;
38
+ /**
39
+ * Decide where the engine switcher's new session goes.
40
+ *
41
+ * Workspace membership is what the user actually chose, and it is *not*
42
+ * implied by the directory: `create` attaches a session to a workspace only on
43
+ * the `workspaceId` branch (`commands.ts:96-106`). Passing the current
44
+ * session's `cwd` instead therefore produces a session in the right directory
45
+ * that belongs to no workspace, and the UI asks the user to pick a workspace
46
+ * all over again — which is exactly the bug this function exists to prevent.
47
+ *
48
+ * Membership is held by the workspace rather than the session, so finding it
49
+ * means scanning `items` for one listing the current session. That is the same
50
+ * lookup `uiWorkspace.startSession` does
51
+ * (`client/ui-workspace/src/client/navigation.ts:117-120`).
52
+ *
53
+ * `cwd` is the fallback for a session that genuinely has no workspace — a
54
+ * default-workspace profile, or a session created outside the workspace UI —
55
+ * not a companion to `workspaceId`.
56
+ *
57
+ * @param sessions - the current `sessions.list` snapshot.
58
+ * @param workspaces - the current workspace views, or undefined when the
59
+ * profile mounts no workspace controller.
60
+ * @returns the location argument to spread into `sessions.create`.
61
+ */
62
+ export declare function sessionLocation(sessions: SessionListLike, workspaces: readonly WorkspaceViewLike[] | undefined): SessionLocation;
63
+ //# sourceMappingURL=session-location.d.ts.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Loop engine selection store: the durable settings scope is the transport,
3
+ * and the store publishes a render-safe snapshot plus the write path.
4
+ * @module dsh-agent-hub/client/store
5
+ */
6
+ import type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client';
7
+ import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
8
+ import type { LoopEngineId } from '../settings.ts';
9
+ /** State rendered by the loop engine section. */
10
+ export interface LoopEngineState {
11
+ status: 'loading' | 'ready' | 'unavailable' | 'saving';
12
+ engine: LoopEngineId;
13
+ showInComposer: boolean;
14
+ writable: boolean;
15
+ error: string | null;
16
+ }
17
+ /** Narrow a wire section to the stored engine id and display toggle; an invalid one reads default. */
18
+ export declare function decodeLoopEngine(section: unknown): {
19
+ engine: LoopEngineId;
20
+ showInComposer: boolean;
21
+ } | undefined;
22
+ /** Coordinates the settings-backed loop engine selection. */
23
+ export declare class LoopEngineStore {
24
+ private readonly scope;
25
+ /** uSES-safe state source shared by the registered settings section. */
26
+ readonly store: SnapshotStore<LoopEngineState>;
27
+ private following;
28
+ private saving;
29
+ /**
30
+ * @param scope - the loop engine settings namespace scope.
31
+ */
32
+ constructor(scope: SettingsScope<{
33
+ engine: LoopEngineId;
34
+ showInComposer: boolean;
35
+ }>);
36
+ /** Begin following the bound scope and publish its current answer. */
37
+ load(): void;
38
+ /**
39
+ * Persist the selected engine. Success is judged against the snapshot the
40
+ * write left behind, so a refused write reports error after its recovery.
41
+ * @param engine - the engine to select for future Agent turns.
42
+ * @returns whether the write landed.
43
+ */
44
+ setEngine(engine: LoopEngineId): Promise<boolean>;
45
+ /**
46
+ * Persist whether the composer shows the engine picker. Success is judged
47
+ * against the snapshot the write left behind, so a refused write reports
48
+ * error after its recovery. Unlike {@link setEngine}, landing does not reload
49
+ * the page — the toggle only changes composer visibility.
50
+ * @param show - whether the chat page composer reveals the engine picker.
51
+ * @returns whether the write landed.
52
+ */
53
+ setShowInComposer(show: boolean): Promise<boolean>;
54
+ /** Stop following the scope. */
55
+ dispose(): void;
56
+ private derive;
57
+ }
58
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Claude Code slash-command bridge.
3
+ *
4
+ * The dsh `commands` runtime executes a registered command locally — the line
5
+ * is consumed and never reaches the model — so a command whose real processing
6
+ * lives inside the Claude Code CLI must forward the raw line back to the
7
+ * engine. The definitions here do exactly that: the handler delivers
8
+ * `/<name> [args]` to the receiving agent as a plain user message, and the CLI
9
+ * then expands it natively (built-ins and custom `.claude/commands/*.md`).
10
+ * Registering the built-ins keeps them visible in the dsh web slash menu;
11
+ * unregistered `/lines` pass through as user text, but the menu would hide the
12
+ * engine's command surface.
13
+ *
14
+ * User-level custom slash commands (`~/.claude/commands/*.md`) are discovered
15
+ * and registered the same way, so they appear in the menu AND reach the CLI.
16
+ * Project-level `.claude/commands/` files are left to the CLI entirely: they
17
+ * are cwd-dependent, and a global dsh registration would collide across
18
+ * projects.
19
+ *
20
+ * @module dsh-agent-hub/commands
21
+ */
22
+ import type { UserMessage } from '@deepseek-ai/dsh-session';
23
+ /** Minimal shape of a DSH command definition (avoiding a direct peer dep on @deepseek-ai/dsh-commands). */
24
+ export interface CommandDefinition {
25
+ readonly name: string;
26
+ readonly description: string;
27
+ readonly input?: {
28
+ readonly hint: string;
29
+ readonly images?: boolean;
30
+ };
31
+ readonly handler: (invocation: CommandInvocation) => CommandResult | Promise<CommandResult>;
32
+ }
33
+ /** Invocation delivered to one registered command handler. */
34
+ export interface CommandInvocation {
35
+ readonly commandId: string;
36
+ /** The receiving agent; forwarding handlers deliver the raw line back to it. */
37
+ readonly agent: {
38
+ readonly followup: (input: UserMessage) => void;
39
+ };
40
+ /** Exact text following the command name, including separator whitespace. */
41
+ readonly rawInput: string;
42
+ readonly signal: AbortSignal;
43
+ }
44
+ /** Settled result of one command handler. */
45
+ export interface CommandResult {
46
+ readonly kind: 'success' | 'error';
47
+ readonly text?: string;
48
+ }
49
+ /**
50
+ * Build the forwarding handler for one Claude Code slash command: it
51
+ * re-delivers the full `/<name> [args]` line to the receiving agent as a
52
+ * plain user message, where the CLI expands it. `rawInput` already carries the
53
+ * separator whitespace and any arguments.
54
+ * @param name - the command name without the leading slash.
55
+ * @returns the command handler.
56
+ */
57
+ export declare function forwardClaudeCodeCommand(name: string): (invocation: CommandInvocation) => CommandResult;
58
+ /** Claude Code's built-in slash commands. */
59
+ export declare const CLAUDE_CODE_COMMANDS: readonly CommandDefinition[];
60
+ /**
61
+ * Discover the user-level custom slash commands from `~/.claude/commands/*.md`
62
+ * and build forwarding definitions for them. The scan is synchronous so the
63
+ * mount path can register the commands before the engine-selection commit
64
+ * returns; files without a usable name or description, and names already taken
65
+ * by the built-ins, are skipped.
66
+ * @returns forwarding definitions, sorted by file name.
67
+ */
68
+ export declare function discoverUserSlashCommands(): CommandDefinition[];
69
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Context-file collection and body loading shared by the hosted engine
3
+ * drivers.
4
+ *
5
+ * Codex and Pi read per-directory instruction files (`AGENTS.md`; pi also
6
+ * accepts `CLAUDE.md` and prefers `AGENTS.override.md` inside any directory
7
+ * that has one) while walking from the session cwd up to the git root. The
8
+ * skill providers surface each collected set as one merged skill candidate so
9
+ * the dsh skill-injection seam (`/name` gestures) can carry it into the
10
+ * prompt; the body-loading helpers below feed both providers' list/get paths.
11
+ *
12
+ * @module dsh-agent-hub/driver-core/context-files
13
+ */
14
+ /** Per-directory context-file resolution policy for one engine. */
15
+ export interface ContextFilePolicy {
16
+ /** Per-directory override file that replaces the primary files when present. */
17
+ readonly override?: string;
18
+ /** Per-directory primary files, tried in order until one exists. */
19
+ readonly primary: readonly string[];
20
+ }
21
+ /**
22
+ * The directory chain from `cwd` up to the git root, nearest first. Without a
23
+ * repository the chain is just the resolved `cwd` itself, matching
24
+ * {@link findProjectRoot}'s fallback so the walk stays bounded.
25
+ * @param cwd - the session working directory.
26
+ * @returns the chain of directories to inspect.
27
+ */
28
+ export declare function projectAncestors(cwd: string): Promise<string[]>;
29
+ /**
30
+ * Collect every directory's context file per the policy, from the session cwd
31
+ * up to the git root.
32
+ * @param cwd - the session working directory.
33
+ * @param policy - per-directory resolution policy.
34
+ * @returns existing context files, nearest directory first.
35
+ */
36
+ export declare function collectProjectContextFiles(cwd: string, policy: ContextFilePolicy): Promise<string[]>;
37
+ /**
38
+ * Read one file, or `undefined` when it is unreadable.
39
+ * @param path - the file to read.
40
+ * @returns the file body, or `undefined` on any failure.
41
+ */
42
+ export declare function readOptionalFile(path: string): Promise<string | undefined>;
43
+ /**
44
+ * Whether any of the given sources carries non-whitespace content.
45
+ * @param paths - candidate file paths.
46
+ * @returns whether at least one readable source is non-empty.
47
+ */
48
+ export declare function anySourceNonEmpty(paths: readonly string[]): Promise<boolean>;
49
+ /**
50
+ * Whether one file exists and carries non-whitespace content.
51
+ * @param path - the file to inspect.
52
+ * @returns whether the file is readable and non-empty.
53
+ */
54
+ export declare function fileNonEmpty(path: string): Promise<boolean>;
55
+ /**
56
+ * Concatenate every non-empty readable source body in order, or `undefined`
57
+ * when none are readable.
58
+ * @param paths - candidate file paths, nearest directory first.
59
+ * @returns the joined bodies, or `undefined` when nothing could be read.
60
+ */
61
+ export declare function readSources(paths: readonly string[]): Promise<string | undefined>;
62
+ //# sourceMappingURL=context-files.d.ts.map
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared factory ownership and abort-race machinery for the hosted engines.
3
+ * Both the Claude Code and Codex loop drivers run the same lifecycle: exactly
4
+ * one factory owns the AgentFactory slot, every live agent's teardown is
5
+ * tracked until it settles, and setup awaits are raced against a fused abort
6
+ * signal. These helpers are engine-free — they only touch the fiber state,
7
+ * the session id type, and an AbortController — so the two loop modules share
8
+ * them verbatim.
9
+ *
10
+ * @module dsh-agent-hub/driver-core/ownership
11
+ */
12
+ import type { Context } from '@deepseek-ai/cordis';
13
+ import type { SessionId } from '@deepseek-ai/dsh-session';
14
+ /** Fiber states that cannot own or serve a new lifecycle. */
15
+ export declare const INACTIVE_STATES: ReadonlySet<number>;
16
+ /** Factory-level ownership: live agent teardowns plus load-time tracking. */
17
+ export declare class FactoryOwnership {
18
+ private readonly fiber;
19
+ private accepting;
20
+ private readonly teardown;
21
+ private readonly inactive;
22
+ private readonly liveAgents;
23
+ private startupTasks;
24
+ constructor(fiber: Context['fiber']);
25
+ /** Aborts (reason: `agent loop is not active` error) when factory teardown begins. */
26
+ get signal(): AbortSignal;
27
+ isActive(): boolean;
28
+ /** Track one live agent's shared teardown until it has run. */
29
+ track(dispose: () => Promise<void>): () => void;
30
+ /** Join config startup work that begins before an agent exists. */
31
+ trackStartup(job: Promise<void>): void;
32
+ /** Join one public create/resume continuation; factory dispose awaits its settlement. */
33
+ trackWrapper(job: Promise<unknown>): void;
34
+ dispose(): Promise<void>;
35
+ }
36
+ /** Await `operation`, or throw the signal's reason as soon as it aborts. */
37
+ export declare function raceAbort<T>(operation: PromiseLike<T> | T, signal: AbortSignal, id: SessionId): Promise<T>;
38
+ /** Start an abortable operation and release a value that arrives after cancellation. */
39
+ export declare function raceAbortCall<T>(operation: () => PromiseLike<T> | T, signal: AbortSignal, id: SessionId, releaseAbandoned?: (value: T) => void): Promise<T>;
40
+ //# sourceMappingURL=ownership.d.ts.map