runwork 0.15.0 → 0.15.1
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/dist/agents/__tests__/claude-code-managed-block.test.js +52 -44
- package/dist/agents/claude-code.d.ts +11 -5
- package/dist/agents/claude-code.js +50 -51
- package/dist/agents/cursor.d.ts +2 -15
- package/dist/agents/cursor.js +3 -33
- package/dist/agents/defaults-merge.d.ts +15 -0
- package/dist/agents/defaults-merge.js +19 -0
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
5
|
import { ClaudeCodeAdapter } from '../claude-code.js';
|
|
6
|
+
const MARKER = '# runwork-managed';
|
|
6
7
|
let homeDir;
|
|
7
8
|
let originalHome;
|
|
8
9
|
beforeEach(() => {
|
|
@@ -10,7 +11,6 @@ beforeEach(() => {
|
|
|
10
11
|
homeDir = join(tmpdir(), `runwork-cc-managed-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
11
12
|
mkdirSync(join(homeDir, '.claude'), { recursive: true });
|
|
12
13
|
process.env.HOME = homeDir;
|
|
13
|
-
// Reset the homedir() cache by mocking it via env (Node's os.homedir reads HOME on macOS/Linux).
|
|
14
14
|
});
|
|
15
15
|
afterEach(() => {
|
|
16
16
|
if (originalHome)
|
|
@@ -23,75 +23,83 @@ afterEach(() => {
|
|
|
23
23
|
catch { /* ignore */ }
|
|
24
24
|
vi.restoreAllMocks();
|
|
25
25
|
});
|
|
26
|
+
function settingsPath() {
|
|
27
|
+
return join(homeDir, '.claude', 'settings.json');
|
|
28
|
+
}
|
|
26
29
|
function writeSettings(settings) {
|
|
27
|
-
writeFileSync(
|
|
30
|
+
writeFileSync(settingsPath(), JSON.stringify(settings, null, 2));
|
|
31
|
+
}
|
|
32
|
+
function readSettings() {
|
|
33
|
+
return JSON.parse(readFileSync(settingsPath(), 'utf-8'));
|
|
28
34
|
}
|
|
29
|
-
describe('ClaudeCodeAdapter.readManagedBlock (
|
|
35
|
+
describe('ClaudeCodeAdapter.readManagedBlock (markerless)', () => {
|
|
30
36
|
it('returns undefined when settings.json is missing', async () => {
|
|
31
37
|
const adapter = new ClaudeCodeAdapter();
|
|
32
|
-
|
|
33
|
-
expect(result).toBeUndefined();
|
|
38
|
+
expect(await adapter.readManagedBlock('user')).toBeUndefined();
|
|
34
39
|
});
|
|
35
|
-
it('returns
|
|
36
|
-
writeSettings({
|
|
40
|
+
it('returns undefined even when a legacy marker is present (no on-disk read)', async () => {
|
|
41
|
+
writeSettings({ permissions: { allow: [MARKER, 'Bash(runwork *)'], deny: [] } });
|
|
37
42
|
const adapter = new ClaudeCodeAdapter();
|
|
38
|
-
|
|
39
|
-
expect(result).toEqual({ allow: [], deny: [] });
|
|
43
|
+
expect(await adapter.readManagedBlock('user')).toBeUndefined();
|
|
40
44
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const result = await adapter.readManagedBlock('user');
|
|
45
|
-
expect(result).toEqual({ allow: [], deny: [] });
|
|
46
|
-
});
|
|
47
|
-
it('returns entries after marker in allow', async () => {
|
|
45
|
+
});
|
|
46
|
+
describe('ClaudeCodeAdapter.writeAgentConfig legacy-marker migration', () => {
|
|
47
|
+
it('strips the legacy marker from allow and deny when injecting new rules', async () => {
|
|
48
48
|
writeSettings({
|
|
49
49
|
permissions: {
|
|
50
|
-
allow: [
|
|
51
|
-
deny: [],
|
|
50
|
+
allow: [MARKER, 'Bash(runwork *)', 'Bash(git status)'],
|
|
51
|
+
deny: [MARKER, 'Bash(rm -rf *)'],
|
|
52
52
|
},
|
|
53
53
|
});
|
|
54
54
|
const adapter = new ClaudeCodeAdapter();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
expect(
|
|
55
|
+
await adapter.writeAgentConfig({ permissionRules: { allow: ['Bash(runwork *)', 'Bash(git status)'], deny: ['Bash(rm -rf *)'] } }, 'user', { allow: ['Bash(runwork *)', 'Bash(git status)'], deny: ['Bash(rm -rf *)'] });
|
|
56
|
+
const perms = readSettings().permissions;
|
|
57
|
+
expect(perms.allow).not.toContain(MARKER);
|
|
58
|
+
expect(perms.deny).not.toContain(MARKER);
|
|
59
|
+
expect(perms.allow).toEqual(['Bash(runwork *)', 'Bash(git status)']);
|
|
60
|
+
expect(perms.deny).toEqual(['Bash(rm -rf *)']);
|
|
58
61
|
});
|
|
59
|
-
it('
|
|
62
|
+
it('strips the legacy marker even when there are no new permission rules', async () => {
|
|
60
63
|
writeSettings({
|
|
61
64
|
permissions: {
|
|
62
|
-
allow: [],
|
|
63
|
-
deny: [
|
|
65
|
+
allow: ['Bash(user-custom)', MARKER, 'Bash(runwork *)'],
|
|
66
|
+
deny: [MARKER],
|
|
64
67
|
},
|
|
65
68
|
});
|
|
66
69
|
const adapter = new ClaudeCodeAdapter();
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
await adapter.writeAgentConfig({}, 'user');
|
|
71
|
+
const perms = readSettings().permissions;
|
|
72
|
+
expect(perms.allow).toEqual(['Bash(user-custom)', 'Bash(runwork *)']);
|
|
73
|
+
expect(perms.deny).toEqual([]);
|
|
69
74
|
});
|
|
70
|
-
|
|
75
|
+
});
|
|
76
|
+
describe('ClaudeCodeAdapter.writeAgentConfig markerless merge', () => {
|
|
77
|
+
it('preserves user entries and subtracts the baseline before re-injecting', async () => {
|
|
71
78
|
writeSettings({
|
|
72
79
|
permissions: {
|
|
73
|
-
allow: ['Bash(
|
|
74
|
-
deny: [],
|
|
80
|
+
allow: ['Bash(user-custom)', 'Bash(runwork *)', 'Bash(old-default)'],
|
|
75
81
|
},
|
|
76
82
|
});
|
|
77
83
|
const adapter = new ClaudeCodeAdapter();
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
await adapter.writeAgentConfig({ permissionRules: { allow: ['Bash(runwork *)', 'Bash(git status)'] } }, 'user', { allow: ['Bash(runwork *)', 'Bash(old-default)'] });
|
|
85
|
+
// user entry kept; baseline-only entry (old-default) dropped; new managed set appended
|
|
86
|
+
expect(readSettings().permissions.allow).toEqual([
|
|
87
|
+
'Bash(user-custom)',
|
|
88
|
+
'Bash(runwork *)',
|
|
89
|
+
'Bash(git status)',
|
|
90
|
+
]);
|
|
80
91
|
});
|
|
81
|
-
it('
|
|
82
|
-
writeFileSync(join(homeDir, '.claude', 'settings.json'), '{ not valid json');
|
|
92
|
+
it('writes model preference and defaultMode', async () => {
|
|
83
93
|
const adapter = new ClaudeCodeAdapter();
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
await adapter.writeAgentConfig({ modelPreference: 'claude-sonnet-4-6', permissionRules: { allow: ['Bash(runwork *)'], defaultMode: 'acceptEdits' } }, 'user');
|
|
95
|
+
const settings = readSettings();
|
|
96
|
+
expect(settings.model).toBe('claude-sonnet-4-6');
|
|
97
|
+
expect(settings.permissions.defaultMode).toBe('acceptEdits');
|
|
98
|
+
expect(settings.permissions.allow).toEqual(['Bash(runwork *)']);
|
|
86
99
|
});
|
|
87
|
-
it('
|
|
88
|
-
writeSettings({
|
|
89
|
-
permissions: {
|
|
90
|
-
allow: ['# runwork-managed', 'Bash(runwork *)', 42, null, 'Bash(git status)'],
|
|
91
|
-
},
|
|
92
|
-
});
|
|
100
|
+
it('does not create an empty settings file when there is nothing to write', async () => {
|
|
93
101
|
const adapter = new ClaudeCodeAdapter();
|
|
94
|
-
|
|
95
|
-
expect(
|
|
102
|
+
await adapter.writeAgentConfig({}, 'user');
|
|
103
|
+
expect(existsSync(settingsPath())).toBe(false);
|
|
96
104
|
});
|
|
97
105
|
});
|
|
@@ -31,11 +31,17 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
|
|
|
31
31
|
writeBuiltInHooks(scope: 'project' | 'user'): Promise<void>;
|
|
32
32
|
writeInstructionHint(hint: string, scope: 'project' | 'user'): Promise<void>;
|
|
33
33
|
writeTeamInstructions(instructions: string, scope: 'project' | 'user'): Promise<void>;
|
|
34
|
-
writeAgentConfig(config: AgentConfigOverride, scope: 'project' | 'user'
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
writeAgentConfig(config: AgentConfigOverride, scope: 'project' | 'user', baseline?: {
|
|
35
|
+
allow?: string[];
|
|
36
|
+
deny?: string[];
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Markerless adapter: removal detection relies on the `baseline`
|
|
40
|
+
* (state.lastInjected) passed to writeAgentConfig, exactly like Cursor, since
|
|
41
|
+
* the adapter no longer writes an on-disk sentinel to delimit its entries.
|
|
42
|
+
* Returning undefined tells the sync loop to skip on-disk removal detection.
|
|
43
|
+
*/
|
|
44
|
+
readManagedBlock(_scope: 'project' | 'user'): Promise<{
|
|
39
45
|
allow?: string[];
|
|
40
46
|
deny?: string[];
|
|
41
47
|
} | undefined>;
|
|
@@ -7,8 +7,17 @@ import { buildSkillMd } from './types.js';
|
|
|
7
7
|
import { mergeJsonMcpServers, readJsonConfig, writeJsonConfig, removeRunworkMcpServers } from './utils/json-config.js';
|
|
8
8
|
import { writeHintToFile, writeTeamInstructionsToFile, removeHintFromFile, removeTeamInstructionsFromFile } from './utils/instruction-hint.js';
|
|
9
9
|
import { SESSION_START_HOOK_SCRIPT } from './session-start-hook.js';
|
|
10
|
+
import { mergeMarkerless } from './defaults-merge.js';
|
|
10
11
|
const PLUGIN_NAME = 'runwork';
|
|
11
12
|
const PLUGIN_VERSION = '1.0.0';
|
|
13
|
+
/**
|
|
14
|
+
* Legacy in-array sentinel that older CLI versions injected at the head of the
|
|
15
|
+
* managed permission block. Claude Code >= 2.1.x validates every allow/deny
|
|
16
|
+
* entry as a tool rule and rejects this with "matches no known tool" on
|
|
17
|
+
* startup, so the adapter is now markerless and actively strips this marker
|
|
18
|
+
* from any existing user's settings on their next sync.
|
|
19
|
+
*/
|
|
20
|
+
const LEGACY_MANAGED_MARKER = '# runwork-managed';
|
|
12
21
|
function getPluginJson() {
|
|
13
22
|
return {
|
|
14
23
|
name: PLUGIN_NAME,
|
|
@@ -215,18 +224,31 @@ export class ClaudeCodeAdapter {
|
|
|
215
224
|
}
|
|
216
225
|
}
|
|
217
226
|
// ── Agent config (model, permissions) ──────────────────────────────
|
|
218
|
-
async writeAgentConfig(config, scope) {
|
|
227
|
+
async writeAgentConfig(config, scope, baseline) {
|
|
219
228
|
const settingsPath = scope === 'project'
|
|
220
229
|
? join(process.cwd(), '.claude', 'settings.json')
|
|
221
230
|
: join(homedir(), '.claude', 'settings.json');
|
|
231
|
+
const hadFile = existsSync(settingsPath);
|
|
222
232
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
223
233
|
let settings = {};
|
|
224
|
-
if (
|
|
234
|
+
if (hadFile) {
|
|
225
235
|
try {
|
|
226
236
|
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
227
237
|
}
|
|
228
238
|
catch { /* start fresh */ }
|
|
229
239
|
}
|
|
240
|
+
// Retro-fix migration: strip the legacy in-array sentinel from any existing
|
|
241
|
+
// settings, regardless of whether we inject new rules this sync. This is
|
|
242
|
+
// what removes the broken "# runwork-managed" entry that makes Claude Code
|
|
243
|
+
// >= 2.1.x print "matches no known tool" on startup.
|
|
244
|
+
if (settings.permissions && typeof settings.permissions === 'object') {
|
|
245
|
+
for (const key of ['allow', 'deny']) {
|
|
246
|
+
const arr = settings.permissions[key];
|
|
247
|
+
if (Array.isArray(arr)) {
|
|
248
|
+
settings.permissions[key] = arr.filter((x) => x !== LEGACY_MANAGED_MARKER);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
230
252
|
if (config.modelPreference) {
|
|
231
253
|
settings.model = config.modelPreference;
|
|
232
254
|
}
|
|
@@ -234,55 +256,32 @@ export class ClaudeCodeAdapter {
|
|
|
234
256
|
if (!settings.permissions)
|
|
235
257
|
settings.permissions = {};
|
|
236
258
|
const perms = settings.permissions;
|
|
237
|
-
|
|
259
|
+
// Markerless merge: subtract our previous contribution (baseline) to leave
|
|
260
|
+
// the user's own entries intact, then append the freshly resolved set.
|
|
238
261
|
if (config.permissionRules.allow) {
|
|
239
|
-
|
|
240
|
-
perms.allow = [...cleanExisting, MANAGED_MARKER, ...config.permissionRules.allow];
|
|
262
|
+
perms.allow = mergeMarkerless(Array.isArray(perms.allow) ? perms.allow : [], baseline?.allow ?? [], config.permissionRules.allow);
|
|
241
263
|
}
|
|
242
264
|
if (config.permissionRules.deny) {
|
|
243
|
-
|
|
244
|
-
perms.deny = [...cleanExisting, MANAGED_MARKER, ...config.permissionRules.deny];
|
|
265
|
+
perms.deny = mergeMarkerless(Array.isArray(perms.deny) ? perms.deny : [], baseline?.deny ?? [], config.permissionRules.deny);
|
|
245
266
|
}
|
|
246
267
|
if (config.permissionRules.defaultMode) {
|
|
247
268
|
perms.defaultMode = config.permissionRules.defaultMode;
|
|
248
269
|
}
|
|
249
270
|
}
|
|
271
|
+
// Nothing to persist and no file to clean up: avoid creating an empty file.
|
|
272
|
+
if (!hadFile && !config.modelPreference && !config.permissionRules)
|
|
273
|
+
return;
|
|
250
274
|
mkdirSync(join(settingsPath, '..'), { recursive: true });
|
|
251
275
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
252
276
|
}
|
|
253
|
-
/**
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
async readManagedBlock(
|
|
260
|
-
|
|
261
|
-
? join(process.cwd(), '.claude', 'settings.json')
|
|
262
|
-
: join(homedir(), '.claude', 'settings.json');
|
|
263
|
-
if (!existsSync(settingsPath))
|
|
264
|
-
return undefined;
|
|
265
|
-
const MANAGED_MARKER = '# runwork-managed';
|
|
266
|
-
try {
|
|
267
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
268
|
-
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
269
|
-
const perms = settings?.permissions;
|
|
270
|
-
if (!perms || typeof perms !== 'object')
|
|
271
|
-
return { allow: [], deny: [] };
|
|
272
|
-
const extractAfter = (arr) => {
|
|
273
|
-
if (!Array.isArray(arr))
|
|
274
|
-
return [];
|
|
275
|
-
const idx = arr.indexOf(MANAGED_MARKER);
|
|
276
|
-
return idx >= 0 ? arr.slice(idx + 1).filter((x) => typeof x === 'string') : [];
|
|
277
|
-
};
|
|
278
|
-
return {
|
|
279
|
-
allow: extractAfter(perms.allow),
|
|
280
|
-
deny: extractAfter(perms.deny),
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
return undefined;
|
|
285
|
-
}
|
|
277
|
+
/**
|
|
278
|
+
* Markerless adapter: removal detection relies on the `baseline`
|
|
279
|
+
* (state.lastInjected) passed to writeAgentConfig, exactly like Cursor, since
|
|
280
|
+
* the adapter no longer writes an on-disk sentinel to delimit its entries.
|
|
281
|
+
* Returning undefined tells the sync loop to skip on-disk removal detection.
|
|
282
|
+
*/
|
|
283
|
+
async readManagedBlock(_scope) {
|
|
284
|
+
return undefined;
|
|
286
285
|
}
|
|
287
286
|
// ── Cleanup (uninstall) ────────────────────────────────────────────
|
|
288
287
|
async cleanup(scope, manifest) {
|
|
@@ -320,18 +319,18 @@ export class ClaudeCodeAdapter {
|
|
|
320
319
|
if (existsSync(settingsPath)) {
|
|
321
320
|
try {
|
|
322
321
|
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
323
|
-
// Remove managed permission entries
|
|
322
|
+
// Remove managed permission entries. Legacy installs carry the
|
|
323
|
+
// in-array marker, so everything from the marker onward is ours.
|
|
324
|
+
// Always also drop any stray marker so uninstall never leaves the
|
|
325
|
+
// entry that breaks Claude Code startup.
|
|
324
326
|
if (settings.permissions) {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const idx = settings.permissions.deny.indexOf(MANAGED_MARKER);
|
|
333
|
-
if (idx >= 0)
|
|
334
|
-
settings.permissions.deny = settings.permissions.deny.slice(0, idx);
|
|
327
|
+
for (const key of ['allow', 'deny']) {
|
|
328
|
+
const arr = settings.permissions[key];
|
|
329
|
+
if (!Array.isArray(arr))
|
|
330
|
+
continue;
|
|
331
|
+
const idx = arr.indexOf(LEGACY_MANAGED_MARKER);
|
|
332
|
+
settings.permissions[key] = (idx >= 0 ? arr.slice(0, idx) : arr)
|
|
333
|
+
.filter((x) => x !== LEGACY_MANAGED_MARKER);
|
|
335
334
|
}
|
|
336
335
|
}
|
|
337
336
|
// Remove plugin enablement
|
package/dist/agents/cursor.d.ts
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
import type { AgentAdapter, AgentConfigOverride, AgentUsageStats, CleanupManifest, McpServerEntry, SkillFile } from './types.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* user-added entries.
|
|
5
|
-
*
|
|
6
|
-
* existing = what's currently on disk
|
|
7
|
-
* baseline = what we wrote on the previous sync (state.lastInjected). Items
|
|
8
|
-
* present here are assumed to be ours; subtracting them from
|
|
9
|
-
* existing leaves entries the user added themselves.
|
|
10
|
-
* incoming = the new managed set to inject (defaults already filtered for
|
|
11
|
-
* opt-outs, merged with team rules)
|
|
12
|
-
*
|
|
13
|
-
* If baseline is empty (bootstrap or first run with this CLI version), we
|
|
14
|
-
* assume nothing currently on disk is ours and preserve all existing entries.
|
|
15
|
-
*/
|
|
16
|
-
export declare function mergeMarkerless(existing: string[], baseline: string[], incoming: string[]): string[];
|
|
2
|
+
import { mergeMarkerless } from './defaults-merge.js';
|
|
3
|
+
export { mergeMarkerless };
|
|
17
4
|
export declare class CursorAdapter implements AgentAdapter {
|
|
18
5
|
name: string;
|
|
19
6
|
slug: string;
|
package/dist/agents/cursor.js
CHANGED
|
@@ -5,39 +5,9 @@ import { execFileSync } from '../utils/subprocess.js';
|
|
|
5
5
|
import { querySqlite, openWritableSqlite } from '../utils/sqlite.js';
|
|
6
6
|
import { whichBinary } from '../utils/which.js';
|
|
7
7
|
import { mergeJsonMcpServers, removeRunworkMcpServers, readJsonConfig, writeJsonConfig } from './utils/json-config.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
* existing = what's currently on disk
|
|
13
|
-
* baseline = what we wrote on the previous sync (state.lastInjected). Items
|
|
14
|
-
* present here are assumed to be ours; subtracting them from
|
|
15
|
-
* existing leaves entries the user added themselves.
|
|
16
|
-
* incoming = the new managed set to inject (defaults already filtered for
|
|
17
|
-
* opt-outs, merged with team rules)
|
|
18
|
-
*
|
|
19
|
-
* If baseline is empty (bootstrap or first run with this CLI version), we
|
|
20
|
-
* assume nothing currently on disk is ours and preserve all existing entries.
|
|
21
|
-
*/
|
|
22
|
-
export function mergeMarkerless(existing, baseline, incoming) {
|
|
23
|
-
const baselineSet = new Set(baseline);
|
|
24
|
-
const userEntries = existing.filter((item) => !baselineSet.has(item));
|
|
25
|
-
const seen = new Set();
|
|
26
|
-
const result = [];
|
|
27
|
-
for (const item of userEntries) {
|
|
28
|
-
if (!seen.has(item)) {
|
|
29
|
-
seen.add(item);
|
|
30
|
-
result.push(item);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
for (const item of incoming) {
|
|
34
|
-
if (!seen.has(item)) {
|
|
35
|
-
seen.add(item);
|
|
36
|
-
result.push(item);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return result;
|
|
40
|
-
}
|
|
8
|
+
import { mergeMarkerless } from './defaults-merge.js';
|
|
9
|
+
// Re-exported for backward compatibility with existing importers/tests.
|
|
10
|
+
export { mergeMarkerless };
|
|
41
11
|
export class CursorAdapter {
|
|
42
12
|
name = 'Cursor';
|
|
43
13
|
slug = 'cursor';
|
|
@@ -20,6 +20,21 @@ export declare function computeApplicable(baked: string[], optOuts: string[]): s
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function mergeAllow(applicableDefaults: string[], teamRules: string[]): string[];
|
|
22
22
|
export declare function unique(items: string[]): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Merge a new managed list into a markerless on-disk array while preserving
|
|
25
|
+
* user-added entries. Shared by all markerless adapters (Cursor, Claude Code).
|
|
26
|
+
*
|
|
27
|
+
* existing = what's currently on disk
|
|
28
|
+
* baseline = what we wrote on the previous sync (state.lastInjected). Items
|
|
29
|
+
* present here are assumed to be ours; subtracting them from
|
|
30
|
+
* existing leaves entries the user added themselves.
|
|
31
|
+
* incoming = the new managed set to inject (defaults already filtered for
|
|
32
|
+
* opt-outs, merged with team rules)
|
|
33
|
+
*
|
|
34
|
+
* If baseline is empty (bootstrap or first run with this CLI version), we
|
|
35
|
+
* assume nothing currently on disk is ours and preserve all existing entries.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mergeMarkerless(existing: string[], baseline: string[], incoming: string[]): string[];
|
|
23
38
|
/** True if no per-agent state has ever been recorded — first sync after upgrade or fresh install. */
|
|
24
39
|
export declare function isBootstrap(state: AgentDefaultsState | undefined): boolean;
|
|
25
40
|
export interface ResolveResult {
|
|
@@ -45,6 +45,25 @@ export function unique(items) {
|
|
|
45
45
|
}
|
|
46
46
|
return out;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Merge a new managed list into a markerless on-disk array while preserving
|
|
50
|
+
* user-added entries. Shared by all markerless adapters (Cursor, Claude Code).
|
|
51
|
+
*
|
|
52
|
+
* existing = what's currently on disk
|
|
53
|
+
* baseline = what we wrote on the previous sync (state.lastInjected). Items
|
|
54
|
+
* present here are assumed to be ours; subtracting them from
|
|
55
|
+
* existing leaves entries the user added themselves.
|
|
56
|
+
* incoming = the new managed set to inject (defaults already filtered for
|
|
57
|
+
* opt-outs, merged with team rules)
|
|
58
|
+
*
|
|
59
|
+
* If baseline is empty (bootstrap or first run with this CLI version), we
|
|
60
|
+
* assume nothing currently on disk is ours and preserve all existing entries.
|
|
61
|
+
*/
|
|
62
|
+
export function mergeMarkerless(existing, baseline, incoming) {
|
|
63
|
+
const baselineSet = new Set(baseline);
|
|
64
|
+
const userEntries = existing.filter((item) => !baselineSet.has(item));
|
|
65
|
+
return unique([...userEntries, ...incoming]);
|
|
66
|
+
}
|
|
48
67
|
/** True if no per-agent state has ever been recorded — first sync after upgrade or fresh install. */
|
|
49
68
|
export function isBootstrap(state) {
|
|
50
69
|
return state === undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.15.
|
|
1
|
+
export declare const VERSION = "0.15.1";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/embed-types.ts -- do not edit
|
|
2
|
-
export const VERSION = "0.15.
|
|
2
|
+
export const VERSION = "0.15.1";
|
package/package.json
CHANGED