@duckmind/dm-darwin-x64 0.13.2 → 0.13.6

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 (41) hide show
  1. package/CHANGELOG.md +49 -2
  2. package/README.md +5 -5
  3. package/dm +0 -0
  4. package/docs/settings.md +1 -0
  5. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  6. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  7. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  8. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  9. package/examples/extensions/with-deps/package-lock.json +2 -2
  10. package/examples/extensions/with-deps/package.json +1 -1
  11. package/extensions/.dm-extensions.json +2 -2
  12. package/extensions/dm-multicodex/README.md +3 -1
  13. package/extensions/dm-multicodex/account-manager.test.ts +3 -1
  14. package/extensions/dm-multicodex/account-manager.ts +27 -1
  15. package/extensions/dm-multicodex/commands.test.ts +1 -0
  16. package/extensions/dm-multicodex/commands.ts +81 -1
  17. package/extensions/dm-multicodex/index.ts +7 -0
  18. package/extensions/dm-multicodex/node_modules/.package-lock.json +28 -28
  19. package/extensions/dm-multicodex/package-lock.json +9633 -9633
  20. package/extensions/dm-multicodex/package.json +56 -56
  21. package/extensions/dm-multicodex/storage.ts +2 -2
  22. package/extensions/dm-multicodex/sync.test.ts +114 -0
  23. package/extensions/dm-multicodex/sync.ts +249 -0
  24. package/extensions/dm-multicodex/tsconfig.json +17 -0
  25. package/extensions/dm-subagents/artifacts.ts +11 -5
  26. package/extensions/dm-subagents/async-execution.ts +6 -1
  27. package/extensions/dm-subagents/execution.ts +1 -1
  28. package/extensions/dm-subagents/index.ts +1 -1
  29. package/extensions/dm-subagents/intercom-bridge.ts +8 -0
  30. package/extensions/dm-subagents/package.json +1 -1
  31. package/extensions/dm-subagents/schemas.ts +1 -1
  32. package/extensions/dm-subagents/settings.ts +6 -4
  33. package/extensions/dm-subagents/skills.ts +117 -25
  34. package/extensions/dm-subagents/subagent-executor.ts +2 -6
  35. package/extensions/dm-subagents/subagent-runner.ts +176 -51
  36. package/extensions/dm-subagents/types.ts +62 -2
  37. package/extensions/dm-subagents/worktree.ts +27 -9
  38. package/extensions/dm-thinking-timer/README.md +1 -1
  39. package/extensions/dm-ultrathink/src/naming.ts +151 -10
  40. package/package.json +1 -1
  41. package/theme/duckmind.json +77 -65
@@ -1,58 +1,58 @@
1
1
  {
2
- "name": "dm-multicodex",
3
- "version": "2.3.1",
4
- "description": "Codex account rotation extension for DM",
5
- "type": "module",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/victor-software-house/pi-multicodex.git"
10
- },
11
- "homepage": "https://github.com/victor-software-house/pi-multicodex#readme",
12
- "bugs": {
13
- "url": "https://github.com/victor-software-house/pi-multicodex/issues"
14
- },
15
- "scripts": {
16
- "lint": "biome check .",
17
- "test": "vitest run -c vitest.config.ts",
18
- "tsgo": "tsgo -p tsconfig.json",
19
- "generate:schema": "bun scripts/generate-schema.ts",
20
- "check": "pnpm lint && pnpm tsgo && pnpm test",
21
- "pack:dry": "npm pack --dry-run",
22
- "release:dry": "pnpm exec semantic-release --dry-run"
23
- },
24
- "dependencies": {
25
- "pi-provider-utils": "^0.0.0",
26
- "zod": "^4.3.6"
27
- },
28
- "peerDependencies": {
29
- "@mariozechner/pi-ai": "*",
30
- "@mariozechner/pi-coding-agent": "*",
31
- "@mariozechner/pi-tui": "*"
32
- },
33
- "devDependencies": {
34
- "@biomejs/biome": "^2.4.7",
35
- "@commitlint/cli": "^20.4.4",
36
- "@commitlint/config-conventional": "^20.4.4",
37
- "@mariozechner/pi-ai": "^0.63.1",
38
- "@mariozechner/pi-coding-agent": "^0.63.1",
39
- "@mariozechner/pi-tui": "^0.63.1",
40
- "@semantic-release/changelog": "^6.0.3",
41
- "@semantic-release/commit-analyzer": "^13.0.1",
42
- "@semantic-release/git": "^10.0.1",
43
- "@semantic-release/github": "^12.0.6",
44
- "@semantic-release/npm": "^13.1.5",
45
- "@semantic-release/release-notes-generator": "^14.1.0",
46
- "@types/node": "^25.5.0",
47
- "@typescript/native-preview": "7.0.0-dev.20260314.1",
48
- "conventional-changelog-conventionalcommits": "^9.3.0",
49
- "semantic-release": "^25.0.3",
50
- "typescript": "^5.9.3",
51
- "vitest": "^4.1.0"
52
- },
53
- "pi": {
54
- "extensions": [
55
- "./index.ts"
56
- ]
57
- }
2
+ "name": "dm-multicodex",
3
+ "version": "2.3.1",
4
+ "description": "Codex account rotation extension for DM",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/victor-software-house/pi-multicodex.git"
10
+ },
11
+ "homepage": "https://github.com/victor-software-house/pi-multicodex#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/victor-software-house/pi-multicodex/issues"
14
+ },
15
+ "scripts": {
16
+ "lint": "biome check .",
17
+ "test": "vitest run -c vitest.config.ts",
18
+ "tsgo": "tsgo -p tsconfig.json",
19
+ "generate:schema": "bun scripts/generate-schema.ts",
20
+ "check": "pnpm lint && pnpm tsgo && pnpm test",
21
+ "pack:dry": "npm pack --dry-run",
22
+ "release:dry": "pnpm exec semantic-release --dry-run"
23
+ },
24
+ "dependencies": {
25
+ "pi-provider-utils": "^0.0.0",
26
+ "zod": "^4.3.6"
27
+ },
28
+ "peerDependencies": {
29
+ "@mariozechner/pi-ai": "*",
30
+ "@mariozechner/pi-coding-agent": "*",
31
+ "@mariozechner/pi-tui": "*"
32
+ },
33
+ "devDependencies": {
34
+ "@biomejs/biome": "^2.4.7",
35
+ "@commitlint/cli": "^20.4.4",
36
+ "@commitlint/config-conventional": "^20.4.4",
37
+ "@mariozechner/pi-ai": "^0.63.1",
38
+ "@mariozechner/pi-coding-agent": "^0.63.1",
39
+ "@mariozechner/pi-tui": "^0.63.1",
40
+ "@semantic-release/changelog": "^6.0.3",
41
+ "@semantic-release/commit-analyzer": "^13.0.1",
42
+ "@semantic-release/git": "^10.0.1",
43
+ "@semantic-release/github": "^12.0.6",
44
+ "@semantic-release/npm": "^13.1.5",
45
+ "@semantic-release/release-notes-generator": "^14.1.0",
46
+ "@types/node": "^25.5.0",
47
+ "@typescript/native-preview": "7.0.0-dev.20260314.1",
48
+ "conventional-changelog-conventionalcommits": "^9.3.0",
49
+ "semantic-release": "^25.0.3",
50
+ "typescript": "^5.9.3",
51
+ "vitest": "^4.1.0"
52
+ },
53
+ "pi": {
54
+ "extensions": [
55
+ "./index.ts"
56
+ ]
57
+ }
58
58
  }
@@ -90,7 +90,7 @@ function stripLegacyFields(raw: Record<string, unknown>): boolean {
90
90
  return stripped;
91
91
  }
92
92
 
93
- function migrateRawStorage(raw: unknown): StorageData {
93
+ export function coerceStorageData(raw: unknown): StorageData {
94
94
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
95
95
  return { version: CURRENT_VERSION, accounts: [], activeEmail: undefined };
96
96
  }
@@ -141,7 +141,7 @@ export function loadStorage(): StorageData {
141
141
  !("version" in raw) ||
142
142
  raw.version !== CURRENT_VERSION ||
143
143
  needsLegacyStrip(raw);
144
- const data = migrateRawStorage(raw);
144
+ const data = coerceStorageData(raw);
145
145
  if (needsMigration) {
146
146
  saveStorage(data);
147
147
  }
@@ -0,0 +1,114 @@
1
+ import {
2
+ createCipheriv,
3
+ createHash,
4
+ pbkdf2Sync,
5
+ randomBytes,
6
+ } from "node:crypto";
7
+ import { describe, expect, it, vi } from "vitest";
8
+ import {
9
+ decryptCodexAccountsBundle,
10
+ type EncryptedCodexAccountsBundle,
11
+ syncManagedAccountsFromDuckMind,
12
+ } from "./sync";
13
+
14
+ function createEncryptedBundle(
15
+ secret: string,
16
+ plaintext: Record<string, unknown>,
17
+ ): EncryptedCodexAccountsBundle {
18
+ const salt = randomBytes(16);
19
+ const iv = randomBytes(12);
20
+ const iterations = 100_000;
21
+ const serialized = Buffer.from(JSON.stringify(plaintext), "utf8");
22
+ const key = pbkdf2Sync(secret, salt, iterations, 32, "sha256");
23
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
24
+ const ciphertext = Buffer.concat([
25
+ cipher.update(serialized),
26
+ cipher.final(),
27
+ cipher.getAuthTag(),
28
+ ]);
29
+ return {
30
+ version: 1,
31
+ algorithm: "AES-256-GCM",
32
+ kdf: {
33
+ name: "PBKDF2",
34
+ hash: "SHA-256",
35
+ iterations,
36
+ salt: salt.toString("base64"),
37
+ },
38
+ iv: iv.toString("base64"),
39
+ ciphertext: ciphertext.toString("base64"),
40
+ file_name: "codex-accounts.json",
41
+ content_type: "application/json",
42
+ plaintext_sha256: createHash("sha256").update(serialized).digest("hex"),
43
+ plaintext_bytes: serialized.length,
44
+ uploaded_at: "2026-04-13T18:00:00.000Z",
45
+ };
46
+ }
47
+
48
+ describe("decryptCodexAccountsBundle", () => {
49
+ it("decrypts a DuckMind bundle into normalized storage data", () => {
50
+ const secret = "duckmind-secret";
51
+ const bundle = createEncryptedBundle(secret, {
52
+ accounts: [
53
+ {
54
+ email: "alpha@example.com",
55
+ accessToken: "access-token",
56
+ refreshToken: "refresh-token",
57
+ expiresAt: 1234567890,
58
+ },
59
+ ],
60
+ activeEmail: "alpha@example.com",
61
+ });
62
+
63
+ const result = decryptCodexAccountsBundle(bundle, secret);
64
+ expect(result.storage.accounts).toHaveLength(1);
65
+ expect(result.storage.accounts[0]?.email).toBe("alpha@example.com");
66
+ expect(result.storage.activeEmail).toBe("alpha@example.com");
67
+ expect(result.uploadedAt).toBe("2026-04-13T18:00:00.000Z");
68
+ });
69
+
70
+ it("rejects an invalid sync secret", () => {
71
+ const bundle = createEncryptedBundle("right-secret", {
72
+ accounts: [],
73
+ activeEmail: undefined,
74
+ });
75
+
76
+ expect(() => decryptCodexAccountsBundle(bundle, "wrong-secret")).toThrow(
77
+ /invalid/i,
78
+ );
79
+ });
80
+ });
81
+
82
+ describe("syncManagedAccountsFromDuckMind", () => {
83
+ it("downloads the encrypted bundle with browser-like headers", async () => {
84
+ const secret = "duckmind-secret";
85
+ const bundle = createEncryptedBundle(secret, {
86
+ accounts: [],
87
+ activeEmail: undefined,
88
+ });
89
+ const fetchImpl = vi.fn(
90
+ async (_url: string, init?: { headers?: Record<string, string> }) => ({
91
+ ok: true,
92
+ status: 200,
93
+ text: async () => JSON.stringify(bundle),
94
+ init,
95
+ }),
96
+ );
97
+
98
+ const result = await syncManagedAccountsFromDuckMind(secret, {
99
+ fetchImpl,
100
+ sourceUrl: "https://example.com/codex-accounts.json",
101
+ });
102
+
103
+ expect(fetchImpl).toHaveBeenCalledWith(
104
+ "https://example.com/codex-accounts.json",
105
+ expect.objectContaining({
106
+ headers: expect.objectContaining({
107
+ Accept: expect.stringContaining("application/json"),
108
+ "User-Agent": "Mozilla/5.0",
109
+ }),
110
+ }),
111
+ );
112
+ expect(result.sourceUrl).toBe("https://example.com/codex-accounts.json");
113
+ });
114
+ });
@@ -0,0 +1,249 @@
1
+ import { createDecipheriv, createHash, pbkdf2Sync } from "node:crypto";
2
+ import { coerceStorageData, type StorageData } from "./storage";
3
+
4
+ export const DUCKMIND_CODEX_ACCOUNTS_URL =
5
+ "https://duckmind.ai/codex-accounts.json";
6
+
7
+ type EncryptedKdfConfig = {
8
+ name: string;
9
+ hash: string;
10
+ iterations: number;
11
+ salt: string;
12
+ };
13
+
14
+ export type EncryptedCodexAccountsBundle = {
15
+ version: number;
16
+ algorithm: string;
17
+ kdf: EncryptedKdfConfig;
18
+ iv: string;
19
+ ciphertext: string;
20
+ file_name?: string;
21
+ content_type?: string;
22
+ plaintext_sha256: string;
23
+ plaintext_bytes?: number;
24
+ uploaded_at?: string;
25
+ };
26
+
27
+ type FetchResponseLike = {
28
+ ok: boolean;
29
+ status: number;
30
+ statusText?: string;
31
+ text(): Promise<string>;
32
+ };
33
+
34
+ type FetchLike = (
35
+ url: string,
36
+ init?: {
37
+ headers?: Record<string, string>;
38
+ },
39
+ ) => Promise<FetchResponseLike>;
40
+
41
+ export interface SyncManagedAccountsResult {
42
+ storage: StorageData;
43
+ uploadedAt?: string;
44
+ fileName?: string;
45
+ plaintextSha256: string;
46
+ sourceUrl: string;
47
+ }
48
+
49
+ function ensureString(value: unknown, label: string): string {
50
+ if (typeof value !== "string" || !value.trim()) {
51
+ throw new Error(`Missing ${label} in DuckMind account bundle.`);
52
+ }
53
+ return value.trim();
54
+ }
55
+
56
+ function ensurePositiveInteger(value: unknown, label: string): number {
57
+ if (!Number.isInteger(value) || typeof value !== "number" || value <= 0) {
58
+ throw new Error(`Invalid ${label} in DuckMind account bundle.`);
59
+ }
60
+ return value;
61
+ }
62
+
63
+ function parseBundle(raw: unknown): EncryptedCodexAccountsBundle {
64
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
65
+ throw new Error("DuckMind account bundle is not a JSON object.");
66
+ }
67
+
68
+ const record = raw as Record<string, unknown>;
69
+ const kdfRecord = record.kdf;
70
+ if (!kdfRecord || typeof kdfRecord !== "object" || Array.isArray(kdfRecord)) {
71
+ throw new Error("DuckMind account bundle is missing kdf settings.");
72
+ }
73
+
74
+ return {
75
+ version: ensurePositiveInteger(record.version, "version"),
76
+ algorithm: ensureString(record.algorithm, "algorithm"),
77
+ kdf: {
78
+ name: ensureString(
79
+ (kdfRecord as Record<string, unknown>).name,
80
+ "kdf.name",
81
+ ),
82
+ hash: ensureString(
83
+ (kdfRecord as Record<string, unknown>).hash,
84
+ "kdf.hash",
85
+ ),
86
+ iterations: ensurePositiveInteger(
87
+ (kdfRecord as Record<string, unknown>).iterations,
88
+ "kdf.iterations",
89
+ ),
90
+ salt: ensureString(
91
+ (kdfRecord as Record<string, unknown>).salt,
92
+ "kdf.salt",
93
+ ),
94
+ },
95
+ iv: ensureString(record.iv, "iv"),
96
+ ciphertext: ensureString(record.ciphertext, "ciphertext"),
97
+ file_name:
98
+ typeof record.file_name === "string" ? record.file_name : undefined,
99
+ content_type:
100
+ typeof record.content_type === "string" ? record.content_type : undefined,
101
+ plaintext_sha256: ensureString(record.plaintext_sha256, "plaintext_sha256"),
102
+ plaintext_bytes:
103
+ typeof record.plaintext_bytes === "number"
104
+ ? record.plaintext_bytes
105
+ : undefined,
106
+ uploaded_at:
107
+ typeof record.uploaded_at === "string" ? record.uploaded_at : undefined,
108
+ };
109
+ }
110
+
111
+ function decodeBase64(value: string, label: string): Buffer {
112
+ try {
113
+ const decoded = Buffer.from(value, "base64");
114
+ if (decoded.length === 0) {
115
+ throw new Error("empty");
116
+ }
117
+ return decoded;
118
+ } catch {
119
+ throw new Error(`Invalid ${label} encoding in DuckMind account bundle.`);
120
+ }
121
+ }
122
+
123
+ export function decryptCodexAccountsBundle(
124
+ bundle: EncryptedCodexAccountsBundle,
125
+ secret: string,
126
+ ): SyncManagedAccountsResult {
127
+ const trimmedSecret = secret.trim();
128
+ if (!trimmedSecret) {
129
+ throw new Error("Sync secret is required.");
130
+ }
131
+
132
+ if (bundle.algorithm !== "AES-256-GCM") {
133
+ throw new Error(
134
+ `Unsupported DuckMind bundle algorithm: ${bundle.algorithm}`,
135
+ );
136
+ }
137
+ if (bundle.kdf.name !== "PBKDF2") {
138
+ throw new Error(`Unsupported DuckMind bundle KDF: ${bundle.kdf.name}`);
139
+ }
140
+ if (bundle.kdf.hash !== "SHA-256") {
141
+ throw new Error(`Unsupported DuckMind bundle hash: ${bundle.kdf.hash}`);
142
+ }
143
+
144
+ const salt = decodeBase64(bundle.kdf.salt, "kdf.salt");
145
+ const iv = decodeBase64(bundle.iv, "iv");
146
+ const encrypted = decodeBase64(bundle.ciphertext, "ciphertext");
147
+ if (iv.length !== 12) {
148
+ throw new Error(
149
+ `Unsupported DuckMind IV length: expected 12 bytes, got ${iv.length}.`,
150
+ );
151
+ }
152
+ if (encrypted.length <= 16) {
153
+ throw new Error("DuckMind ciphertext is too short to contain a GCM tag.");
154
+ }
155
+
156
+ const key = pbkdf2Sync(
157
+ trimmedSecret,
158
+ salt,
159
+ bundle.kdf.iterations,
160
+ 32,
161
+ "sha256",
162
+ );
163
+ const authTag = encrypted.subarray(encrypted.length - 16);
164
+ const payload = encrypted.subarray(0, encrypted.length - 16);
165
+
166
+ let plaintext: Buffer;
167
+ try {
168
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
169
+ decipher.setAuthTag(authTag);
170
+ plaintext = Buffer.concat([decipher.update(payload), decipher.final()]);
171
+ } catch {
172
+ throw new Error(
173
+ "DuckMind sync secret is invalid or the bundle is corrupted.",
174
+ );
175
+ }
176
+
177
+ if (
178
+ typeof bundle.plaintext_bytes === "number" &&
179
+ plaintext.length !== bundle.plaintext_bytes
180
+ ) {
181
+ throw new Error(
182
+ "DuckMind account bundle size check failed after decryption.",
183
+ );
184
+ }
185
+
186
+ const plaintextSha256 = createHash("sha256").update(plaintext).digest("hex");
187
+ if (plaintextSha256 !== bundle.plaintext_sha256) {
188
+ throw new Error(
189
+ "DuckMind account bundle integrity check failed after decryption.",
190
+ );
191
+ }
192
+
193
+ let parsed: unknown;
194
+ try {
195
+ parsed = JSON.parse(plaintext.toString("utf8"));
196
+ } catch {
197
+ throw new Error(
198
+ "DuckMind account bundle decrypted successfully, but the plaintext is not valid JSON.",
199
+ );
200
+ }
201
+
202
+ return {
203
+ storage: coerceStorageData(parsed),
204
+ uploadedAt: bundle.uploaded_at,
205
+ fileName: bundle.file_name,
206
+ plaintextSha256,
207
+ sourceUrl: DUCKMIND_CODEX_ACCOUNTS_URL,
208
+ };
209
+ }
210
+
211
+ export async function syncManagedAccountsFromDuckMind(
212
+ secret: string,
213
+ options?: {
214
+ fetchImpl?: FetchLike;
215
+ sourceUrl?: string;
216
+ },
217
+ ): Promise<SyncManagedAccountsResult> {
218
+ const fetchImpl =
219
+ options?.fetchImpl ?? (globalThis.fetch as FetchLike | undefined);
220
+ if (!fetchImpl) {
221
+ throw new Error("Runtime fetch API is unavailable for MultiCodex sync.");
222
+ }
223
+
224
+ const sourceUrl = options?.sourceUrl ?? DUCKMIND_CODEX_ACCOUNTS_URL;
225
+ const response = await fetchImpl(sourceUrl, {
226
+ headers: {
227
+ Accept: "application/json, text/plain, */*",
228
+ "User-Agent": "Mozilla/5.0",
229
+ },
230
+ });
231
+ if (!response.ok) {
232
+ throw new Error(
233
+ `DuckMind account sync failed with HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ""}.`,
234
+ );
235
+ }
236
+
237
+ let parsed: unknown;
238
+ try {
239
+ parsed = JSON.parse(await response.text());
240
+ } catch {
241
+ throw new Error("DuckMind account sync returned malformed JSON.");
242
+ }
243
+
244
+ const decrypted = decryptCodexAccountsBundle(parseBundle(parsed), secret);
245
+ return {
246
+ ...decrypted,
247
+ sourceUrl,
248
+ };
249
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "lib": ["ES2022"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "types": ["node"],
13
+ "noEmit": true
14
+ },
15
+ "include": ["*.ts"],
16
+ "exclude": ["node_modules", "dist"]
17
+ }
@@ -1,9 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import type { ArtifactPaths } from "./types.ts";
5
-
6
- const TEMP_ARTIFACTS_DIR = path.join(os.tmpdir(), "dm-subagent-artifacts");
4
+ import { TEMP_ARTIFACTS_DIR, type ArtifactPaths } from "./types.ts";
7
5
  const CLEANUP_MARKER_FILE = ".last-cleanup";
8
6
 
9
7
  export function getArtifactsDir(sessionFile: string | null): string {
@@ -64,7 +62,10 @@ export function cleanupOldArtifacts(dir: string, maxAgeDays: number): void {
64
62
  if (stat.mtimeMs < cutoff) {
65
63
  fs.unlinkSync(filePath);
66
64
  }
67
- } catch {}
65
+ } catch {
66
+ // Artifact cleanup is best-effort housekeeping. Skip files that disappear
67
+ // or become unreadable while scanning so one bad entry does not block the rest.
68
+ }
68
69
  }
69
70
 
70
71
  fs.writeFileSync(markerPath, String(now));
@@ -80,6 +81,8 @@ export function cleanupAllArtifactDirs(maxAgeDays: number): void {
80
81
  try {
81
82
  dirs = fs.readdirSync(sessionsBase);
82
83
  } catch {
84
+ // Session artifact cleanup is best-effort. If the sessions root cannot be read,
85
+ // skip cleanup instead of failing extension startup.
83
86
  return;
84
87
  }
85
88
 
@@ -87,6 +90,9 @@ export function cleanupAllArtifactDirs(maxAgeDays: number): void {
87
90
  const artifactsDir = path.join(sessionsBase, dir, "subagent-artifacts");
88
91
  try {
89
92
  cleanupOldArtifacts(artifactsDir, maxAgeDays);
90
- } catch {}
93
+ } catch {
94
+ // Session cleanup is best-effort. Keep going so one unreadable session dir
95
+ // does not block cleanup for the rest.
96
+ }
91
97
  }
92
98
  }
@@ -23,6 +23,8 @@ import {
23
23
  type MaxOutputConfig,
24
24
  ASYNC_DIR,
25
25
  RESULTS_DIR,
26
+ TEMP_ROOT_DIR,
27
+ getAsyncConfigPath,
26
28
  resolveChildMaxSubagentDepth,
27
29
  } from "./types.ts";
28
30
 
@@ -113,7 +115,8 @@ export function isAsyncAvailable(): boolean {
113
115
  function spawnRunner(cfg: object, suffix: string, cwd: string): number | undefined {
114
116
  if (!jitiCliPath) return undefined;
115
117
 
116
- const cfgPath = path.join(os.tmpdir(), `dm-async-cfg-${suffix}.json`);
118
+ fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
119
+ const cfgPath = getAsyncConfigPath(suffix);
117
120
  fs.writeFileSync(cfgPath, JSON.stringify(cfg));
118
121
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
119
122
 
@@ -264,6 +267,7 @@ export function executeAsyncChain(
264
267
  asyncDir,
265
268
  sessionId: ctx.currentSessionId,
266
269
  piPackageRoot,
270
+ piArgv1: process.argv[1],
267
271
  worktreeSetupHook,
268
272
  worktreeSetupHookTimeoutMs,
269
273
  },
@@ -384,6 +388,7 @@ export function executeAsyncSingle(
384
388
  asyncDir,
385
389
  sessionId: ctx.currentSessionId,
386
390
  piPackageRoot,
391
+ piArgv1: process.argv[1],
387
392
  worktreeSetupHook,
388
393
  worktreeSetupHookTimeoutMs,
389
394
  },
@@ -155,7 +155,7 @@ async function runSingleAttempt(
155
155
  finish(-2);
156
156
  };
157
157
 
158
- const unsubscribeIntercomDetach = options.intercomEvents?.on(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
158
+ const unsubscribeIntercomDetach = options.intercomEvents?.on?.(INTERCOM_DETACH_REQUEST_EVENT, (payload) => {
159
159
  if (!options.allowIntercomDetach || detached || processClosed) return;
160
160
  if (!payload || typeof payload !== "object") return;
161
161
  const requestId = (payload as { requestId?: unknown }).requestId;
@@ -259,7 +259,7 @@ EXECUTION (use exactly ONE mode):
259
259
  CHAIN TEMPLATE VARIABLES (use in task strings):
260
260
  • {task} - The original task/request from the user
261
261
  • {previous} - Text response from the previous step (empty for first step)
262
- • {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/dm-chain-runs/abc123/)
262
+ • {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/pi-subagents-<scope>/chain-runs/abc123/)
263
263
 
264
264
  Example: { chain: [{agent:"scout", task:"Analyze {task}"}, {agent:"planner", task:"Plan based on {previous}"}] }
265
265
 
@@ -7,6 +7,7 @@ import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "
7
7
  const DEFAULT_INTERCOM_EXTENSION_DIR = path.join(os.homedir(), ".dm", "agent", "extensions", "pi-intercom");
8
8
  const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".dm", "agent", "intercom", "config.json");
9
9
  const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".dm", "agent", "extensions", "subagent");
10
+ const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
10
11
  const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
11
12
  const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `Use intercom only for coordination with the orchestrator session:
12
13
  - Need a decision or blocked: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
@@ -30,6 +31,13 @@ interface ResolveIntercomBridgeInput {
30
31
  settingsDir?: string;
31
32
  }
32
33
 
34
+ export function resolveIntercomSessionTarget(sessionName: string | undefined, sessionId: string): string {
35
+ const trimmedName = sessionName?.trim();
36
+ if (trimmedName) return trimmedName;
37
+ const normalizedSessionId = sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId;
38
+ return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
39
+ }
40
+
33
41
  export function resolveIntercomBridgeMode(value: unknown): IntercomBridgeMode {
34
42
  if (value === "off" || value === "always" || value === "fork-only") return value;
35
43
  return "always";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dm-subagents",
3
- "version": "0.13.3",
3
+ "version": "0.13.4",
4
4
  "description": "DM extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -83,7 +83,7 @@ export const SubagentParams = Type.Object({
83
83
  enum: ["fresh", "fork"],
84
84
  description: "'fresh' (default) or 'fork' to branch from parent session",
85
85
  })),
86
- chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: <tmpdir>/dm-chain-runs/ (auto-cleaned after 24h)" })),
86
+ chainDir: Type.Optional(Type.String({ description: "Persistent directory for chain artifacts. Default: a user-scoped temp directory under <tmpdir>/ (auto-cleaned after 24h)" })),
87
87
  async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
88
88
  agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
89
89
  cwd: Type.Optional(Type.String()),