@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
@@ -115,7 +115,7 @@ export function reuseStoredCredential(input: {
115
115
  /**
116
116
  * Ask for a static credential — an API key, or an app-specific password.
117
117
  *
118
- * This did not exist, which meant the custom-provider path `docs/detailed/creating-a-provider.md`
118
+ * This did not exist, which meant the custom-provider path `https://lanes.sh/docs/link/creating-a-provider`
119
119
  * documents did not work: a manifest declaring `auth: {kind: header}` connected
120
120
  * with no complaint, never asked for the key, and then reported itself
121
121
  * unauthorized forever — with `doctor` advising the very command that had just
@@ -0,0 +1,220 @@
1
+ import type { AnyConnector, ProviderManifest } from '#connectivity';
2
+ import { connectorFactory, type ConnectorFactory } from '#connectivity/transports';
3
+ import type { ProviderRegistry } from '#registry';
4
+
5
+ /** Only the fields this reads; the real row carries more. */
6
+ interface ConnectionRow {
7
+ readonly provider: string;
8
+ readonly id: string;
9
+ readonly config?: Readonly<Record<string, unknown>> | undefined;
10
+ }
11
+ import type { SecretStore } from '#secrets';
12
+ import type { Prompter } from '../../prompt.ts';
13
+
14
+ /**
15
+ * Asking a connection where its service is.
16
+ *
17
+ * Only a provider that declares `variables` reaches this — a Zendesk-shaped host
18
+ * that carries the tenant, or anything self-hosted. Everything else has one
19
+ * address for everybody and is never asked anything here.
20
+ *
21
+ * Asked *before* the account is settled, and that ordering is forced rather than
22
+ * chosen: settling the identity means calling the service, and there is nothing
23
+ * to call until the address is known. It is also why the values cannot simply be
24
+ * written to the connection row first — the row's id is not decided until the
25
+ * identity comes back.
26
+ */
27
+ export interface VariableValues {
28
+ readonly [key: string]: string;
29
+ }
30
+
31
+ export async function askForVariables(input: {
32
+ readonly manifest: ProviderManifest;
33
+ readonly prompter: Prompter;
34
+ /** Values already on the row, when reconnecting. Offered as the default. */
35
+ readonly existing?: Readonly<Record<string, unknown>> | undefined;
36
+ /** With nobody to ask, an absent value is a refusal rather than a prompt. */
37
+ readonly interactive: boolean;
38
+ /** From `--set key=value`, which is how a non-interactive run supplies one. */
39
+ readonly supplied?: Readonly<Record<string, string>> | undefined;
40
+ }): Promise<VariableValues> {
41
+ const { manifest, prompter, existing, interactive } = input;
42
+ const supplied = input.supplied ?? {};
43
+ if (manifest.variables.length === 0) return {};
44
+
45
+ const values: Record<string, string> = {};
46
+
47
+ for (const variable of manifest.variables) {
48
+ // `--set` outranks what the row already carries: naming a value is how
49
+ // somebody says the server moved.
50
+ const previous = supplied[variable.key] ?? existing?.[variable.key];
51
+ const known = typeof previous === 'string' && previous !== '' ? previous : undefined;
52
+
53
+ if (!interactive) {
54
+ if (!known) {
55
+ throw new Error(
56
+ `${manifest.name} needs to be told its ${variable.label} and there is nobody to ask. ` +
57
+ `Give it on the command line: --set ${variable.key}=${variable.example}`,
58
+ );
59
+ }
60
+ if (!new RegExp(variable.pattern).test(known)) {
61
+ throw new Error(
62
+ `"${known}" is not a usable ${variable.label} — it must look like ${variable.example}.`,
63
+ );
64
+ }
65
+ values[variable.key] = known;
66
+ continue;
67
+ }
68
+
69
+ // The description goes in the question rather than beside it: `Prompter.ask`
70
+ // takes one string, and a hostname nobody can guess needs the sentence that
71
+ // says where to find it.
72
+ const answer = (
73
+ await prompter.ask(
74
+ `${variable.description}\n${variable.label}${known ? ` [${known}]` : ` (e.g. ${variable.example})`}`,
75
+ )
76
+ ).trim();
77
+
78
+ const chosen = answer === '' ? known : answer;
79
+ if (!chosen) {
80
+ throw new Error(`${manifest.name} cannot be reached without its ${variable.label}.`);
81
+ }
82
+
83
+ // Checked here as well as at substitution, so a mistyped subdomain is a
84
+ // question asked again rather than a connection that is written and then
85
+ // fails on its first call. `applyVariables` is still the one that matters —
86
+ // it guards the value a *file* supplies, which never passes through here.
87
+ if (!new RegExp(variable.pattern).test(chosen)) {
88
+ throw new Error(
89
+ `"${chosen}" is not a usable ${variable.label} — it must look like ${variable.example}. ` +
90
+ `The value goes into the address this connection calls, so anything that could change ` +
91
+ `the host is refused.`,
92
+ );
93
+ }
94
+
95
+ values[variable.key] = chosen;
96
+ }
97
+
98
+ return values;
99
+ }
100
+
101
+ /**
102
+ * The address a provider was last given, from whichever of its connections has one.
103
+ *
104
+ * Also reached across a family: `nextcloud_calendar` and `nextcloud_contacts`
105
+ * are two providers on one server, and the setup steps tell people to connect
106
+ * them together — so being asked the same hostname twice is the thing to avoid.
107
+ * They are matched on sharing a credential app, which is already how "one
108
+ * account, several providers" is expressed.
109
+ */
110
+ function previousAddress(
111
+ connections: readonly ConnectionRow[],
112
+ providerId: string,
113
+ manifest: ProviderManifest,
114
+ ): Readonly<Record<string, unknown>> | undefined {
115
+ const family = manifest.auth.kind === 'basic' ? manifest.auth.app : undefined;
116
+
117
+ const mine = connections.find(
118
+ (connection) => connection.provider === providerId && connection.config,
119
+ );
120
+ if (mine) return mine.config;
121
+
122
+ if (!family) return undefined;
123
+
124
+ return connections.find(
125
+ (connection) => connection.provider.startsWith(`${family}_`) && connection.config,
126
+ )?.config;
127
+ }
128
+
129
+ export interface ResolvedAddress {
130
+ /** What the connection will store under `config`. Empty for a fixed-address provider. */
131
+ readonly values: VariableValues;
132
+ /** The factory the identity probe and discovery should use. */
133
+ readonly connectorFor: (providerId: string, connectionId: string) => AnyConnector | undefined;
134
+ /** Only ever a scoped factory; the runtime's own is never closed by this. */
135
+ close(): Promise<void>;
136
+ }
137
+
138
+ /**
139
+ * Everything a connect run needs in order to reach a service whose address it
140
+ * has just been told.
141
+ *
142
+ * The scoped factory is the part worth explaining. The values belong to a
143
+ * connection row that does not exist yet — the row is written at step 4, and
144
+ * the id it will carry is not settled until step 2 — so there is nowhere for
145
+ * the runtime's own lookup to read them from. Rather than write a provisional
146
+ * row and rename it, this run gets a factory of its own holding the values
147
+ * directly. The runtime's factory is untouched, which matters because its cache
148
+ * is what keeps a stateful connector a single instance.
149
+ *
150
+ * A provider with no variables gets the runtime's factory back and no second
151
+ * cache, so the ordinary path is exactly as it was.
152
+ */
153
+ export async function resolveConnectionAddress(input: {
154
+ readonly manifest: ProviderManifest;
155
+ readonly prompter: Prompter;
156
+ readonly providerId: string;
157
+ readonly provisionalId: string;
158
+ readonly interactive: boolean;
159
+ /** `--set key=value`, as the parser hands it over: absent, one, or many. */
160
+ readonly set?: readonly string[] | string | undefined;
161
+ readonly runtime: {
162
+ readonly registry: ProviderRegistry;
163
+ readonly credentials: SecretStore;
164
+ readonly config: { readonly connections: readonly ConnectionRow[] };
165
+ connectorFor(providerId: string, connectionId: string): AnyConnector | undefined;
166
+ };
167
+ }): Promise<ResolvedAddress> {
168
+ const { manifest, runtime } = input;
169
+
170
+ const values = await askForVariables({
171
+ manifest,
172
+ prompter: input.prompter,
173
+ // What was said last time, offered as the default.
174
+ //
175
+ // Not keyed on the provisional id, which is `pending` unless `--id` was
176
+ // given — so the lookup almost never hit and every reconnect asked for the
177
+ // server address again. Any declared connection of this provider will do:
178
+ // the id is not settled yet, and a second Nextcloud on a *different* host is
179
+ // rarer than reconnecting the one there is. It is offered, not imposed —
180
+ // pressing Enter accepts it and typing replaces it.
181
+ existing: previousAddress(runtime.config.connections, input.providerId, manifest),
182
+ interactive: input.interactive,
183
+ supplied: parseSet(input.set),
184
+ });
185
+
186
+ if (manifest.variables.length === 0) {
187
+ return {
188
+ values,
189
+ connectorFor: runtime.connectorFor.bind(runtime),
190
+ close: async () => {},
191
+ };
192
+ }
193
+
194
+ const scoped: ConnectorFactory = connectorFactory({
195
+ registry: runtime.registry,
196
+ credentials: runtime.credentials,
197
+ connectionConfig: () => values,
198
+ });
199
+
200
+ return { values, connectorFor: scoped, close: () => scoped.closeAll() };
201
+ }
202
+
203
+ /**
204
+ * `--set key=value` into a map, refusing anything that is not a pair.
205
+ *
206
+ * A flag given once arrives as a string and twice as an array, which is the
207
+ * parser's shape rather than a choice here.
208
+ */
209
+ export function parseSet(input: readonly string[] | string | undefined): Record<string, string> {
210
+ const given = input === undefined ? [] : typeof input === 'string' ? [input] : input;
211
+ const values: Record<string, string> = {};
212
+
213
+ for (const entry of given) {
214
+ const at = entry.indexOf('=');
215
+ if (at <= 0) throw new Error(`--set wants key=value, and got "${entry}".`);
216
+ values[entry.slice(0, at)] = entry.slice(at + 1);
217
+ }
218
+
219
+ return values;
220
+ }
@@ -74,10 +74,17 @@ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
74
74
  const selection = ` --profile ${runtime.resolution.profile} --target ${runtime.target}`;
75
75
  const skills = (await runtime.skills.list()).length;
76
76
  const memory = (await runtime.storage.list(`${KNOWLEDGE_LAYOUT.memory}/`)).length;
77
+ const entities = (await runtime.storage.list(`${KNOWLEDGE_LAYOUT.entities}/`)).length;
77
78
  const where = runtime.knowledge?.describe;
78
79
 
79
80
  if (flags.json) {
80
- print(JSON.stringify({ target: runtime.target, where: where ?? 'local', memory, skills }, null, 2));
81
+ print(
82
+ JSON.stringify(
83
+ { target: runtime.target, where: where ?? 'local', memory, skills, entities },
84
+ null,
85
+ 2,
86
+ ),
87
+ );
81
88
  return;
82
89
  }
83
90
 
@@ -96,6 +103,16 @@ export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
96
103
  where ? `${where}/${KNOWLEDGE_LAYOUT.skills}` : layout.skills(profile),
97
104
  style.dim(`${skills} file${skills === 1 ? '' : 's'}`),
98
105
  ],
106
+ // The count includes the derived `_index.json`, deliberately: it is a
107
+ // file in that directory and it is committed with the rest, so a number
108
+ // that quietly excluded it would not match what a person sees there.
109
+ [
110
+ ' entities',
111
+ where
112
+ ? `${where}/${KNOWLEDGE_LAYOUT.entities}`
113
+ : `${layout.blobs(profile)}/${KNOWLEDGE_LAYOUT.entities}`,
114
+ style.dim(`${entities} file${entities === 1 ? '' : 's'}`),
115
+ ],
99
116
  ]);
100
117
 
101
118
  print('');
@@ -266,8 +283,9 @@ async function useLocal(flags: KnowledgeFlags): Promise<void> {
266
283
  const moved = await moveOut(repository, knowledge, local.storage, local.skills);
267
284
  print(
268
285
  ok(
269
- `wrote back ${moved.memory} memory entr${moved.memory === 1 ? 'y' : 'ies'} and ` +
270
- `${moved.skills} skill file${moved.skills === 1 ? '' : 's'}`,
286
+ `wrote back ${moved.memory} memory entr${moved.memory === 1 ? 'y' : 'ies'}, ` +
287
+ `${moved.skills} skill file${moved.skills === 1 ? '' : 's'} and ` +
288
+ `${moved.entities} entity file${moved.entities === 1 ? '' : 's'}`,
271
289
  ),
272
290
  );
273
291
  }
@@ -275,7 +293,7 @@ async function useLocal(flags: KnowledgeFlags): Promise<void> {
275
293
  await writeBlock(runtime.config, runtime.resolution.workspaceRoot, undefined);
276
294
 
277
295
  print('');
278
- print(ok('memory and skills are back on this target\'s own storage'));
296
+ print(ok('memory, skills and entities are back on this target\'s own storage'));
279
297
  print(
280
298
  style.dim(
281
299
  ` ${knowledge.repo} still holds everything — it is version control, so nothing was removed from it.`,
@@ -1,4 +1,10 @@
1
- import { ConfigError, KNOWLEDGE_LAYOUT, knowledgeRoot, type KnowledgeConfig } from '#profile';
1
+ import {
2
+ ConfigError,
3
+ KNOWLEDGE_LAYOUT,
4
+ knowledgeRoot,
5
+ type KnowledgeArea,
6
+ type KnowledgeConfig,
7
+ } from '#profile';
2
8
  import type { BlobStore } from '#stores/blobs';
3
9
  import { commitFiles, type CommitFile } from '#deployments/adapters/github-commit.ts';
4
10
  import type { GithubRepository } from '#deployments/adapters/github-repo.ts';
@@ -24,10 +30,31 @@ export interface Movable {
24
30
  readonly key: string;
25
31
  /** Where it goes in the repository. */
26
32
  readonly path: string;
27
- readonly area: 'memory' | 'skills';
33
+ readonly area: KnowledgeArea;
28
34
  readonly data: Uint8Array;
29
35
  }
30
36
 
37
+ /**
38
+ * Where each area lives locally, as one table.
39
+ *
40
+ * Two of the three are prefixes inside the profile's blob root and the third is
41
+ * a store of its own, and that asymmetry was written out three times before —
42
+ * once per direction — which is three places to forget a fourth area. Stating
43
+ * it once means `localContents`, `removeLocal` and `moveOut` each became a loop
44
+ * over this rather than a branch on a literal.
45
+ */
46
+ type LocalArea = { readonly store: BlobStore; readonly prefix: string };
47
+
48
+ function localAreas(storage: BlobStore, skills: BlobStore): Record<KnowledgeArea, LocalArea> {
49
+ return {
50
+ memory: { store: storage, prefix: `${KNOWLEDGE_LAYOUT.memory}/` },
51
+ skills: { store: skills, prefix: '' },
52
+ entities: { store: storage, prefix: `${KNOWLEDGE_LAYOUT.entities}/` },
53
+ };
54
+ }
55
+
56
+ const AREAS = ['memory', 'skills', 'entities'] as const;
57
+
31
58
  /**
32
59
  * Everything this profile has stored locally, ready to move.
33
60
  *
@@ -42,30 +69,19 @@ export async function localContents(
42
69
  skills: BlobStore,
43
70
  knowledge: KnowledgeConfig,
44
71
  ): Promise<Movable[]> {
72
+ const areas = localAreas(storage, skills);
45
73
  const found: Movable[] = [];
46
- const prefix = `${KNOWLEDGE_LAYOUT.memory}/`;
47
-
48
- for (const entry of await storage.list(prefix)) {
49
- const data = await storage.get(entry.key);
50
- if (data === null) continue; // Listed then deleted; not worth failing over.
51
- const key = entry.key.slice(prefix.length);
52
- found.push({
53
- key,
54
- area: 'memory',
55
- path: `${knowledgeRoot(knowledge, 'memory')}/${key}`,
56
- data,
57
- });
58
- }
59
74
 
60
- for (const entry of await skills.list()) {
61
- const data = await skills.get(entry.key);
62
- if (data === null) continue;
63
- found.push({
64
- key: entry.key,
65
- area: 'skills',
66
- path: `${knowledgeRoot(knowledge, 'skills')}/${entry.key}`,
67
- data,
68
- });
75
+ for (const area of AREAS) {
76
+ const { store, prefix } = areas[area];
77
+
78
+ for (const entry of await store.list(prefix)) {
79
+ const data = await store.get(entry.key);
80
+ if (data === null) continue; // Listed then deleted; not worth failing over.
81
+
82
+ const key = entry.key.slice(prefix.length);
83
+ found.push({ key, area, path: `${knowledgeRoot(knowledge, area)}/${key}`, data });
84
+ }
69
85
  }
70
86
 
71
87
  return found;
@@ -115,9 +131,11 @@ export async function removeLocal(
115
131
  skills: BlobStore,
116
132
  movable: readonly Movable[],
117
133
  ): Promise<void> {
134
+ const areas = localAreas(storage, skills);
135
+
118
136
  for (const item of movable) {
119
- if (item.area === 'memory') await storage.delete(`${KNOWLEDGE_LAYOUT.memory}/${item.key}`);
120
- else await skills.delete(item.key);
137
+ const { store, prefix } = areas[item.area];
138
+ await store.delete(`${prefix}${item.key}`);
121
139
  }
122
140
  }
123
141
 
@@ -135,24 +153,19 @@ export async function moveOut(
135
153
  knowledge: KnowledgeConfig,
136
154
  storage: BlobStore,
137
155
  skills: BlobStore,
138
- ): Promise<{ memory: number; skills: number }> {
156
+ ): Promise<Record<KnowledgeArea, number>> {
139
157
  const { entries } = await repository.entries();
140
- const roots = {
141
- memory: `${knowledgeRoot(knowledge, 'memory')}/`,
142
- skills: `${knowledgeRoot(knowledge, 'skills')}/`,
143
- } as const;
144
-
145
- const counts = { memory: 0, skills: 0 };
158
+ const areas = localAreas(storage, skills);
159
+ const counts: Record<KnowledgeArea, number> = { memory: 0, skills: 0, entities: 0 };
146
160
 
147
161
  for (const entry of entries.values()) {
148
- for (const area of ['memory', 'skills'] as const) {
149
- if (!entry.path.startsWith(roots[area])) continue;
150
-
151
- const key = entry.path.slice(roots[area].length);
152
- const data = await repository.blob(entry.sha);
162
+ for (const area of AREAS) {
163
+ const root = `${knowledgeRoot(knowledge, area)}/`;
164
+ if (!entry.path.startsWith(root)) continue;
153
165
 
154
- if (area === 'memory') await storage.put(`${KNOWLEDGE_LAYOUT.memory}/${key}`, data);
155
- else await skills.put(key, data);
166
+ const key = entry.path.slice(root.length);
167
+ const { store, prefix } = areas[area];
168
+ await store.put(`${prefix}${key}`, await repository.blob(entry.sha));
156
169
 
157
170
  counts[area] += 1;
158
171
  }
@@ -161,14 +174,19 @@ export async function moveOut(
161
174
  return counts;
162
175
  }
163
176
 
164
- /** `12 skills and 47 memory entries`, or whichever half of that is non-zero. */
177
+ /** `12 skills and 47 memory entries`, or whichever parts of that are non-zero. */
165
178
  export function summarise(movable: readonly Movable[]): string {
166
- const skills = movable.filter((item) => item.area === 'skills').length;
167
- const memory = movable.length - skills;
179
+ const count = (area: KnowledgeArea) => movable.filter((item) => item.area === area).length;
180
+ const skills = count('skills');
181
+ const memory = count('memory');
182
+ const entities = count('entities');
168
183
 
184
+ // Skills and memory keep their existing wording and their existing order: a
185
+ // third clause is an addition to this sentence, not a rewrite of it.
169
186
  const parts = [
170
187
  ...(skills > 0 ? [`${skills} skill${skills === 1 ? '' : 's'}`] : []),
171
188
  ...(memory > 0 ? [`${memory} memory entr${memory === 1 ? 'y' : 'ies'}`] : []),
189
+ ...(entities > 0 ? [`${entities} entit${entities === 1 ? 'y' : 'ies'}`] : []),
172
190
  ];
173
191
  return parts.length === 0 ? 'nothing' : parts.join(' and ');
174
192
  }
@@ -154,7 +154,7 @@ interface Surface {
154
154
  * `tools` as a count, matching `/reload` and `connect`; the names beside it.
155
155
  *
156
156
  * One key, one meaning. `ReloadResult.tools` and `PublishOutcome.tools` are both
157
- * numbers, and the count is what `docs/connect.md` tells an operator to compare
157
+ * numbers, and the count is what `https://lanes.sh/docs/link/connect` tells an operator to compare
158
158
  * against their client — shipping the same key here as an array would make
159
159
  * `.tools > 5` true for a single tool.
160
160
  */