@compilr-dev/sdk 0.29.6 → 0.29.7

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.
@@ -10,7 +10,7 @@
10
10
  * └── project-{id}.json # Per-project anchors
11
11
  */
12
12
  import { AnchorManager } from '@compilr-dev/agents';
13
- import type { Anchor, AnchorInput, AnchorQueryOptions } from '@compilr-dev/agents';
13
+ import type { Anchor, AnchorInput, AnchorPriority, AnchorQueryOptions } from '@compilr-dev/agents';
14
14
  import type { IAnchorService } from './services.js';
15
15
  export interface ProjectAnchorStoreConfig {
16
16
  /** Directory for anchor JSON files (e.g. ~/.compilr-dev/anchors/) */
@@ -36,6 +36,55 @@ export declare class ProjectAnchorStore {
36
36
  addAnchor(input: AnchorInput & {
37
37
  projectId?: string;
38
38
  }): Anchor;
39
+ /**
40
+ * Find an anchor by id, and say which file holds it.
41
+ *
42
+ * Searches the active project's manager then the global one — the same two the UI lists, and
43
+ * the same order `toService().remove` already uses. It deliberately does not sweep
44
+ * `this.managers`: those are lazily cached, so which ones exist depends on what happened
45
+ * earlier in the session, and the answer would drift with it.
46
+ *
47
+ * The null check on `currentProjectId` is defensive rather than behavioural — `String(null)`
48
+ * would key an empty manager and the lookup would fall through to global with the same
49
+ * result, and `loadPersistent` writes nothing for a file that does not exist. It is here so
50
+ * the intent is legible, not because a test can tell the difference. Mutating it away leaves
51
+ * every test green, and that is correct.
52
+ */
53
+ findAnchor(id: string, currentProjectId?: string | null): {
54
+ anchor: Anchor;
55
+ projectKey: string | null;
56
+ } | undefined;
57
+ /** Remove an anchor by id from whichever file holds it. */
58
+ removeAnchorById(id: string, currentProjectId?: string | null): {
59
+ removed: boolean;
60
+ scope?: 'global' | 'project';
61
+ };
62
+ /**
63
+ * Edit an anchor in place, keeping its id.
64
+ *
65
+ * ⚠️ WHY THE ID MATTERS. Without this the caller has to remove and re-add, which mints a new
66
+ * id — an open tab would then point at an anchor that no longer exists, and the row would
67
+ * jump in the list. `AnchorInput` accepts an id, so the re-add reuses the old one and both
68
+ * stay put.
69
+ *
70
+ * ⚠️ CHANGING SCOPE IS A MOVE BETWEEN FILES, not a field edit. Global anchors live in
71
+ * `global.json` and project ones in `project-<id>.json`, so switching "applies to" removes
72
+ * from one manager and adds to the other. The id survives that too.
73
+ *
74
+ * ⚠️ `createdAt` IS RESET, deliberately. `AnchorManager.add` always stamps `new Date()` and
75
+ * its `savePersistent` is private, so the only ways to keep the original are to mutate a
76
+ * stored object behind the manager's back or to widen `AnchorInput` in @compilr-dev/agents.
77
+ * Nothing displays `createdAt`; it is read only by the two eviction tie-breakers
78
+ * (`makeRoom`, `removeOldestLowPriority`), so on an edited anchor it now means "last edited"
79
+ * and that anchor is evicted last under budget pressure. Built-ins keep `new Date(0)` because
80
+ * they cannot be edited at all. See F-4.
81
+ */
82
+ updateAnchor(id: string, patch: {
83
+ content?: string;
84
+ priority?: AnchorPriority;
85
+ tags?: string[];
86
+ scope?: 'global' | 'project';
87
+ }, currentProjectId?: string | null): Anchor | undefined;
39
88
  /** Get all anchors across all cached managers. */
40
89
  getAllAnchors(options?: AnchorQueryOptions): Anchor[];
41
90
  /** Clear all anchors for a project and delete the persistence file. */
@@ -53,6 +102,29 @@ export declare class ProjectAnchorStore {
53
102
  projectCounts: Map<string, number>;
54
103
  totalTokens: number;
55
104
  };
105
+ /**
106
+ * Usage against the caps, per file.
107
+ *
108
+ * ⚠️ THE CAPS ARE PER FILE, NOT COMBINED. Each `AnchorManager` enforces `maxAnchors` and
109
+ * `maxTokens` on its own store, so global.json and project-<id>.json each get the full
110
+ * budget. A single meter summing the two and measuring it against one cap would read half
111
+ * full at the moment an `anchor_add` starts evicting — which is the opposite of the point,
112
+ * since the meter exists to show the limit before it bites.
113
+ *
114
+ * Both are returned and the caller shows the fuller one.
115
+ */
116
+ getUsage(projectId?: string | null): {
117
+ global: {
118
+ anchors: number;
119
+ tokens: number;
120
+ };
121
+ project: {
122
+ anchors: number;
123
+ tokens: number;
124
+ } | null;
125
+ maxAnchors: number;
126
+ maxTokens: number;
127
+ };
56
128
  /** Clear all cached managers (useful for testing or refresh). */
57
129
  clearCache(): void;
58
130
  /** Create an IAnchorService implementation for use with createPlatformTools(). */
@@ -52,6 +52,89 @@ export class ProjectAnchorStore {
52
52
  const manager = this.getManager(input.projectId ?? null);
53
53
  return manager.add(input);
54
54
  }
55
+ /**
56
+ * Find an anchor by id, and say which file holds it.
57
+ *
58
+ * Searches the active project's manager then the global one — the same two the UI lists, and
59
+ * the same order `toService().remove` already uses. It deliberately does not sweep
60
+ * `this.managers`: those are lazily cached, so which ones exist depends on what happened
61
+ * earlier in the session, and the answer would drift with it.
62
+ *
63
+ * The null check on `currentProjectId` is defensive rather than behavioural — `String(null)`
64
+ * would key an empty manager and the lookup would fall through to global with the same
65
+ * result, and `loadPersistent` writes nothing for a file that does not exist. It is here so
66
+ * the intent is legible, not because a test can tell the difference. Mutating it away leaves
67
+ * every test green, and that is correct.
68
+ */
69
+ findAnchor(id, currentProjectId) {
70
+ if (currentProjectId != null && currentProjectId !== '') {
71
+ const projectManager = this.getManager(currentProjectId);
72
+ const anchor = projectManager.get(id);
73
+ if (anchor)
74
+ return { anchor, projectKey: currentProjectId };
75
+ }
76
+ const anchor = this.getGlobalManager().get(id);
77
+ return anchor ? { anchor, projectKey: null } : undefined;
78
+ }
79
+ /** Remove an anchor by id from whichever file holds it. */
80
+ removeAnchorById(id, currentProjectId) {
81
+ const found = this.findAnchor(id, currentProjectId);
82
+ if (!found)
83
+ return { removed: false };
84
+ const removed = this.getManager(found.projectKey).remove(id);
85
+ return { removed, scope: found.projectKey === null ? 'global' : 'project' };
86
+ }
87
+ /**
88
+ * Edit an anchor in place, keeping its id.
89
+ *
90
+ * ⚠️ WHY THE ID MATTERS. Without this the caller has to remove and re-add, which mints a new
91
+ * id — an open tab would then point at an anchor that no longer exists, and the row would
92
+ * jump in the list. `AnchorInput` accepts an id, so the re-add reuses the old one and both
93
+ * stay put.
94
+ *
95
+ * ⚠️ CHANGING SCOPE IS A MOVE BETWEEN FILES, not a field edit. Global anchors live in
96
+ * `global.json` and project ones in `project-<id>.json`, so switching "applies to" removes
97
+ * from one manager and adds to the other. The id survives that too.
98
+ *
99
+ * ⚠️ `createdAt` IS RESET, deliberately. `AnchorManager.add` always stamps `new Date()` and
100
+ * its `savePersistent` is private, so the only ways to keep the original are to mutate a
101
+ * stored object behind the manager's back or to widen `AnchorInput` in @compilr-dev/agents.
102
+ * Nothing displays `createdAt`; it is read only by the two eviction tie-breakers
103
+ * (`makeRoom`, `removeOldestLowPriority`), so on an edited anchor it now means "last edited"
104
+ * and that anchor is evicted last under budget pressure. Built-ins keep `new Date(0)` because
105
+ * they cannot be edited at all. See F-4.
106
+ */
107
+ updateAnchor(id, patch, currentProjectId) {
108
+ const found = this.findAnchor(id, currentProjectId);
109
+ if (!found)
110
+ return undefined;
111
+ const { anchor, projectKey } = found;
112
+ let targetKey = projectKey;
113
+ if (patch.scope !== undefined) {
114
+ if (patch.scope === 'project') {
115
+ if (currentProjectId == null || currentProjectId === '') {
116
+ throw new Error('Cannot scope an anchor to a project: no active project. Use scope: "global" instead.');
117
+ }
118
+ targetKey = currentProjectId;
119
+ }
120
+ else {
121
+ targetKey = null;
122
+ }
123
+ }
124
+ // Remove first: re-adding the same id into the same manager would otherwise be an
125
+ // overwrite whose budget accounting counts the content twice.
126
+ this.getManager(projectKey).remove(id);
127
+ return this.getManager(targetKey).add({
128
+ id,
129
+ content: patch.content ?? anchor.content,
130
+ priority: patch.priority ?? anchor.priority,
131
+ scope: anchor.scope,
132
+ tags: patch.tags ?? anchor.tags,
133
+ metadata: anchor.metadata,
134
+ expiresAt: anchor.expiresAt,
135
+ projectId: targetKey ?? undefined,
136
+ });
137
+ }
55
138
  /** Get all anchors across all cached managers. */
56
139
  getAllAnchors(options) {
57
140
  const all = [];
@@ -127,6 +210,30 @@ export class ProjectAnchorStore {
127
210
  totalTokens,
128
211
  };
129
212
  }
213
+ /**
214
+ * Usage against the caps, per file.
215
+ *
216
+ * ⚠️ THE CAPS ARE PER FILE, NOT COMBINED. Each `AnchorManager` enforces `maxAnchors` and
217
+ * `maxTokens` on its own store, so global.json and project-<id>.json each get the full
218
+ * budget. A single meter summing the two and measuring it against one cap would read half
219
+ * full at the moment an `anchor_add` starts evicting — which is the opposite of the point,
220
+ * since the meter exists to show the limit before it bites.
221
+ *
222
+ * Both are returned and the caller shows the fuller one.
223
+ */
224
+ getUsage(projectId) {
225
+ const globalManager = this.getGlobalManager();
226
+ const hasProject = projectId != null && projectId !== '';
227
+ const projectManager = hasProject ? this.getManager(projectId) : null;
228
+ return {
229
+ global: { anchors: globalManager.size, tokens: globalManager.getTotalTokens() },
230
+ project: projectManager === null
231
+ ? null
232
+ : { anchors: projectManager.size, tokens: projectManager.getTotalTokens() },
233
+ maxAnchors: this.config.maxAnchors ?? 50,
234
+ maxTokens: this.config.maxTokens ?? 4000,
235
+ };
236
+ }
130
237
  /** Clear all cached managers (useful for testing or refresh). */
131
238
  clearCache() {
132
239
  this.managers.clear();
@@ -164,6 +271,32 @@ export class ProjectAnchorStore {
164
271
  tags: anchor.tags,
165
272
  });
166
273
  },
274
+ update: (id, patch) => {
275
+ const projId = this.config.getCurrentProjectId();
276
+ const updated = this.updateAnchor(id, patch, projId == null ? null : String(projId));
277
+ if (!updated)
278
+ return Promise.resolve(null);
279
+ return Promise.resolve({
280
+ id: updated.id,
281
+ content: updated.content,
282
+ priority: updated.priority,
283
+ scope: updated.projectId === undefined ? 'global' : 'project',
284
+ tags: updated.tags,
285
+ });
286
+ },
287
+ get: (id) => {
288
+ const projId = this.config.getCurrentProjectId();
289
+ const found = this.findAnchor(id, projId == null ? null : String(projId));
290
+ if (!found)
291
+ return Promise.resolve(null);
292
+ return Promise.resolve({
293
+ id: found.anchor.id,
294
+ content: found.anchor.content,
295
+ priority: found.anchor.priority,
296
+ scope: found.projectKey === null ? 'global' : 'project',
297
+ tags: found.anchor.tags,
298
+ });
299
+ },
167
300
  remove: (id) => {
168
301
  let removed = false;
169
302
  let scope;
@@ -23,6 +23,21 @@ export interface IAnchorService {
23
23
  removed: boolean;
24
24
  scope?: string;
25
25
  }>;
26
+ /**
27
+ * Edit an anchor in place, keeping its id.
28
+ *
29
+ * Optional so existing hosts are unaffected. Without it a caller has to remove and re-add,
30
+ * which mints a new id — anything holding the old one (an open tab, a row in a list) is then
31
+ * pointing at an anchor that no longer exists.
32
+ */
33
+ update?(id: string, patch: {
34
+ content?: string;
35
+ priority?: AnchorPriority;
36
+ tags?: string[];
37
+ scope?: 'global' | 'project';
38
+ }): Promise<AnchorData | null>;
39
+ /** One anchor by id, or null. */
40
+ get?(id: string): Promise<AnchorData | null>;
26
41
  list(options?: {
27
42
  scope?: 'global' | 'project' | 'all';
28
43
  priority?: AnchorPriority;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.29.6",
3
+ "version": "0.29.7",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",