@lanes-sh/link 0.6.11 → 0.7.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 (201) hide show
  1. package/README.md +62 -45
  2. package/instructions/skills/lanes-link/SKILL.md +25 -0
  3. package/package.json +3 -2
  4. package/src/audit/conformance.ts +1 -1
  5. package/src/cli/argv.ts +14 -1
  6. package/src/cli/commands/connect/acquire.ts +100 -0
  7. package/src/cli/commands/connect/custom/index.ts +1 -1
  8. package/src/cli/commands/connect/declare.ts +19 -0
  9. package/src/cli/commands/connect/index.ts +54 -56
  10. package/src/cli/commands/connect/requirements.ts +23 -1
  11. package/src/cli/commands/connect/setup.ts +1 -1
  12. package/src/cli/commands/connect/variables.ts +220 -0
  13. package/src/cli/commands/knowledge/index.ts +22 -4
  14. package/src/cli/commands/knowledge/migrate.ts +61 -43
  15. package/src/cli/commands/operate/tools.ts +1 -1
  16. package/src/cli/commands/owner/entities.ts +244 -0
  17. package/src/cli/commands/owner/shared.ts +14 -0
  18. package/src/cli/commands/owner/vault.ts +2 -2
  19. package/src/cli/commands/owner.ts +13 -3
  20. package/src/cli/commands/setup.ts +12 -0
  21. package/src/cli/config-edit.ts +16 -6
  22. package/src/cli/config-repair.ts +10 -0
  23. package/src/cli/dispatch-owner.ts +30 -1
  24. package/src/cli/main.ts +2 -1
  25. package/src/cli/runtime/open.ts +14 -7
  26. package/src/cli/runtime/registry.ts +6 -1
  27. package/src/cli/selection.ts +11 -0
  28. package/src/cli/usage.ts +13 -2
  29. package/src/connectivity/auth/strategy/index.ts +1 -1
  30. package/src/connectivity/capability.ts +1 -1
  31. package/src/connectivity/context.ts +1 -1
  32. package/src/connectivity/index.ts +1 -1
  33. package/src/connectivity/mail/attachments.ts +42 -73
  34. package/src/connectivity/mail/content-type.ts +71 -0
  35. package/src/connectivity/mail/index.ts +1 -1
  36. package/src/connectivity/manifest/provider.ts +50 -0
  37. package/src/connectivity/manifest/variables.ts +173 -0
  38. package/src/connectivity/transports/factory.ts +74 -1
  39. package/src/connectivity/transports/http/index.ts +8 -2
  40. package/src/connectivity/transports/http/keys.ts +83 -0
  41. package/src/connectivity/transports/imap/attachment.ts +53 -10
  42. package/src/connectivity/transports/imap/capabilities.ts +28 -0
  43. package/src/connectivity/transports/imap/download.ts +111 -0
  44. package/src/connectivity/transports/imap/index.ts +13 -0
  45. package/src/connectivity/transports/imap/operations.ts +1 -0
  46. package/src/deployments/adapters/s3.ts +1 -1
  47. package/src/deployments/gcp/provision.ts +1 -1
  48. package/src/deployments/knowledge.ts +44 -18
  49. package/src/deployments/prepare.ts +1 -1
  50. package/src/deployments/serving.ts +1 -1
  51. package/src/dispatch/dispatch.ts +17 -10
  52. package/src/dispatch/staging.ts +76 -1
  53. package/src/profile/knowledge.ts +19 -11
  54. package/src/profile/schema.ts +2 -2
  55. package/src/providers/README.md +189 -0
  56. package/src/providers/airtable/index.ts +10 -0
  57. package/src/providers/algolia/index.ts +10 -0
  58. package/src/providers/amplitude/index.ts +10 -0
  59. package/src/providers/apify/index.ts +10 -0
  60. package/src/providers/asana/index.ts +10 -0
  61. package/src/providers/assets/provider.ts +11 -4
  62. package/src/providers/atlassian/index.ts +19 -0
  63. package/src/providers/attio/index.ts +10 -0
  64. package/src/providers/betterstack/index.ts +10 -0
  65. package/src/providers/box/index.ts +64 -0
  66. package/src/providers/brightdata/index.ts +10 -0
  67. package/src/providers/buildkite/index.ts +10 -0
  68. package/src/providers/bunq/index.ts +2 -2
  69. package/src/providers/calendly/index.ts +10 -0
  70. package/src/providers/canva/index.ts +10 -0
  71. package/src/providers/circleci/index.ts +10 -0
  72. package/src/providers/clickup/index.ts +10 -0
  73. package/src/providers/close/index.ts +10 -0
  74. package/src/providers/cloudflare_bindings/index.ts +14 -0
  75. package/src/providers/cloudflare_observability/index.ts +10 -0
  76. package/src/providers/contentful/index.ts +10 -0
  77. package/src/providers/custom/load.ts +1 -1
  78. package/src/providers/datadog/index.ts +10 -0
  79. package/src/providers/discord/index.ts +1 -1
  80. package/src/providers/discord/specs/discord.v10.json +0 -88
  81. package/src/providers/discord/specs/vendor.ts +1 -1
  82. package/src/providers/dropbox/index.ts +10 -0
  83. package/src/providers/entities/catalogue.ts +366 -0
  84. package/src/providers/entities/find.ts +279 -0
  85. package/src/providers/entities/provider.ts +334 -0
  86. package/src/providers/entities/render.ts +142 -0
  87. package/src/providers/entities/store.ts +276 -0
  88. package/src/providers/entities/writes.ts +279 -0
  89. package/src/providers/example/provider.ts +1 -1
  90. package/src/providers/expensify/index.ts +10 -0
  91. package/src/providers/fastmail/calendar/index.ts +21 -0
  92. package/src/providers/fastmail/contacts/index.ts +17 -0
  93. package/src/providers/fastmail/index.ts +4 -0
  94. package/src/providers/fastmail/mail/index.ts +38 -0
  95. package/src/providers/fastmail/shared/setup.ts +52 -0
  96. package/src/providers/figma/index.ts +10 -0
  97. package/src/providers/fireflies/index.ts +10 -0
  98. package/src/providers/flagsmith/index.ts +10 -0
  99. package/src/providers/gamma/index.ts +10 -0
  100. package/src/providers/github/index.ts +2 -2
  101. package/src/providers/google/gmail/attachment.ts +16 -1
  102. package/src/providers/google/gmail-imap/index.ts +7 -2
  103. package/src/providers/google/shared/oauth.ts +1 -1
  104. package/src/providers/google/shared/service-account.ts +1 -1
  105. package/src/providers/google/shared/setup.ts +1 -1
  106. package/src/providers/google/specs/calendar.v3.json +0 -32
  107. package/src/providers/google/specs/docs.v1.json +0 -76
  108. package/src/providers/google/specs/drive.v3.json +0 -32
  109. package/src/providers/google/specs/gmail.v1.json +0 -76
  110. package/src/providers/google/specs/people.v1.json +0 -77
  111. package/src/providers/google/specs/sheets.v4.json +0 -76
  112. package/src/providers/google/specs/tasks.v1.json +0 -76
  113. package/src/providers/grafana/index.ts +10 -0
  114. package/src/providers/heroku/index.ts +10 -0
  115. package/src/providers/hubspot/index.ts +75 -0
  116. package/src/providers/hygraph/index.ts +10 -0
  117. package/src/providers/icloud/mail/index.ts +5 -0
  118. package/src/providers/icloud/shared/setup.ts +1 -1
  119. package/src/providers/index.ts +255 -0
  120. package/src/providers/insightly/index.ts +10 -0
  121. package/src/providers/jam/index.ts +10 -0
  122. package/src/providers/klaviyo/index.ts +10 -0
  123. package/src/providers/mailbox/index.ts +87 -0
  124. package/src/providers/memory/provider.ts +3 -8
  125. package/src/providers/mercury/index.ts +10 -0
  126. package/src/providers/microsoft/calendar/index.ts +49 -0
  127. package/src/providers/microsoft/contacts/index.ts +50 -0
  128. package/src/providers/microsoft/drive/index.ts +62 -0
  129. package/src/providers/microsoft/index.ts +12 -0
  130. package/src/providers/microsoft/mail/index.ts +76 -0
  131. package/src/providers/microsoft/shared/oauth.ts +58 -0
  132. package/src/providers/microsoft/shared/scopes.ts +56 -0
  133. package/src/providers/microsoft/shared/setup.ts +45 -0
  134. package/src/providers/microsoft/specs/microsoft-todo.v1.json +755 -0
  135. package/src/providers/microsoft/specs/onedrive.v1.json +993 -0
  136. package/src/providers/microsoft/specs/outlook-calendar.v1.json +1743 -0
  137. package/src/providers/microsoft/specs/outlook-contacts.v1.json +430 -0
  138. package/src/providers/microsoft/specs/outlook-mail.v1.json +901 -0
  139. package/src/providers/microsoft/specs/vendor.ts +232 -0
  140. package/src/providers/microsoft/todo/index.ts +54 -0
  141. package/src/providers/miro/index.ts +10 -0
  142. package/src/providers/mixpanel/index.ts +10 -0
  143. package/src/providers/monday/index.ts +10 -0
  144. package/src/providers/mux/index.ts +10 -0
  145. package/src/providers/navan/index.ts +10 -0
  146. package/src/providers/neon/index.ts +10 -0
  147. package/src/providers/netlify/index.ts +10 -0
  148. package/src/providers/nextcloud/calendar/index.ts +28 -0
  149. package/src/providers/nextcloud/contacts/index.ts +18 -0
  150. package/src/providers/nextcloud/index.ts +3 -0
  151. package/src/providers/nextcloud/shared/setup.ts +59 -0
  152. package/src/providers/owner.ts +15 -3
  153. package/src/providers/paddle/index.ts +10 -0
  154. package/src/providers/paypal/index.ts +10 -0
  155. package/src/providers/posthog/index.ts +10 -0
  156. package/src/providers/prisma/index.ts +10 -0
  157. package/src/providers/ramp/index.ts +10 -0
  158. package/src/providers/recurly/index.ts +10 -0
  159. package/src/providers/reddit/index.ts +1 -1
  160. package/src/providers/remote/index.ts +10 -0
  161. package/src/providers/render/index.ts +35 -0
  162. package/src/providers/replicate/index.ts +10 -0
  163. package/src/providers/resend/index.ts +10 -0
  164. package/src/providers/riverside/index.ts +10 -0
  165. package/src/providers/rootly/index.ts +10 -0
  166. package/src/providers/rudderstack/index.ts +10 -0
  167. package/src/providers/salesloft/index.ts +10 -0
  168. package/src/providers/sanity/index.ts +10 -0
  169. package/src/providers/scopes.ts +2 -0
  170. package/src/providers/sentry/index.ts +10 -0
  171. package/src/providers/setup/plan.ts +11 -0
  172. package/src/providers/setup/provider.ts +12 -0
  173. package/src/providers/shared/openapi.ts +70 -0
  174. package/src/providers/shared/slug.ts +28 -0
  175. package/src/providers/shared/vendor-operations.ts +43 -1
  176. package/src/providers/shared/vendor-spec.ts +98 -17
  177. package/src/providers/shortcut/index.ts +10 -0
  178. package/src/providers/slack/index.ts +2 -2
  179. package/src/providers/square/index.ts +10 -0
  180. package/src/providers/storyblok/index.ts +10 -0
  181. package/src/providers/stripe/index.ts +10 -0
  182. package/src/providers/supabase/index.ts +10 -0
  183. package/src/providers/tasks/store.ts +2 -7
  184. package/src/providers/tavily/index.ts +10 -0
  185. package/src/providers/todoist/index.ts +10 -0
  186. package/src/providers/untested.ts +105 -0
  187. package/src/providers/vault/provider.ts +1 -1
  188. package/src/providers/vercel/index.ts +10 -0
  189. package/src/providers/vimeo/index.ts +10 -0
  190. package/src/providers/webflow/index.ts +10 -0
  191. package/src/providers/whimsical/index.ts +10 -0
  192. package/src/providers/wix/index.ts +10 -0
  193. package/src/providers/workable/index.ts +10 -0
  194. package/src/providers/yahoo_mail/index.ts +73 -0
  195. package/src/providers/zapier/index.ts +10 -0
  196. package/src/providers/zoho_mail/index.ts +79 -0
  197. package/src/secrets/document.ts +1 -1
  198. package/src/server/attachments.ts +129 -5
  199. package/src/server/index.ts +3 -3
  200. package/src/server/mcp/instructions.ts +88 -18
  201. package/src/server/stdio.ts +1 -1
@@ -0,0 +1,244 @@
1
+ import { ConfigError } from '#profile';
2
+ import { scopeNamespace } from '#dispatch';
3
+ import { scopeBlobStore, type BlobStore } from '#stores/blobs';
4
+ import {
5
+ indexState,
6
+ openCatalogue,
7
+ rebuildCatalogue,
8
+ writeCatalogue,
9
+ } from '#providers/entities/catalogue.ts';
10
+ import { forgetEntity, persistEntity } from '#providers/entities/writes.ts';
11
+ import { matchEntities, type Criteria } from '#providers/entities/find.ts';
12
+ import { describe, renderEntity } from '#providers/entities/render.ts';
13
+ import { entityStorage, type Attribute, type Entity, type Relation } from '#providers/owner.ts';
14
+ import { heading, ok, print, style, table, warn } from '../../output.ts';
15
+ import type { Runtime } from '../../runtime.ts';
16
+ import { agreed, optionalStdin, ownerConnection, required, withRuntime, type OwnerFlags } from './shared.ts';
17
+
18
+ /**
19
+ * `lanes link entities` — who and what everyone else is.
20
+ *
21
+ * Reaches the same bytes the provider does, through the same two scoping
22
+ * functions and the same `entityStorage`, `catalogue` and `find` modules. Two
23
+ * spellings of one layout is how a control plane and its data plane drift
24
+ * apart, and here it would drift in a way nothing would notice: a CLI that
25
+ * wrote entity files without maintaining `_index.json` would leave every write
26
+ * costing the next reader a full rebuild.
27
+ */
28
+
29
+ /** `--attr email=jan@example.test`, or `--attr github` for "has one at all". */
30
+ function parseAttr(given: string): { kind: string; value?: string } {
31
+ const at = given.indexOf('=');
32
+ if (at === -1) return { kind: given.trim() };
33
+ return { kind: given.slice(0, at).trim(), value: given.slice(at + 1).trim() };
34
+ }
35
+
36
+ /** `--related works_at=acme-bv`, or `--related acme-bv` for any predicate. */
37
+ function parseRelated(given: string): { predicate?: string; entity: string } {
38
+ const at = given.indexOf('=');
39
+ if (at === -1) return { entity: given.trim() };
40
+ return { predicate: given.slice(0, at).trim(), entity: given.slice(at + 1).trim() };
41
+ }
42
+
43
+ function criteriaFrom(query: string | undefined, flags: OwnerFlags): Criteria {
44
+ return {
45
+ query,
46
+ type: flags.type,
47
+ tag: flags.tag,
48
+ attr: flags.attr?.map(parseAttr),
49
+ related: flags.related?.map(parseRelated),
50
+ limit: 50,
51
+ };
52
+ }
53
+
54
+ export async function entitiesFind(query: string | undefined, flags: OwnerFlags): Promise<void> {
55
+ await withRuntime(flags, async (runtime) => {
56
+ const store = entitiesStore(runtime, flags);
57
+ const catalogue = await openCatalogue(store);
58
+ const criteria = criteriaFrom(query, flags);
59
+ const matches = matchEntities(catalogue, criteria);
60
+
61
+ if (matches.candidates.length === 0) {
62
+ heading('Entities (0)');
63
+ print(
64
+ style.dim(
65
+ catalogue.entities.length === 0
66
+ ? ' none — declare one with: lanes link entities write <name>'
67
+ : ` nothing matches ${describe(criteria)}`,
68
+ ),
69
+ );
70
+ return;
71
+ }
72
+
73
+ heading(`Entities (${matches.total})`);
74
+ table(
75
+ matches.candidates.map((candidate) => [
76
+ ` ${candidate.entity.id}`,
77
+ candidate.entity.name,
78
+ candidate.entity.type ? style.dim(candidate.entity.type) : '',
79
+ style.dim(candidate.entity.updatedAt.slice(0, 10)),
80
+ ]),
81
+ );
82
+
83
+ // The same sentence the tool result carries, for the same reason: the list
84
+ // is not a ranking, and the person reading it is about to pick from it.
85
+ if (matches.candidates.length > 1 && query !== undefined) {
86
+ print('');
87
+ print(style.dim(' more than one matches — the order is not a ranking'));
88
+ }
89
+ });
90
+ }
91
+
92
+ export async function entitiesGet(id: string | undefined, flags: OwnerFlags): Promise<void> {
93
+ const entityId = required(id, 'lanes link entities get <id>');
94
+
95
+ await withRuntime(flags, async (runtime) => {
96
+ const store = entitiesStore(runtime, flags);
97
+ const entity = await entityStorage.read(store, entityId);
98
+ if (!entity) throw new ConfigError(`No entity "${entityId}" in this profile.`);
99
+
100
+ const catalogue = await openCatalogue(store);
101
+ print('');
102
+ print(renderEntity(entity, catalogue, entity.body));
103
+ });
104
+ }
105
+
106
+ export async function entitiesWrite(name: string | undefined, flags: OwnerFlags): Promise<void> {
107
+ const given = required(name, 'lanes link entities write <name> [--type person] [--attr email=…]');
108
+ // Optional rather than required: an entity legitimately has no prose, and
109
+ // refusing an empty pipe would break every scripted invocation.
110
+ const notes = await optionalStdin();
111
+
112
+ await withRuntime(flags, async (runtime) => {
113
+ const store = entitiesStore(runtime, flags);
114
+ const id = flags.name ?? entityStorage.slugify(given);
115
+ const existing = await entityStorage.read(store, id);
116
+
117
+ const attributes = flags.attr?.map((one) => {
118
+ const { kind, value } = parseAttr(one);
119
+ if (value === undefined) {
120
+ throw new ConfigError(`--attr ${one} needs a value here: --attr ${kind}=<value>`);
121
+ }
122
+ return { kind, value } satisfies Attribute;
123
+ });
124
+
125
+ const relations = flags.related?.map((one) => {
126
+ const { predicate, entity } = parseRelated(one);
127
+ if (predicate === undefined) {
128
+ throw new ConfigError(`--related ${one} needs a predicate: --related <predicate>=${entity}`);
129
+ }
130
+ return { predicate, entity } satisfies Relation;
131
+ });
132
+
133
+ // A flag that was not passed keeps what is on disk, exactly as the tool
134
+ // does — `entities write` is how a person corrects one field.
135
+ const next: Entity = {
136
+ id,
137
+ name: given,
138
+ type: flags.type ?? existing?.type ?? '',
139
+ aliases: flags.alias ?? existing?.aliases ?? [],
140
+ tags: flags.tag ? [flags.tag] : (existing?.tags ?? []),
141
+ attributes: attributes ?? existing?.attributes ?? [],
142
+ relations: relations ?? existing?.relations ?? [],
143
+ updatedAt: new Date().toISOString(),
144
+ body: notes ?? existing?.body ?? '',
145
+ bytes: 0,
146
+ };
147
+
148
+ await persistEntity(store, next);
149
+ print(ok(`${existing ? 'updated' : 'declared'} entity ${style.bold(id)}`));
150
+ });
151
+ }
152
+
153
+ export async function entitiesLink(
154
+ from: string | undefined,
155
+ edge: string | undefined,
156
+ flags: OwnerFlags,
157
+ ): Promise<void> {
158
+ const usage = 'lanes link entities link <from> <predicate>=<to>';
159
+ const given = required(from, usage);
160
+ const { predicate, entity: to } = parseRelated(required(edge, usage));
161
+ if (predicate === undefined) {
162
+ throw new ConfigError(`"${edge}" needs a predicate — ${usage}`);
163
+ }
164
+
165
+ await withRuntime(flags, async (runtime) => {
166
+ const store = entitiesStore(runtime, flags);
167
+ const entity = await entityStorage.read(store, given);
168
+ if (!entity) throw new ConfigError(`No entity "${given}" in this profile.`);
169
+
170
+ if (entity.relations.some((one) => one.predicate === predicate && one.entity === to)) {
171
+ print(style.dim(` ${given} already ${predicate} ${to}`));
172
+ return;
173
+ }
174
+
175
+ await persistEntity(store, {
176
+ ...entity,
177
+ relations: [...entity.relations, { predicate, entity: to }],
178
+ updatedAt: new Date().toISOString(),
179
+ });
180
+
181
+ const catalogue = await openCatalogue(store);
182
+ print(ok(`${style.bold(given)} ${predicate} ${style.bold(to)}`));
183
+ if (!catalogue.byId.has(to)) print(warn(`"${to}" is not declared yet — the edge is kept as written`));
184
+ });
185
+ }
186
+
187
+ export async function entitiesForget(id: string | undefined, flags: OwnerFlags): Promise<void> {
188
+ const entityId = required(id, 'lanes link entities forget <id>');
189
+
190
+ await withRuntime(flags, async (runtime) => {
191
+ const store = entitiesStore(runtime, flags);
192
+ const catalogue = await openCatalogue(store);
193
+ const entity = catalogue.byId.get(entityId);
194
+ if (!entity) throw new ConfigError(`No entity "${entityId}" in this profile.`);
195
+
196
+ const referencedBy = (catalogue.backlinks.get(entityId) ?? []).map((one) => one.from);
197
+
198
+ print(` ${style.bold(entity.id)} ${entity.name}`);
199
+ // Said before the prompt rather than after the delete: this is the fact
200
+ // that should change the answer, and `forget` deliberately does not cascade.
201
+ if (referencedBy.length > 0) {
202
+ print(warn(`still referenced by ${referencedBy.join(', ')} — those edges will dangle`));
203
+ }
204
+ if (!(await agreed(flags, 'Remove this entity?'))) return;
205
+
206
+ await forgetEntity(store, catalogue, entityId, new Date().toISOString());
207
+
208
+ print(ok(`removed entity ${style.bold(entityId)}`));
209
+ });
210
+ }
211
+
212
+ /**
213
+ * Rebuild `_index.json` from the files, and say why it needed it.
214
+ *
215
+ * The index self-heals on the next write, so this exists for the case a write
216
+ * is not coming: a bulk edit made in an editor or pulled from a knowledge
217
+ * repository, where the next read would otherwise pay a full scan every time.
218
+ */
219
+ export async function entitiesReindex(flags: OwnerFlags): Promise<void> {
220
+ await withRuntime(flags, async (runtime) => {
221
+ const store = entitiesStore(runtime, flags);
222
+ const before = await indexState(store);
223
+
224
+ if (before.current) {
225
+ print(style.dim(` index is already current — ${before.reason}`));
226
+ return;
227
+ }
228
+
229
+ const catalogue = await rebuildCatalogue(store);
230
+ await writeCatalogue(store, catalogue.entities, catalogue.fingerprint, new Date().toISOString());
231
+ print(ok(`rebuilt the index over ${catalogue.entities.length} entities — ${before.reason}`));
232
+ });
233
+ }
234
+
235
+ /**
236
+ * The blob namespace core would scope this provider to.
237
+ *
238
+ * The same two functions `buildProviderContext` uses, for the reason
239
+ * `memoryStore` gives: a path spelled out again is a path that can differ.
240
+ */
241
+ export function entitiesStore(runtime: Runtime, flags: OwnerFlags): BlobStore {
242
+ const connection = ownerConnection(runtime.config, 'entities', flags);
243
+ return scopeBlobStore(runtime.storage, scopeNamespace('entities', connection));
244
+ }
@@ -26,6 +26,20 @@ export interface OwnerFlags extends GlobalFlags {
26
26
  readonly name?: string | undefined;
27
27
  /** `assets`: for the file whose extension does not say what it is. */
28
28
  readonly contentType?: string | undefined;
29
+ /** `entities`: person, company, project — free-form, never a closed list. */
30
+ readonly type?: string | undefined;
31
+ /**
32
+ * `entities`: repeatable, which is why `ownerFlags` needs argv.
33
+ *
34
+ * `parseArgv` keeps only the last value of a repeated flag, and an entity
35
+ * with two email addresses is the case this whole component exists for — so
36
+ * these three read the raw argv through `all()`, as `customFlags` does.
37
+ */
38
+ readonly alias?: readonly string[] | undefined;
39
+ /** `entities`: `email=jan@example.test`, or `github` to mean "has one". */
40
+ readonly attr?: readonly string[] | undefined;
41
+ /** `entities`: `works_at=acme-bv`, or `acme-bv` for any predicate. */
42
+ readonly related?: readonly string[] | undefined;
29
43
  /** Reveal a vault value on a terminal. */
30
44
  readonly show?: boolean | undefined;
31
45
  /** Print only the value, for `$(…)`. */
@@ -15,7 +15,7 @@ import {
15
15
  *
16
16
  * `lanes link vault get` prints a value, which `lanes link secrets` deliberately never does.
17
17
  * That is the two-kinds-of-secret distinction doing its job
18
- * (`docs/detailed/security.md`): a credential authorises the system and is never
18
+ * (`https://lanes.sh/docs/link/security`): a credential authorises the system and is never
19
19
  * disclosed, and a vault the owner cannot read without an agent is not a vault.
20
20
  */
21
21
 
@@ -136,5 +136,5 @@ export function vaultKeyGenerate(flags: OwnerFlags): void {
136
136
  print('');
137
137
  print(style.dim(' Store it as LANES_LINK_VAULT_KEY. It is not written anywhere by this command:'));
138
138
  print(style.dim(' a key kept beside the document it protects protects nothing.'));
139
- print(style.dim(' Separate from the credential-store key, deliberately — docs/detailed/security.md.'));
139
+ print(style.dim(' Separate from the credential-store key, deliberately — https://lanes.sh/docs/link/security.'));
140
140
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * `lanes link memory`, `tasks`, `assets`, `skills`, `vault` — the owner layer's
3
- * control plane.
2
+ * `lanes link memory`, `tasks`, `assets`, `skills`, `vault`, `entities` — the
3
+ * owner layer's control plane.
4
4
  *
5
5
  * The layer shipped in M4 with no CLI at all, so the two stores holding the
6
6
  * owner's *own* data were reachable only by an agent, and the one thing that
@@ -22,7 +22,7 @@
22
22
  * MCP because ADR-014 §1 decided a policy-gated grant beats a missing path.
23
23
  *
24
24
  * One noun per file — `memory.ts`, `tasks.ts`, `assets.ts`, `skills.ts`,
25
- * `vault.ts` — over the shape they all share in `shared.ts`: the flag type, the
25
+ * `vault.ts`, `entities.ts` — over the shape they all share in `shared.ts`: the flag type, the
26
26
  * open-announce-act-close wrapper, connection resolution, and the two prompts.
27
27
  */
28
28
 
@@ -40,6 +40,16 @@ export { assetsAdd, assetsGet, assetsList, assetsRemove } from './owner/assets.t
40
40
 
41
41
  export { skillsAdd, skillsList, skillsRemove, skillsShow } from './owner/skills.ts';
42
42
 
43
+ export {
44
+ entitiesFind,
45
+ entitiesForget,
46
+ entitiesGet,
47
+ entitiesLink,
48
+ entitiesReindex,
49
+ entitiesStore,
50
+ entitiesWrite,
51
+ } from './owner/entities.ts';
52
+
43
53
  export {
44
54
  vaultGet,
45
55
  vaultKeyGenerate,
@@ -128,6 +128,18 @@ function renderOne(plan: ProviderPlan, missing: ReadonlySet<string>): void {
128
128
  }
129
129
  }
130
130
 
131
+ if (plan.variables.length > 0) {
132
+ // Its own heading rather than a row under "Values it needs", because there
133
+ // is nothing to store ahead of time: `connect` asks for these and writes
134
+ // them to the connection, so the useful thing to show is what will be asked
135
+ // and what an answer looks like.
136
+ heading('Where it is');
137
+ for (const variable of plan.variables) {
138
+ print(` ${style.bold(variable.label)} ${style.dim(`e.g. ${variable.example}`)}`);
139
+ print(` ${style.dim(variable.description)}`);
140
+ }
141
+ }
142
+
131
143
  heading('Then');
132
144
  print(` ${plan.command}`);
133
145
 
@@ -282,7 +282,7 @@ oauth_apps: {}
282
282
  # reports — an address, a workspace — so this list says whose data is reachable
283
283
  # without having to look anything up.
284
284
  #
285
- # The six below hold no account, and that is why they are here already: they
285
+ # The seven below hold no account, and that is why they are here already: they
286
286
  # reach your own material rather than anybody's API, so there was never anything
287
287
  # for a connect step to authorise (ADR-050). What each one is:
288
288
  #
@@ -292,6 +292,9 @@ oauth_apps: {}
292
292
  # skills procedures you have written, handed to an agent as instructions
293
293
  # vault passwords and API keys, released one at a time
294
294
  # setup what is connected here, and what connecting more would take
295
+ # entities the people, companies and projects you deal with, and how to
296
+ # reach each of them — so an agent looks an address up rather
297
+ # than recalling one
295
298
  #
296
299
  # Nothing is stored in any of them until you or an agent puts something there,
297
300
  # and none of them can read an account. To switch one off, deny it below —
@@ -304,6 +307,7 @@ connections:
304
307
  - { id: main, provider: skills, account: Skills }
305
308
  - { id: main, provider: vault, account: Vault }
306
309
  - { id: main, provider: setup, account: Setup }
310
+ - { id: main, provider: entities, account: Entities }
307
311
 
308
312
  # Only what is listed here is reachable, and an empty policy grants nothing.
309
313
  #
@@ -317,19 +321,25 @@ connections:
317
321
  # allow: [notion.*, gmail.*] two providers
318
322
  # deny: [gmail.send_message] a deny always beats an allow
319
323
  #
320
- # The rules below grant each of the six its whole namespace, writes included —
324
+ # The rules below grant each of the seven its whole namespace, writes included —
321
325
  # the same thing "connect memory" wrote when it was a command you had to run.
322
326
  # Narrowing is one line, and these are the three worth knowing:
323
327
  #
324
- # deny: [memory.write] memory becomes read-only
325
- # deny: [skills.manage.*] skills can be invoked but not authored
326
- # deny: [vault.put, vault.remove] nothing new can be stored
328
+ # deny: [memory.write, memory.forget] memory becomes read-only
329
+ # deny: [skills.manage.*] skills can be invoked but not authored
330
+ # deny: [vault.put, vault.remove] nothing new can be stored
331
+ # deny: [entities.write, entities.link, entities.forget]
332
+ # entities becomes read-only
333
+ #
334
+ # Those lists are exhaustive on purpose: a namespace is read-only only when
335
+ # every capability that changes something is named, so "deny: [memory.write]"
336
+ # alone leaves "memory.forget" granted.
327
337
  #
328
338
  # A vault read is not granted by "vault.*" alone: each stored item is its own
329
339
  # "vault.get.<id>" capability and only appears after a restart, so a write can
330
340
  # never hand itself a read (ADR-012).
331
341
  policy:
332
- allow: [memory.*, tasks.*, assets.*, skills.*, vault.*, setup.*]
342
+ allow: [memory.*, tasks.*, assets.*, skills.*, vault.*, setup.*, entities.*]
333
343
  deny: []
334
344
  `;
335
345
  }
@@ -27,6 +27,7 @@ const RESERVED_SURFACES = {
27
27
  vault: 'Vault',
28
28
  setup: 'Setup',
29
29
  identity: 'Identity',
30
+ entities: 'Entities',
30
31
  } as const;
31
32
 
32
33
  type ReservedSurface = keyof typeof RESERVED_SURFACES;
@@ -42,6 +43,14 @@ type ReservedSurface = keyof typeof RESERVED_SURFACES;
42
43
  * which is ADR-050's whole argument — so a profile written before those existed
43
44
  * gets them on the next command rather than needing five of its own.
44
45
  *
46
+ * `entities` is on the list and `identity` is not, which reads as inconsistent
47
+ * until the test is stated exactly. It is not "is it empty" — memory arrives
48
+ * empty and is granted. It is **can it be filled in from here**: identity is
49
+ * configuration, changed in the CLI under ADR-007, so a surface that reported
50
+ * an empty one could never do anything about it. Entities accumulate on the
51
+ * same surface that reads them, so an empty one is a store waiting to be used
52
+ * rather than a tool with nothing to say (ADR-056).
53
+ *
45
54
  * Ordered as `RESERVED_PROVIDER_IDS` is, so a repair reports in the order the
46
55
  * template writes and a diff between the two reads as a diff.
47
56
  */
@@ -52,6 +61,7 @@ export const DEFAULT_SURFACES: readonly ReservedSurface[] = [
52
61
  'skills',
53
62
  'vault',
54
63
  'setup',
64
+ 'entities',
55
65
  ];
56
66
 
57
67
  /**
@@ -3,6 +3,12 @@ import {
3
3
  assetsGet,
4
4
  assetsList,
5
5
  assetsRemove,
6
+ entitiesFind,
7
+ entitiesForget,
8
+ entitiesGet,
9
+ entitiesLink,
10
+ entitiesReindex,
11
+ entitiesWrite,
6
12
  memoryForget,
7
13
  memoryGet,
8
14
  memoryList,
@@ -25,7 +31,8 @@ import {
25
31
  } from './commands/owner.ts';
26
32
 
27
33
  /**
28
- * The commands over the owner's own data: memory, tasks, assets, skills, vault.
34
+ * The commands over the owner's own data: memory, tasks, assets, skills, vault,
35
+ * entities.
29
36
  *
30
37
  * Split out of `main.ts` for the reason the budget in `src/architecture.test.ts`
31
38
  * exists to find, rather than to satisfy a line count. These are one subject —
@@ -125,6 +132,28 @@ export function dispatchOwner(
125
132
  throw new Error(`Unknown: ${program} vault ${second}`);
126
133
  }
127
134
 
135
+ case 'entities':
136
+ switch (second) {
137
+ // Bare `entities` is a listing, which is `find` with no criteria — one
138
+ // name for one concept rather than a `list` that would be the same code
139
+ // under a second word.
140
+ case 'find':
141
+ case undefined:
142
+ return entitiesFind(rest[0], owner);
143
+ case 'get':
144
+ return entitiesGet(rest[0], owner);
145
+ case 'write':
146
+ return entitiesWrite(rest[0], owner);
147
+ case 'link':
148
+ return entitiesLink(rest[0], rest[1], owner);
149
+ case 'forget':
150
+ return entitiesForget(rest[0], owner);
151
+ case 'reindex':
152
+ return entitiesReindex(owner);
153
+ default:
154
+ throw new Error(`Unknown: ${program} entities ${second}`);
155
+ }
156
+
128
157
  default:
129
158
  // Unreachable: `main.ts` narrows to the nouns above before calling. Kept
130
159
  // so that adding one there and forgetting it here is a thrown error rather
package/src/cli/main.ts CHANGED
@@ -58,7 +58,7 @@ export async function run(argv: readonly string[]): Promise<void> {
58
58
  const [first, second, ...rest] = command;
59
59
 
60
60
  const global = globalFlags(flags);
61
- const owner = ownerFlags(flags);
61
+ const owner = ownerFlags(flags, argv);
62
62
 
63
63
  const show = flags['show'] === true;
64
64
  const raw = flags['raw'] === true;
@@ -260,6 +260,7 @@ export async function run(argv: readonly string[]): Promise<void> {
260
260
  case 'assets':
261
261
  case 'skills':
262
262
  case 'vault':
263
+ case 'entities':
263
264
  return dispatchOwner(first, second, rest, owner, PROGRAM);
264
265
 
265
266
  // Beside `memory` and `skills` because it is the question they raise next:
@@ -6,7 +6,6 @@ import type { BlobStore } from '#stores/blobs';
6
6
  import type { AnyConnector, ProviderManifest } from '#connectivity';
7
7
  import { RateLimiter, allowedConnections } from '#policy';
8
8
  import {
9
- KNOWLEDGE_LAYOUT,
10
9
  layout,
11
10
  listProfiles,
12
11
  workspacePath,
@@ -26,7 +25,7 @@ import {
26
25
  type StorageFactory,
27
26
  type TargetInput,
28
27
  } from '#deployments/target.ts';
29
- import { openKnowledge, type FetchLike, type KnowledgeStores } from '#deployments/knowledge.ts';
28
+ import { knowledgeRoutes, openKnowledge, type FetchLike, type KnowledgeStores } from '#deployments/knowledge.ts';
30
29
  import { routeBlobStore } from '#stores/blobs/route.ts';
31
30
  import { connectorFactory } from '#connectivity/transports';
32
31
  import { requestAuthorizer } from '#connectivity/auth/index.ts';
@@ -174,9 +173,7 @@ export async function openRuntime(
174
173
  // The audit log, `state.kv`, the credential store and the vault keep their
175
174
  // own roots on the target's own storage and are untouched.
176
175
  const knowledge = await openKnowledge(adapters, credentials, options.fetch);
177
- const storage = knowledge
178
- ? routeBlobStore(storageFor(), [{ prefix: `${KNOWLEDGE_LAYOUT.memory}/`, store: knowledge.memory }])
179
- : storageFor();
176
+ const storage = knowledge ? routeBlobStore(storageFor(), knowledgeRoutes(knowledge)) : storageFor();
180
177
  const state = openState(storageFor, config.instance.profile);
181
178
 
182
179
  // The durable log, plus any copies the target declares. `sink` is what
@@ -195,7 +192,7 @@ export async function openRuntime(
195
192
  // The vault's own store, beside the credential store and never it: a separate
196
193
  // document, a separate key, and a separate environment variable
197
194
  // (`LANES_LINK_VAULT_KEY`). One master secret reused across purposes turns any
198
- // single compromise into a total one — `docs/detailed/security.md`, and the boundary
195
+ // single compromise into a total one — `https://lanes.sh/docs/link/security`, and the boundary
199
196
  // test that has existed since M1.
200
197
  //
201
198
  // Opened before the registry because each stored item becomes its own
@@ -327,7 +324,14 @@ export async function openRuntime(
327
324
  // dispatcher and the CLI share it deliberately: a stateful connector must be
328
325
  // the *same instance* whichever side asks for it, or a held session is held
329
326
  // twice.
330
- const connectorFor = connectorFactory({ registry, credentials });
327
+ const connectorFor = connectorFactory({
328
+ registry,
329
+ credentials,
330
+ // From the connection row, which is where a per-connection setting has
331
+ // always lived. One lookup behind both, so they cannot disagree.
332
+ connectionConfig: (provider, id) => row(config, provider, id)?.config,
333
+ isDeclared: (provider, id) => row(config, provider, id) !== undefined,
334
+ });
331
335
  const authorizeRequest = requestAuthorizer(registry, credentials);
332
336
  let closed = false;
333
337
 
@@ -385,3 +389,6 @@ export async function openRuntime(
385
389
  },
386
390
  };
387
391
  }
392
+
393
+ const row = (config: Config, provider: string, id: string) =>
394
+ config.connections.find((connection) => connection.provider === provider && connection.id === id);
@@ -13,6 +13,7 @@ import {
13
13
  createSetupProvider,
14
14
  createSkillsProvider,
15
15
  createVaultProvider,
16
+ entitiesProvider,
16
17
  memoryProvider,
17
18
  tasksProvider,
18
19
  type IdentityProviderOptions,
@@ -87,7 +88,7 @@ export interface OwnerLayerOptions {
87
88
  * from, which is what lets workspace YAML register alongside these.
88
89
  *
89
90
  * `allowReserved` is what admits `memory`, `tasks`, `assets`, `skills`, `vault`,
90
- * `setup`, and `identity`. The guard
91
+ * `setup`, `identity` and `entities`. The guard
91
92
  * stays rather than being retired: it exists so a *third-party* provider cannot
92
93
  * claim a namespace whose policy rules would then silently mean something else,
93
94
  * and that reason survives the owner layer shipping. Only this one construction
@@ -104,6 +105,10 @@ export function buildRegistry(owner: OwnerLayerOptions = {}): ProviderRegistry {
104
105
  registry.register(memoryProvider);
105
106
  registry.register(tasksProvider);
106
107
  registry.register(assetsProvider);
108
+ // `entities` is a fourth of the same shape: what it holds is the owner's, and
109
+ // its derived index lives in the same scoped store, so there is nothing to
110
+ // hand in here either.
111
+ registry.register(entitiesProvider);
107
112
  registry.register(skillsProviderFor(owner));
108
113
  registry.register(
109
114
  createVaultProvider({
@@ -168,6 +168,7 @@ export const SELECTION: Record<string, Requires> = {
168
168
  assets: 'profile+target',
169
169
  skills: 'profile+target',
170
170
  vault: 'profile+target',
171
+ entities: 'profile+target',
171
172
  // Both halves open the target's adapters — `show` counts what is in the
172
173
  // stores, and `use` migrates between them — and both edit the profile's
173
174
  // config. Neither can be answered without being told which.
@@ -198,6 +199,9 @@ const SUBCOMMANDS: Record<string, readonly string[]> = {
198
199
  assets: ['list', 'get', 'add', 'remove'],
199
200
  skills: ['list', 'show', 'add', 'remove'],
200
201
  vault: ['list', 'get', 'set', 'remove', 'key'],
202
+ // No `list`: a bare `entities` is a listing, which is `find` with no
203
+ // criteria. One concept, one word.
204
+ entities: ['find', 'get', 'write', 'link', 'forget', 'reindex'],
201
205
  mcp: ['skill', 'add', 'stdio', 'list'],
202
206
  secrets: ['push', 'set', 'list'],
203
207
  knowledge: ['show', 'use'],
@@ -253,6 +257,9 @@ const ACCEPTS: Record<string, readonly string[]> = {
253
257
  connect: [
254
258
  'id',
255
259
  'display-name',
260
+ // Repeatable: `--set host=cloud.example.com`. The only way to give a
261
+ // provider its address without a terminal to ask at.
262
+ 'set',
256
263
  'label',
257
264
  'replace',
258
265
  'non-interactive',
@@ -306,6 +313,10 @@ const ACCEPTS: Record<string, readonly string[]> = {
306
313
  assets: ['connection', 'name', 'content-type', 'yes'],
307
314
  skills: ['connection', 'title', 'description', 'file'],
308
315
  vault: ['connection'],
316
+ // `alias`, `attr` and `related` are repeatable — see `ownerFlags`. `name`
317
+ // overrides the id derived from the positional name, which is how you get
318
+ // `acme-bv` rather than `acme-b-v`.
319
+ entities: ['connection', 'type', 'name', 'alias', 'attr', 'related', 'tag', 'yes'],
309
320
  // `no-migrate` is listed beside `migrate` because they are three states
310
321
  // rather than two: neither one asks, and a run with no terminal has to be
311
322
  // able to say which it meant (ADR-041).
package/src/cli/usage.ts CHANGED
@@ -100,6 +100,17 @@ ${style.bold('Your own context')}
100
100
  ${PROGRAM} skills add <name> [--file f] document on stdin
101
101
  ${PROGRAM} skills remove <name>
102
102
 
103
+ ${PROGRAM} entities who and what everyone else is
104
+ ${PROGRAM} entities find [query] [--type t] [--tag t] [--attr kind[=value]]
105
+ [--related predicate=id] every match, never a choice
106
+ ${PROGRAM} entities get <id> with its relationships, both ways
107
+ ${PROGRAM} entities write <name> [--type t] [--name id] [--alias a]
108
+ [--attr kind=value] [--related predicate=id]
109
+ notes on stdin; a flag you omit keeps what is stored
110
+ ${PROGRAM} entities link <from> <predicate>=<to> one edge, written on <from> only
111
+ ${PROGRAM} entities forget <id>
112
+ ${PROGRAM} entities reindex rebuild the lookup index from the files
113
+
103
114
  ${PROGRAM} knowledge show where memory and skills are kept, and how many
104
115
  ${PROGRAM} knowledge use github --repo <owner/name> [--branch b] [--path p]
105
116
  keep both in a private repository, over the GitHub API
@@ -152,8 +163,8 @@ ${style.bold('Naming what a command acts on')}
152
163
  command that names neither refuses and lists what exists.
153
164
 
154
165
  ${style.bold('Other flags')}
155
- --connection <id> which memory/tasks/assets/skills/vault connection, where
156
- a profile has several of one kind
166
+ --connection <id> which memory/tasks/assets/skills/vault/entities
167
+ connection, where a profile has several of one kind
157
168
  --yes skip the confirmation a destructive command would ask for
158
169
  --json machine-readable output, where a command offers it
159
170
  --non-interactive never prompt: connect refuses with what to store,
@@ -133,6 +133,6 @@ export function strategyContextFrom(input: {
133
133
  export function refuseStrategy(strategy: string): never {
134
134
  throw new Error(
135
135
  `Auth strategy "${strategy}" is not registered. ` +
136
- `Strategies are the only place per-vendor code belongs; see docs/detailed/creating-a-provider.md.`,
136
+ `Strategies are the only place per-vendor code belongs; see https://lanes.sh/docs/link/creating-a-provider.`,
137
137
  );
138
138
  }
@@ -8,7 +8,7 @@ import type { ProviderContext } from './context.ts';
8
8
  * Do not make everything a tool. Use resources for read-oriented document or
9
9
  * structured context, tools for actions and parameterised queries, prompts for
10
10
  * reusable procedures. Decide per capability and record the reasoning in
11
- * `docs/detailed/providers.md` — ADR-006.
11
+ * `https://lanes.sh/docs/link/capabilities` — ADR-006.
12
12
  */
13
13
  export type Capability = ToolCapability | ResourceCapability | PromptCapability;
14
14