dsh-agy-cli 0.0.0-stage → 0.12.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.
Files changed (91) hide show
  1. package/LICENSE +30 -0
  2. package/README.en.md +27 -0
  3. package/README.md +94 -2
  4. package/agents/dsh-agy-image-view.md +17 -0
  5. package/agents/dsh-agy-mcp-executor.md +32 -0
  6. package/agents/dsh-agy-read-only.md +20 -0
  7. package/agents/dsh-agy-tool-free.md +16 -0
  8. package/agents/dsh-agy-workspace-write.md +23 -0
  9. package/bin/dsh-agy-cli.js +9 -0
  10. package/bin/win32-x64/agy-launcher.exe +0 -0
  11. package/cordis.patch.yml +16 -0
  12. package/dist/client.js +4259 -0
  13. package/lib/agent-installer.d.ts +66 -0
  14. package/lib/agent-installer.js +147 -0
  15. package/lib/agent-presets.d.ts +42 -0
  16. package/lib/agent-presets.js +130 -0
  17. package/lib/agents-cli.d.ts +3 -0
  18. package/lib/agents-cli.js +110 -0
  19. package/lib/agy/diagnostics.d.ts +29 -0
  20. package/lib/agy/diagnostics.js +109 -0
  21. package/lib/agy/errors.d.ts +20 -0
  22. package/lib/agy/errors.js +77 -0
  23. package/lib/agy/experimental-transport.d.ts +79 -0
  24. package/lib/agy/experimental-transport.js +612 -0
  25. package/lib/agy/keepalive.d.ts +140 -0
  26. package/lib/agy/keepalive.js +424 -0
  27. package/lib/agy/limiter.d.ts +32 -0
  28. package/lib/agy/limiter.js +111 -0
  29. package/lib/agy/log.d.ts +214 -0
  30. package/lib/agy/log.js +503 -0
  31. package/lib/agy/models.d.ts +49 -0
  32. package/lib/agy/models.js +203 -0
  33. package/lib/agy/overrides.d.ts +17 -0
  34. package/lib/agy/overrides.js +73 -0
  35. package/lib/agy/parser.d.ts +102 -0
  36. package/lib/agy/parser.js +492 -0
  37. package/lib/agy/persistent-transport.d.ts +101 -0
  38. package/lib/agy/persistent-transport.js +753 -0
  39. package/lib/agy/process.d.ts +144 -0
  40. package/lib/agy/process.js +452 -0
  41. package/lib/agy/redact.d.ts +3 -0
  42. package/lib/agy/redact.js +24 -0
  43. package/lib/agy/stream-protocol.d.ts +50 -0
  44. package/lib/agy/stream-protocol.js +156 -0
  45. package/lib/agy/windows-launcher.d.ts +21 -0
  46. package/lib/agy/windows-launcher.js +76 -0
  47. package/lib/diagnostics.d.ts +78 -0
  48. package/lib/diagnostics.js +162 -0
  49. package/lib/doctor.d.ts +138 -0
  50. package/lib/doctor.js +923 -0
  51. package/lib/dsh/context.d.ts +133 -0
  52. package/lib/dsh/context.js +352 -0
  53. package/lib/dsh-home.d.ts +6 -0
  54. package/lib/dsh-home.js +11 -0
  55. package/lib/index.d.ts +43 -0
  56. package/lib/index.js +256 -0
  57. package/lib/mcp/agy-registration.d.ts +70 -0
  58. package/lib/mcp/agy-registration.js +245 -0
  59. package/lib/mcp/bridge.d.ts +73 -0
  60. package/lib/mcp/bridge.js +295 -0
  61. package/lib/mcp/endpoint.d.ts +41 -0
  62. package/lib/mcp/endpoint.js +142 -0
  63. package/lib/mcp/protocol.d.ts +107 -0
  64. package/lib/mcp/protocol.js +197 -0
  65. package/lib/mcp/tool-projection.d.ts +64 -0
  66. package/lib/mcp/tool-projection.js +168 -0
  67. package/lib/provider/agy.d.ts +153 -0
  68. package/lib/provider/agy.js +2159 -0
  69. package/lib/provider/config.d.ts +226 -0
  70. package/lib/provider/config.js +294 -0
  71. package/lib/provider/error-codes.d.ts +22 -0
  72. package/lib/provider/error-codes.js +29 -0
  73. package/lib/provider/image-bridge.d.ts +60 -0
  74. package/lib/provider/image-bridge.js +290 -0
  75. package/lib/provider/mock.d.ts +15 -0
  76. package/lib/provider/mock.js +78 -0
  77. package/lib/provider/prompt-budget.d.ts +45 -0
  78. package/lib/provider/prompt-budget.js +596 -0
  79. package/lib/provider/serialize.d.ts +15 -0
  80. package/lib/provider/serialize.js +82 -0
  81. package/lib/provider/tool-protocol.d.ts +80 -0
  82. package/lib/provider/tool-protocol.js +848 -0
  83. package/lib/provider/tool-schema-file.d.ts +11 -0
  84. package/lib/provider/tool-schema-file.js +27 -0
  85. package/lib/session/file-store.d.ts +56 -0
  86. package/lib/session/file-store.js +186 -0
  87. package/lib/session/store.d.ts +49 -0
  88. package/lib/session/store.js +62 -0
  89. package/lib/web/router.d.ts +19 -0
  90. package/lib/web/router.js +906 -0
  91. package/package.json +112 -4
@@ -0,0 +1,11 @@
1
+ import type { StructuredToolProtocol } from './tool-protocol.js';
2
+ export interface StagedToolSchema {
3
+ readonly path: string;
4
+ readonly cleanup: () => Promise<void>;
5
+ }
6
+ /**
7
+ * Stage one bounded schema outside the user workspace. The returned path is
8
+ * intended only for AGY argv; callers must invoke cleanup on every outcome.
9
+ */
10
+ export declare function stageToolSchema(protocol: StructuredToolProtocol): Promise<StagedToolSchema>;
11
+ //# sourceMappingURL=tool-schema-file.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Stage one bounded schema outside the user workspace. The returned path is
6
+ * intended only for AGY argv; callers must invoke cleanup on every outcome.
7
+ */
8
+ export async function stageToolSchema(protocol) {
9
+ const directory = await mkdtemp(join(tmpdir(), 'dsh-agy-tool-schema-'));
10
+ const path = join(directory, 'schema.json');
11
+ let cleaned = false;
12
+ const cleanup = async () => {
13
+ if (cleaned)
14
+ return;
15
+ cleaned = true;
16
+ await rm(directory, { recursive: true, force: true });
17
+ };
18
+ try {
19
+ await writeFile(path, protocol.schemaJson, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
20
+ return { path, cleanup };
21
+ }
22
+ catch (error) {
23
+ await cleanup();
24
+ throw error;
25
+ }
26
+ }
27
+ //# sourceMappingURL=tool-schema-file.js.map
@@ -0,0 +1,56 @@
1
+ import type { SessionFingerprint, SessionRecord, SessionStore } from './store.js';
2
+ /**
3
+ * How many conversation bindings to keep. Each DSH session leaves one AGY
4
+ * conversation database on disk, so the map must not grow without bound; past
5
+ * this cap the oldest-touched records are dropped and those sessions fall back
6
+ * to the full DSH history on their next turn.
7
+ */
8
+ export declare const MAX_SESSION_RECORDS = 2000;
9
+ export interface FileSessionStoreOptions {
10
+ readonly path: string;
11
+ /** Injectable for tests; production uses `Date.now`. */
12
+ readonly now?: () => number;
13
+ /** Records older than this are dropped on load. Default 30 days. */
14
+ readonly maxAgeMs?: number;
15
+ /** Injectable cap; production uses `MAX_SESSION_RECORDS`. */
16
+ readonly maxRecords?: number;
17
+ }
18
+ /** Default location for the durable `sessionId -> AGY conversationId` map. */
19
+ export declare function defaultSessionStorePath(): string;
20
+ /**
21
+ * The default on-disk location is used verbatim; anything else is taken as a
22
+ * directory so a test or a profile can point the store at a scratch folder.
23
+ */
24
+ export declare function resolveSessionStorePath(configured: string | undefined): string;
25
+ /**
26
+ * Durable `DSH sessionId -> AGY conversationId` map.
27
+ *
28
+ * The in-memory store was the reason a long conversation re-sent its entire
29
+ * history: AGY keeps every conversation on disk (`--conversation <id>` resumes
30
+ * across process boundaries, verified), but the mapping lived only in the
31
+ * adapter. After a host restart the map was empty, so the first turn of every
32
+ * pre-existing session went back to `requestMode: 'bootstrap'` and serialized
33
+ * the whole DSH history into one prompt — pinned at the input limit, missing the
34
+ * upstream prompt cache, and timing out on long sessions.
35
+ *
36
+ * Writes are best-effort and coalesced: a failed write only costs the binding,
37
+ * and the next turn re-bootstraps that session. Reads never throw.
38
+ */
39
+ export declare class FileSessionStore implements SessionStore {
40
+ private readonly path;
41
+ private readonly now;
42
+ private readonly maxAgeMs;
43
+ private readonly maxRecords;
44
+ private records;
45
+ private dirty;
46
+ constructor(options: FileSessionStoreOptions);
47
+ private load;
48
+ private persist;
49
+ get(sessionId: string): SessionRecord | undefined;
50
+ set(sessionId: string, conversationId: string, fingerprint?: SessionFingerprint): SessionRecord;
51
+ delete(sessionId: string): boolean;
52
+ clear(): void;
53
+ /** Test/diagnostic view of the in-memory projection. */
54
+ list(): readonly SessionRecord[];
55
+ }
56
+ //# sourceMappingURL=file-store.d.ts.map
@@ -0,0 +1,186 @@
1
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ /**
5
+ * How many conversation bindings to keep. Each DSH session leaves one AGY
6
+ * conversation database on disk, so the map must not grow without bound; past
7
+ * this cap the oldest-touched records are dropped and those sessions fall back
8
+ * to the full DSH history on their next turn.
9
+ */
10
+ export const MAX_SESSION_RECORDS = 2_000;
11
+ const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
12
+ function resolveDshHome() {
13
+ return process.env.DSH_HOME?.trim() || join(homedir(), '.dsh');
14
+ }
15
+ /** Default location for the durable `sessionId -> AGY conversationId` map. */
16
+ export function defaultSessionStorePath() {
17
+ return join(resolveDshHome(), 'agy-cli', 'sessions.json');
18
+ }
19
+ /**
20
+ * The default on-disk location is used verbatim; anything else is taken as a
21
+ * directory so a test or a profile can point the store at a scratch folder.
22
+ */
23
+ export function resolveSessionStorePath(configured) {
24
+ const trimmed = configured?.trim();
25
+ if (trimmed === undefined || trimmed.length === 0)
26
+ return defaultSessionStorePath();
27
+ return trimmed.toLowerCase().endsWith('.json') ? trimmed : join(trimmed, 'sessions.json');
28
+ }
29
+ function isRecord(value) {
30
+ if (typeof value !== 'object' || value === null)
31
+ return false;
32
+ const record = value;
33
+ return typeof record.sessionId === 'string'
34
+ && record.sessionId.length > 0
35
+ && typeof record.conversationId === 'string'
36
+ && record.conversationId.length > 0
37
+ && typeof record.updatedAt === 'number'
38
+ && Number.isFinite(record.updatedAt);
39
+ }
40
+ function parsePersisted(text) {
41
+ const records = new Map();
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(text);
45
+ }
46
+ catch {
47
+ // A truncated or hand-edited file must not break the provider: an empty map
48
+ // means every session bootstraps from the full DSH history, which is the
49
+ // behaviour before this store existed.
50
+ return records;
51
+ }
52
+ const list = Array.isArray(parsed)
53
+ ? parsed
54
+ : parsed !== null && typeof parsed === 'object' && Array.isArray(parsed.sessions)
55
+ ? parsed.sessions
56
+ : [];
57
+ for (const entry of list) {
58
+ if (!isRecord(entry))
59
+ continue;
60
+ records.set(entry.sessionId, {
61
+ sessionId: entry.sessionId,
62
+ conversationId: entry.conversationId,
63
+ updatedAt: entry.updatedAt,
64
+ ...(typeof entry.chainHash === 'string' && typeof entry.messageCount === 'number'
65
+ ? { chainHash: entry.chainHash, messageCount: entry.messageCount }
66
+ : {}),
67
+ });
68
+ }
69
+ return records;
70
+ }
71
+ /**
72
+ * Durable `DSH sessionId -> AGY conversationId` map.
73
+ *
74
+ * The in-memory store was the reason a long conversation re-sent its entire
75
+ * history: AGY keeps every conversation on disk (`--conversation <id>` resumes
76
+ * across process boundaries, verified), but the mapping lived only in the
77
+ * adapter. After a host restart the map was empty, so the first turn of every
78
+ * pre-existing session went back to `requestMode: 'bootstrap'` and serialized
79
+ * the whole DSH history into one prompt — pinned at the input limit, missing the
80
+ * upstream prompt cache, and timing out on long sessions.
81
+ *
82
+ * Writes are best-effort and coalesced: a failed write only costs the binding,
83
+ * and the next turn re-bootstraps that session. Reads never throw.
84
+ */
85
+ export class FileSessionStore {
86
+ path;
87
+ now;
88
+ maxAgeMs;
89
+ maxRecords;
90
+ records;
91
+ dirty = false;
92
+ constructor(options) {
93
+ this.path = options.path;
94
+ this.now = options.now ?? Date.now;
95
+ this.maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
96
+ this.maxRecords = options.maxRecords ?? MAX_SESSION_RECORDS;
97
+ }
98
+ load() {
99
+ if (this.records !== undefined)
100
+ return this.records;
101
+ let loaded = new Map();
102
+ try {
103
+ loaded = parsePersisted(readFileSync(this.path, 'utf8'));
104
+ }
105
+ catch {
106
+ loaded = new Map();
107
+ }
108
+ // Drop bindings AGY itself may have pruned: an attempt against a stale id is
109
+ // harmless (AGY starts a fresh conversation and the provider detects the id
110
+ // mismatch and retries with the full history), but keeping dead ids wastes
111
+ // the cap.
112
+ const cutoff = this.now() - this.maxAgeMs;
113
+ for (const [key, record] of loaded) {
114
+ if (record.updatedAt < cutoff)
115
+ loaded.delete(key);
116
+ }
117
+ this.records = loaded;
118
+ return loaded;
119
+ }
120
+ persist() {
121
+ if (!this.dirty)
122
+ return;
123
+ const list = [...(this.records ?? []).values()];
124
+ // Unpredictable temp name: two processes pointing at the same file must not
125
+ // be able to win each other's rename.
126
+ const temporary = `${this.path}.tmp-${process.pid}-${Date.now().toString(36)}`;
127
+ try {
128
+ mkdirSync(dirname(this.path), { recursive: true });
129
+ // Temp file plus rename: a crash mid-write leaves the previous map intact.
130
+ writeFileSync(temporary, JSON.stringify({ sessions: list }, null, 2), 'utf8');
131
+ renameSync(temporary, this.path);
132
+ this.dirty = false;
133
+ }
134
+ catch {
135
+ try {
136
+ rmSync(temporary, { force: true });
137
+ }
138
+ catch {
139
+ // Nothing to clean up, or the cleanup itself failed.
140
+ }
141
+ // A read-only or unwritable location degrades to in-memory behaviour only.
142
+ this.dirty = false;
143
+ }
144
+ }
145
+ get(sessionId) {
146
+ const record = this.load().get(sessionId);
147
+ return record === undefined ? undefined : { ...record };
148
+ }
149
+ set(sessionId, conversationId, fingerprint) {
150
+ const records = this.load();
151
+ const record = {
152
+ sessionId,
153
+ conversationId,
154
+ updatedAt: this.now(),
155
+ ...(fingerprint === undefined ? {} : { chainHash: fingerprint.chainHash, messageCount: fingerprint.messageCount }),
156
+ };
157
+ records.set(sessionId, record);
158
+ if (records.size > this.maxRecords) {
159
+ const sorted = [...records.values()].sort((a, b) => a.updatedAt - b.updatedAt);
160
+ for (const stale of sorted.slice(0, records.size - this.maxRecords)) {
161
+ records.delete(stale.sessionId);
162
+ }
163
+ }
164
+ this.dirty = true;
165
+ this.persist();
166
+ return { ...record };
167
+ }
168
+ delete(sessionId) {
169
+ const removed = this.load().delete(sessionId);
170
+ if (removed) {
171
+ this.dirty = true;
172
+ this.persist();
173
+ }
174
+ return removed;
175
+ }
176
+ clear() {
177
+ this.load().clear();
178
+ this.dirty = true;
179
+ this.persist();
180
+ }
181
+ /** Test/diagnostic view of the in-memory projection. */
182
+ list() {
183
+ return [...this.load().values()];
184
+ }
185
+ }
186
+ //# sourceMappingURL=file-store.js.map
@@ -0,0 +1,49 @@
1
+ export interface SessionRecord {
2
+ readonly sessionId: string;
3
+ readonly conversationId: string;
4
+ readonly updatedAt: number;
5
+ /**
6
+ * Fingerprint of the DSH history this conversation already holds, as
7
+ * `sha256(serialize(system + messages))` (first 16 hex chars), plus the
8
+ * message count it covered. Design borrowed from dsh-agy-safe's history
9
+ * chain: on a resumed turn the prefix is re-serialized locally and compared,
10
+ * so an edited, compacted or truncated DSH history is detected before a
11
+ * delta prompt lands on stale AGY context — the turn then falls back to the
12
+ * full-history replay instead of answering from the wrong conversation.
13
+ * Both fields are optional: records written before the chain existed simply
14
+ * skip the check.
15
+ */
16
+ readonly chainHash?: string;
17
+ readonly messageCount?: number;
18
+ }
19
+ /** Optional fingerprint metadata carried alongside a conversation binding. */
20
+ export interface SessionFingerprint {
21
+ readonly chainHash: string;
22
+ readonly messageCount: number;
23
+ }
24
+ export interface SessionStore {
25
+ get(sessionId: string): SessionRecord | undefined;
26
+ set(sessionId: string, conversationId: string, fingerprint?: SessionFingerprint): SessionRecord;
27
+ delete(sessionId: string): boolean;
28
+ clear(): void;
29
+ }
30
+ /** In-memory mapping; losing it on plugin restart falls back to full DSH history. */
31
+ export declare class InMemorySessionStore implements SessionStore {
32
+ private readonly records;
33
+ get(sessionId: string): SessionRecord | undefined;
34
+ set(sessionId: string, conversationId: string, fingerprint?: SessionFingerprint): SessionRecord;
35
+ delete(sessionId: string): boolean;
36
+ clear(): void;
37
+ }
38
+ /** Serialize calls for one Session while allowing different Sessions to run concurrently. */
39
+ export declare class SessionRegistry implements SessionStore {
40
+ private readonly store;
41
+ private readonly locks;
42
+ constructor(store?: SessionStore);
43
+ get(sessionId: string): SessionRecord | undefined;
44
+ set(sessionId: string, conversationId: string, fingerprint?: SessionFingerprint): SessionRecord;
45
+ delete(sessionId: string): boolean;
46
+ clear(): void;
47
+ acquire(sessionId: string): Promise<() => void>;
48
+ }
49
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1,62 @@
1
+ /** In-memory mapping; losing it on plugin restart falls back to full DSH history. */
2
+ export class InMemorySessionStore {
3
+ records = new Map();
4
+ get(sessionId) {
5
+ const record = this.records.get(sessionId);
6
+ return record === undefined ? undefined : { ...record };
7
+ }
8
+ set(sessionId, conversationId, fingerprint) {
9
+ const record = {
10
+ sessionId,
11
+ conversationId,
12
+ updatedAt: Date.now(),
13
+ ...(fingerprint === undefined ? {} : { chainHash: fingerprint.chainHash, messageCount: fingerprint.messageCount }),
14
+ };
15
+ this.records.set(sessionId, record);
16
+ return { ...record };
17
+ }
18
+ delete(sessionId) {
19
+ return this.records.delete(sessionId);
20
+ }
21
+ clear() {
22
+ this.records.clear();
23
+ }
24
+ }
25
+ /** Serialize calls for one Session while allowing different Sessions to run concurrently. */
26
+ export class SessionRegistry {
27
+ store;
28
+ locks = new Map();
29
+ constructor(store = new InMemorySessionStore()) {
30
+ this.store = store;
31
+ }
32
+ get(sessionId) {
33
+ return this.store.get(sessionId);
34
+ }
35
+ set(sessionId, conversationId, fingerprint) {
36
+ return this.store.set(sessionId, conversationId, fingerprint);
37
+ }
38
+ delete(sessionId) {
39
+ return this.store.delete(sessionId);
40
+ }
41
+ clear() {
42
+ this.store.clear();
43
+ }
44
+ async acquire(sessionId) {
45
+ const previous = this.locks.get(sessionId) ?? Promise.resolve();
46
+ let releaseCurrent;
47
+ const current = new Promise(resolve => { releaseCurrent = resolve; });
48
+ const tail = previous.then(() => current);
49
+ this.locks.set(sessionId, tail);
50
+ await previous;
51
+ let released = false;
52
+ return () => {
53
+ if (released)
54
+ return;
55
+ released = true;
56
+ releaseCurrent();
57
+ if (this.locks.get(sessionId) === tail)
58
+ this.locks.delete(sessionId);
59
+ };
60
+ }
61
+ }
62
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,19 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Config as ConfigType } from '../provider/config.js';
3
+ import type { AgyModelDiscovery } from '../agy/models.js';
4
+ /** Record the outcome of one boot registration step (called from apply()). */
5
+ export declare function noteRegistration(step: string, detail: string): void;
6
+ interface CardPing {
7
+ at: number;
8
+ props: string[];
9
+ }
10
+ /** Record one card mount reported by the browser half. */
11
+ export declare function noteCardPing(props: string[]): void;
12
+ /** Snapshot for `/plugins/agy-cli/diagnose`. */
13
+ export declare function getAttachDiagnostics(): {
14
+ notes: Record<string, string>;
15
+ cardPings: CardPing[];
16
+ };
17
+ export declare function setupWebRoutes(ctx: Context, activeConfig: ConfigType, modelDiscovery?: AgyModelDiscovery): () => void;
18
+ export {};
19
+ //# sourceMappingURL=router.d.ts.map