@lanes-sh/link 0.8.0 → 0.9.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.
- package/instructions/skills/lanes-link/SKILL.md +12 -12
- package/package.json +1 -1
- package/src/auth/oauth/store.ts +21 -5
- package/src/cli/accepts.ts +6 -5
- package/src/cli/commands/connect/index.ts +2 -1
- package/src/cli/commands/connect/settle.ts +12 -4
- package/src/cli/commands/connection.ts +2 -1
- package/src/cli/commands/identity.ts +4 -4
- package/src/cli/commands/knowledge/index.ts +9 -16
- package/src/cli/commands/knowledge/migrate.ts +9 -4
- package/src/cli/commands/knowledge/show.ts +14 -9
- package/src/cli/commands/operate/migrate.ts +5 -2
- package/src/cli/commands/operate/pair.ts +1 -1
- package/src/cli/commands/operate/serve.ts +1 -1
- package/src/cli/commands/owner/assets.ts +2 -2
- package/src/cli/commands/owner/entities.ts +2 -2
- package/src/cli/commands/owner/memory.ts +2 -2
- package/src/cli/commands/owner/tasks.ts +2 -2
- package/src/cli/commands/owner/vault.ts +3 -3
- package/src/cli/commands/profile/disposition.ts +236 -0
- package/src/cli/commands/profile/removal.ts +100 -13
- package/src/cli/commands/profile/remove.ts +67 -6
- package/src/cli/commands/profile.ts +33 -6
- package/src/cli/commands/secrets.ts +4 -4
- package/src/cli/commands/update-migration.ts +54 -0
- package/src/cli/commands/update.ts +36 -23
- package/src/cli/config-edit.ts +29 -8
- package/src/cli/config-repair-sweep.ts +119 -0
- package/src/cli/config-repair.ts +90 -141
- package/src/cli/config-templates.ts +26 -24
- package/src/cli/contract3-credentials.ts +294 -0
- package/src/cli/contract3-data.ts +143 -209
- package/src/cli/contract3-layout.ts +46 -0
- package/src/cli/contract3-shape.ts +34 -8
- package/src/cli/contract3.ts +141 -24
- package/src/cli/contract4-credentials.ts +207 -0
- package/src/cli/contract4-data.ts +399 -0
- package/src/cli/contract4-rename.ts +73 -0
- package/src/cli/contract4-yaml.ts +223 -0
- package/src/cli/contract4.ts +342 -0
- package/src/cli/identity.ts +44 -26
- package/src/cli/main.ts +6 -1
- package/src/cli/migrate-move.ts +166 -0
- package/src/cli/migrate-plan.ts +3 -3
- package/src/cli/publish.ts +1 -5
- package/src/cli/runtime/open.ts +5 -5
- package/src/cli/runtime/select.ts +2 -11
- package/src/cli/runtime/stores.ts +16 -11
- package/src/cli/runtime/vault.ts +2 -2
- package/src/cli/usage.ts +5 -1
- package/src/cli/workspace-migrate.ts +32 -11
- package/src/connectivity/manifest/provider.ts +31 -12
- package/src/connectivity/transports/imap/parser.ts +70 -9
- package/src/deployments/adapters/filesystem.ts +18 -3
- package/src/deployments/deploy.ts +5 -5
- package/src/deployments/gcp/bucket.ts +42 -6
- package/src/deployments/knowledge.ts +9 -4
- package/src/deployments/target.ts +28 -7
- package/src/deployments/upload.ts +39 -30
- package/src/profile/connections.ts +13 -1
- package/src/profile/deployments.ts +86 -8
- package/src/profile/index.ts +5 -1
- package/src/profile/knowledge.ts +18 -5
- package/src/profile/layout.ts +147 -71
- package/src/profile/load.ts +53 -17
- package/src/profile/schema.ts +11 -2
- package/src/profile/testing.ts +45 -10
- package/src/profile/workspace.ts +66 -30
- package/src/providers/assets/provider.ts +6 -6
- package/src/providers/entities/provider.ts +6 -6
- package/src/providers/entities/writes.ts +1 -1
- package/src/providers/identity/provider.ts +1 -1
- package/src/providers/memory/provider.ts +6 -6
- package/src/providers/setup/provider.ts +3 -3
- package/src/providers/skills/provider.ts +2 -2
- package/src/providers/tasks/provider.ts +6 -6
- package/src/providers/vault/provider.ts +1 -1
- package/src/registry/registry.ts +1 -1
- package/src/server/endpoint.ts +4 -0
- package/src/server/harness.ts +1 -1
- package/src/server/mcp/instructions.ts +21 -21
- package/src/server/mcp/routing.ts +3 -3
- package/src/server/mcp/tools.ts +16 -3
- package/src/server/mcp/visibility.ts +56 -4
- package/src/stores/blobs/conformance.ts +19 -0
- package/src/stores/state/index.ts +76 -10
- package/src/stores/state/testing.ts +5 -1
package/src/profile/load.ts
CHANGED
|
@@ -39,8 +39,12 @@ export interface LoadedConfig {
|
|
|
39
39
|
* 3. Schema shape.
|
|
40
40
|
* 4. Referential integrity, which needs a well-formed document to check.
|
|
41
41
|
*/
|
|
42
|
-
export function validateConfig(
|
|
43
|
-
|
|
42
|
+
export function validateConfig(
|
|
43
|
+
raw: unknown,
|
|
44
|
+
source = '<config>',
|
|
45
|
+
contract = SUPPORTED_CONTRACT,
|
|
46
|
+
): Config {
|
|
47
|
+
const config = validateConfigShape(raw, source, contract);
|
|
44
48
|
assertReferentialIntegrity(config, source);
|
|
45
49
|
return config;
|
|
46
50
|
}
|
|
@@ -58,12 +62,16 @@ export function validateConfig(raw: unknown, source = '<config>'): Config {
|
|
|
58
62
|
* through `validateConfig`, and the split exists so that the one command whose
|
|
59
63
|
* job is to fix a refusal is not blocked by it.
|
|
60
64
|
*/
|
|
61
|
-
export function validateConfigShape(
|
|
65
|
+
export function validateConfigShape(
|
|
66
|
+
raw: unknown,
|
|
67
|
+
source = '<config>',
|
|
68
|
+
contract = SUPPORTED_CONTRACT,
|
|
69
|
+
): Config {
|
|
62
70
|
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
63
71
|
throw new ConfigError(`${source}: expected a YAML mapping at the top level`);
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
assertSupportedContract(raw, source);
|
|
74
|
+
assertSupportedContract(raw, source, contract);
|
|
67
75
|
|
|
68
76
|
const secrets = findSecrets(raw);
|
|
69
77
|
if (secrets.length > 0) {
|
|
@@ -88,7 +96,17 @@ export function validateConfigShape(raw: unknown, source = '<config>'): Config {
|
|
|
88
96
|
* risks reading a document as more permissive than the operator wrote it, and
|
|
89
97
|
* refusing to start is always the safer failure.
|
|
90
98
|
*/
|
|
91
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The contract this document must declare.
|
|
101
|
+
*
|
|
102
|
+
* `expected` is `SUPPORTED_CONTRACT` everywhere except inside a migration,
|
|
103
|
+
* which writes intermediate shapes on the way to the newest one — contract 1 to
|
|
104
|
+
* 2 to 3 to 4, saving at each step. Refusing those would mean a migration
|
|
105
|
+
* cannot use `ConfigDocument` at all and has to write YAML past the validator,
|
|
106
|
+
* losing the secret-shaped-value check that is the reason the validator runs on
|
|
107
|
+
* every save. A step that declares which contract it is producing keeps both.
|
|
108
|
+
*/
|
|
109
|
+
function assertSupportedContract(raw: object, source: string, expected: number): void {
|
|
92
110
|
const contract = (raw as { contract?: unknown }).contract;
|
|
93
111
|
|
|
94
112
|
if (typeof contract !== 'number' || !Number.isInteger(contract)) {
|
|
@@ -97,14 +115,20 @@ function assertSupportedContract(raw: object, source: string): void {
|
|
|
97
115
|
);
|
|
98
116
|
}
|
|
99
117
|
|
|
100
|
-
if (contract !==
|
|
101
|
-
const direction = contract >
|
|
118
|
+
if (contract !== expected) {
|
|
119
|
+
const direction = contract > expected ? 'newer than' : 'older than';
|
|
102
120
|
throw new ConfigError(
|
|
103
121
|
`${source}: contract ${contract} is ${direction} the contract this binary implements (${SUPPORTED_CONTRACT}). ` +
|
|
104
122
|
`Refusing to load rather than guessing at what the document means. ` +
|
|
123
|
+
// **Naming the command is the whole point of this branch.** ADR-051
|
|
124
|
+
// records what a refusal that names none costs: `validateConfig` runs
|
|
125
|
+
// from every command, so one stale workspace takes `status`, `start`,
|
|
126
|
+
// `plan` and `doctor` down together, and "migrate the config" left an
|
|
127
|
+
// operator with a wall and no door. `doctor --fix` runs every migration
|
|
128
|
+
// between the contract on disk and this one.
|
|
105
129
|
(contract > SUPPORTED_CONTRACT
|
|
106
130
|
? 'Upgrade lanes-link.'
|
|
107
|
-
: 'Migrate
|
|
131
|
+
: 'Migrate it with: lanes link doctor --fix'),
|
|
108
132
|
);
|
|
109
133
|
}
|
|
110
134
|
}
|
|
@@ -166,8 +190,11 @@ function formatZodIssues(error: z.ZodError): string {
|
|
|
166
190
|
export interface ProviderRename {
|
|
167
191
|
/** What a row naming the old id should say instead. */
|
|
168
192
|
readonly to: string;
|
|
169
|
-
/**
|
|
170
|
-
|
|
193
|
+
/**
|
|
194
|
+
* The account label that meant this row was the built-in rather than a
|
|
195
|
+
* vendor's, or `null` where the id belongs to nobody now.
|
|
196
|
+
*/
|
|
197
|
+
readonly keeps: string | null;
|
|
171
198
|
/** What the plain noun now names, for the sentence below. */
|
|
172
199
|
readonly becomes: string;
|
|
173
200
|
/** What it used to name. */
|
|
@@ -190,8 +217,12 @@ export interface ProviderRename {
|
|
|
190
217
|
export const RENAMED_PROVIDERS: Readonly<Record<string, ProviderRename>> = {
|
|
191
218
|
tasks: {
|
|
192
219
|
to: 'google_tasks',
|
|
193
|
-
|
|
194
|
-
|
|
220
|
+
// **`null`, where this was `'Tasks'`.** The refusal existed because the
|
|
221
|
+
// built-in claimed `tasks`, so a stale Google Tasks row rebound to it and
|
|
222
|
+
// the label was the only evidence of which was meant (ADR-051). The
|
|
223
|
+
// built-in is `lanes_tasks` now, so no label makes a row here legitimate.
|
|
224
|
+
keeps: null,
|
|
225
|
+
becomes: 'nobody\u2019s provider id \u2014 the built-in task list is lanes_tasks',
|
|
195
226
|
was: 'Google Tasks',
|
|
196
227
|
noun: 'task list',
|
|
197
228
|
},
|
|
@@ -217,8 +248,9 @@ export function renamedProviderFor(connection: {
|
|
|
217
248
|
account: string;
|
|
218
249
|
}): ProviderRename | null {
|
|
219
250
|
const moved = RENAMED_PROVIDERS[connection.provider];
|
|
220
|
-
if (!moved
|
|
221
|
-
|
|
251
|
+
if (!moved) return null;
|
|
252
|
+
// `keeps === null` means the id belongs to nobody, so no label exempts a row.
|
|
253
|
+
return moved.keeps !== null && connection.account === moved.keeps ? null : moved;
|
|
222
254
|
}
|
|
223
255
|
|
|
224
256
|
export function describeRename(
|
|
@@ -229,11 +261,15 @@ export function describeRename(
|
|
|
229
261
|
if (!moved) return null;
|
|
230
262
|
|
|
231
263
|
return (
|
|
232
|
-
|
|
233
|
-
|
|
264
|
+
(moved.keeps === null
|
|
265
|
+
? `"${connection.provider}" is ${moved.becomes}.\n`
|
|
266
|
+
: `"${connection.provider}" is now ${moved.becomes}, and this row is labelled ` +
|
|
267
|
+
`"${connection.account}" rather than "${moved.keeps}".\n`) +
|
|
234
268
|
` If it was ${moved.was}: set provider to ${moved.to} here, and rename any ` +
|
|
235
269
|
`"${connection.provider}.*" policy rule.\n` +
|
|
236
|
-
|
|
270
|
+
(moved.keeps === null
|
|
271
|
+
? ` If it is your own ${moved.noun}: it is lanes_${connection.provider} now.\n`
|
|
272
|
+
: ` If it is your own ${moved.noun}: set account to ${moved.keeps}.\n`) +
|
|
237
273
|
` ${repair} applies the first, where a stored credential proves it.`
|
|
238
274
|
);
|
|
239
275
|
}
|
package/src/profile/schema.ts
CHANGED
|
@@ -50,11 +50,20 @@ import { knowledgeTargetSchema } from './knowledge.ts';
|
|
|
50
50
|
* `members:`, because a profile worth sharing needs to say who may consume it
|
|
51
51
|
* (ADR-060).
|
|
52
52
|
*
|
|
53
|
+
* **4 gave a profile its bytes back, and retired `data/`** (ADR-066, ADR-067).
|
|
54
|
+
* Contract 3 moved the owner layer's stores beside the *connection*, so a
|
|
55
|
+
* profile owned nothing and two profiles granting one memory read one note. The
|
|
56
|
+
* profile is the container again — `profiles/<name>/` holds its declaration and
|
|
57
|
+
* everything it owns — and `data/`, which had been the line between what a
|
|
58
|
+
* revision reads and what it writes, stopped drawing that line the moment the
|
|
59
|
+
* declaration moved inside it. No file's *shape* changed: `grants:`,
|
|
60
|
+
* `members:` and `connections.yaml` are untouched, and only the paths moved.
|
|
61
|
+
*
|
|
53
62
|
* A hard cut each time, and only the newest is read here. `./legacy.ts`
|
|
54
63
|
* understands the older shapes and only the migration uses it — a runtime that
|
|
55
64
|
* loaded either would be the two-sources-of-truth problem again, one level up.
|
|
56
65
|
*/
|
|
57
|
-
export const SUPPORTED_CONTRACT =
|
|
66
|
+
export const SUPPORTED_CONTRACT = 4;
|
|
58
67
|
|
|
59
68
|
/**
|
|
60
69
|
* There is no `database:` block any more.
|
|
@@ -363,7 +372,7 @@ export const policySchema = z.object({
|
|
|
363
372
|
* So the refusal is at load, where it names both rows, rather than at call time
|
|
364
373
|
* where one would silently win.
|
|
365
374
|
*/
|
|
366
|
-
export const SINGLE_INSTANCE_PROVIDERS: readonly string[] = ['
|
|
375
|
+
export const SINGLE_INSTANCE_PROVIDERS: readonly string[] = ['lanes_skills', 'lanes_vault'];
|
|
367
376
|
|
|
368
377
|
/**
|
|
369
378
|
* One connection, and what may be done with it (ADR-058).
|
package/src/profile/testing.ts
CHANGED
|
@@ -89,13 +89,13 @@ export function connectionsYaml(
|
|
|
89
89
|
extra: readonly { id: string; provider: string; account: string }[] = [],
|
|
90
90
|
): string {
|
|
91
91
|
const owner = [
|
|
92
|
-
{ id: '
|
|
93
|
-
{ id: '
|
|
94
|
-
{ id: '
|
|
95
|
-
{ id: '
|
|
96
|
-
{ id: '
|
|
97
|
-
{ id: '
|
|
98
|
-
{ id: '
|
|
92
|
+
{ id: 'lan1', provider: 'lanes_memory', account: 'Memory' },
|
|
93
|
+
{ id: 'lan2', provider: 'lanes_tasks', account: 'Tasks' },
|
|
94
|
+
{ id: 'lan3', provider: 'lanes_assets', account: 'Assets' },
|
|
95
|
+
{ id: 'lan4', provider: 'lanes_skills', account: 'Skills' },
|
|
96
|
+
{ id: 'lan5', provider: 'lanes_vault', account: 'Vault' },
|
|
97
|
+
{ id: 'lan6', provider: 'lanes_setup', account: 'Setup' },
|
|
98
|
+
{ id: 'lan7', provider: 'lanes_entities', account: 'Entities' },
|
|
99
99
|
];
|
|
100
100
|
|
|
101
101
|
const rows = [...owner, ...extra]
|
|
@@ -109,9 +109,19 @@ export function connectionsYaml(
|
|
|
109
109
|
export function grantsYaml(
|
|
110
110
|
extra: readonly { connection: string; allow?: readonly string[]; deny?: readonly string[] }[] = [],
|
|
111
111
|
): string {
|
|
112
|
-
const owner = [
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
const owner = [
|
|
113
|
+
'lanes_memory',
|
|
114
|
+
'lanes_tasks',
|
|
115
|
+
'lanes_assets',
|
|
116
|
+
'lanes_skills',
|
|
117
|
+
'lanes_vault',
|
|
118
|
+
'lanes_setup',
|
|
119
|
+
'lanes_entities',
|
|
120
|
+
].map((provider, index) => ({
|
|
121
|
+
connection: `${provider}.lan${index + 1}`,
|
|
122
|
+
allow: [`${provider}.*`],
|
|
123
|
+
deny: [],
|
|
124
|
+
}));
|
|
115
125
|
|
|
116
126
|
return [...owner, ...extra]
|
|
117
127
|
.map(
|
|
@@ -143,3 +153,28 @@ export function profileYaml(
|
|
|
143
153
|
`members:${members ? `\n${members}` : ' []'}\n`
|
|
144
154
|
);
|
|
145
155
|
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Write a profile's declaration into a fixture workspace.
|
|
159
|
+
*
|
|
160
|
+
* A profile is a directory now (ADR-067), so writing one means creating that
|
|
161
|
+
* directory — which every fixture that used to write `profiles/<name>.yaml`
|
|
162
|
+
* beside its siblings got for free. Centralised here rather than repeated in
|
|
163
|
+
* twenty test files, for the reason `layout.ts` exists at all: the last time a
|
|
164
|
+
* path was spelled in two places one of them went stale, and the failure was a
|
|
165
|
+
* listing that disagreed with a loader about what existed.
|
|
166
|
+
*/
|
|
167
|
+
export async function writeProfileFixture(
|
|
168
|
+
root: string,
|
|
169
|
+
profile: string,
|
|
170
|
+
body: string,
|
|
171
|
+
): Promise<string> {
|
|
172
|
+
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
173
|
+
const { dirname, join } = await import('node:path');
|
|
174
|
+
const { layout } = await import('./layout.ts');
|
|
175
|
+
|
|
176
|
+
const path = join(root, layout.profileConfig(profile));
|
|
177
|
+
await mkdir(dirname(path), { recursive: true });
|
|
178
|
+
await writeFile(path, body);
|
|
179
|
+
return path;
|
|
180
|
+
}
|
package/src/profile/workspace.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
+
import { layout, legacyProfileConfig, LEGACY_WORKSPACE_FILE, PROFILE_FILE } from './layout.ts';
|
|
2
3
|
import { isRemoteWorkspace, readWorkspaceFile, workspaceFiles } from './files.ts';
|
|
3
4
|
import { parseConfig, type LoadedConfig } from './load.ts';
|
|
4
5
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
@@ -19,13 +20,8 @@ import { findSecrets, formatSecretFindings } from './secret-detection.ts';
|
|
|
19
20
|
/**
|
|
20
21
|
* Workspace and profile resolution.
|
|
21
22
|
*
|
|
22
|
-
* A workspace
|
|
23
|
-
*
|
|
24
|
-
* lanes-link.yaml workspace settings: contract, default_profile
|
|
25
|
-
* profiles/
|
|
26
|
-
* personal.yaml
|
|
27
|
-
* work.yaml
|
|
28
|
-
* data/ local state per profile, gitignored
|
|
23
|
+
* A workspace holds its registry, its accounts, and one directory per profile.
|
|
24
|
+
* `layout.ts` has the tree.
|
|
29
25
|
*
|
|
30
26
|
* **A command says which profile it means, or it does not run** (ADR-037).
|
|
31
27
|
* `--profile` is the only thing that selects one. `LANES_LINK_PROFILE` and
|
|
@@ -33,23 +29,23 @@ import { findSecrets, formatSecretFindings } from './secret-detection.ts';
|
|
|
33
29
|
*
|
|
34
30
|
* The argument this replaces was that persisted selection is how operators act
|
|
35
31
|
* on the wrong thing, and that a *visible* fallback — an exported variable, a
|
|
36
|
-
* key in a file the operator reads,
|
|
37
|
-
*
|
|
32
|
+
* key in a file the operator reads, a line printed before every command — was
|
|
33
|
+
* therefore safe. The first half stands and is why this rule exists at all.
|
|
38
34
|
* What did not survive is the conclusion: the printed line is a dim grey one,
|
|
39
35
|
* and a fallback made an ignored flag survivable, so `profile add --target
|
|
40
36
|
* cloud` dropping its flag surfaced on the *next* command, from a different
|
|
41
|
-
* source, detached from its cause. A resolver with nothing to fall back to
|
|
42
|
-
* cannot do that.
|
|
37
|
+
* source, detached from its cause. A resolver with nothing to fall back to cannot.
|
|
43
38
|
*
|
|
44
39
|
* The workspace root is deliberately not part of this and keeps its chain —
|
|
45
|
-
* `LANES_LINK_HOME`, then an ancestor holding `
|
|
46
|
-
*
|
|
40
|
+
* `LANES_LINK_HOME`, then an ancestor holding `workspaces.yaml` (or the
|
|
41
|
+
* `lanes-link.yaml` it was called before contract 4), then `~/.lanes-link`.
|
|
42
|
+
* Getting it wrong yields "no profiles here" rather than an
|
|
47
43
|
* action against the wrong account, it is the only channel a container has for
|
|
48
44
|
* its bucket (ADR-023), and the ancestor walk is what makes a per-repository
|
|
49
45
|
* workspace work at all.
|
|
50
46
|
*/
|
|
51
47
|
|
|
52
|
-
export const WORKSPACE_FILE = '
|
|
48
|
+
export const WORKSPACE_FILE = 'workspaces.yaml';
|
|
53
49
|
|
|
54
50
|
/** A profile, found. Everything a command needs before it has read the config. */
|
|
55
51
|
export interface ProfileSelection {
|
|
@@ -104,7 +100,9 @@ export function resolveWorkspaceRoot(options: ResolveOptions = {}): string {
|
|
|
104
100
|
// `existsSync`, not `Bun.file(path).size`: a missing file reports size 0,
|
|
105
101
|
// so a `>= 0` check would call every candidate a workspace and stop at the
|
|
106
102
|
// first directory it looked at.
|
|
107
|
-
|
|
103
|
+
// Either name: a root that cannot be found cannot be migrated.
|
|
104
|
+
const marker = (name: string): boolean => existsSync(join(directory, name));
|
|
105
|
+
if (marker(WORKSPACE_FILE) || marker(LEGACY_WORKSPACE_FILE)) return directory;
|
|
108
106
|
const parent = dirname(directory);
|
|
109
107
|
if (parent === directory) break;
|
|
110
108
|
directory = parent;
|
|
@@ -114,7 +112,7 @@ export function resolveWorkspaceRoot(options: ResolveOptions = {}): string {
|
|
|
114
112
|
}
|
|
115
113
|
|
|
116
114
|
/**
|
|
117
|
-
* Where Lanes Link itself is installed — the directory
|
|
115
|
+
* Where Lanes Link itself is installed — the directory with `package.json`,
|
|
118
116
|
* and with it `skills/` and `docs/`.
|
|
119
117
|
*
|
|
120
118
|
* Not the workspace: this is the code, not the operator's data. Found by
|
|
@@ -144,7 +142,7 @@ export function installRoot(from: string): string {
|
|
|
144
142
|
* workspace stopped being a directory.
|
|
145
143
|
*/
|
|
146
144
|
export function profilePath(workspaceRoot: string, profile: string): string {
|
|
147
|
-
const key =
|
|
145
|
+
const key = layout.profileConfig(profile);
|
|
148
146
|
return isRemoteWorkspace(workspaceRoot) ? `${workspaceRoot}/${key}` : join(workspaceRoot, key);
|
|
149
147
|
}
|
|
150
148
|
|
|
@@ -153,7 +151,7 @@ export async function loadProfileConfig(
|
|
|
153
151
|
workspaceRoot: string,
|
|
154
152
|
profile: string,
|
|
155
153
|
): Promise<LoadedConfig> {
|
|
156
|
-
const key =
|
|
154
|
+
const key = layout.profileConfig(profile);
|
|
157
155
|
const text = await readWorkspaceFile(workspaceFiles(workspaceRoot), key);
|
|
158
156
|
const shown = profilePath(workspaceRoot, profile);
|
|
159
157
|
|
|
@@ -208,9 +206,13 @@ export async function readConnections(workspaceRoot: string): Promise<Connection
|
|
|
208
206
|
}
|
|
209
207
|
|
|
210
208
|
export async function readWorkspace(workspaceRoot: string): Promise<WorkspaceConfig | null> {
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
// New name first: a workspace mid-migration still has the old file, and
|
|
210
|
+
// preferring it would undo the rename on the next write.
|
|
211
|
+
const files = workspaceFiles(workspaceRoot);
|
|
212
|
+
const current = await readWorkspaceFile(files, WORKSPACE_FILE);
|
|
213
|
+
const text = current ?? (await readWorkspaceFile(files, LEGACY_WORKSPACE_FILE));
|
|
213
214
|
if (text === null) return null;
|
|
215
|
+
const path = join(workspaceRoot, current === null ? LEGACY_WORKSPACE_FILE : WORKSPACE_FILE);
|
|
214
216
|
|
|
215
217
|
const parsed = workspaceSchema.safeParse(parseYaml(text));
|
|
216
218
|
if (!parsed.success) {
|
|
@@ -223,15 +225,34 @@ export async function readWorkspace(workspaceRoot: string): Promise<WorkspaceCon
|
|
|
223
225
|
|
|
224
226
|
export async function listProfiles(workspaceRoot: string): Promise<string[]> {
|
|
225
227
|
try {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
228
|
+
const root = `${layout.profilesRoot()}/`;
|
|
229
|
+
const entries = await workspaceFiles(workspaceRoot).list(root);
|
|
230
|
+
|
|
231
|
+
// A profile is a *directory* holding a `profile.yaml` (ADR-067), so the
|
|
232
|
+
// shape matched is `<name>/profile.yaml`. The listing walks each profile's
|
|
233
|
+
// memory and assets on the way past, which is the price of the declaration
|
|
234
|
+
// sitting beside the bytes; `audit.log/` is the workspace's, so the largest
|
|
235
|
+
// collection is not in here.
|
|
236
|
+
//
|
|
237
|
+
// **Both shapes**, because every migration enumerates through here: listing
|
|
238
|
+
// only the new one leaves an unmigrated workspace holding no profiles as
|
|
239
|
+
// far as its own migration is concerned. Same rule as the marker file.
|
|
240
|
+
const names = new Set<string>();
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
const rest = entry.key.slice(root.length);
|
|
243
|
+
const parts = rest.split('/');
|
|
244
|
+
|
|
245
|
+
if (parts.length === 2 && parts[1] === PROFILE_FILE) {
|
|
246
|
+
names.add(parts[0]!);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Contract 3 and earlier. `.example.yaml` was never a profile.
|
|
251
|
+
if (parts.length === 1 && rest.endsWith('.yaml') && !rest.endsWith('.example.yaml')) {
|
|
252
|
+
names.add(rest.slice(0, -'.yaml'.length));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return [...names].sort();
|
|
235
256
|
} catch {
|
|
236
257
|
return [];
|
|
237
258
|
}
|
|
@@ -254,8 +275,23 @@ export async function resolveSelection(options: ResolveOptions = {}): Promise<Pr
|
|
|
254
275
|
if (!profile) throw noProfileNamed(workspaceRoot, await listProfiles(workspaceRoot), env);
|
|
255
276
|
|
|
256
277
|
const path = profilePath(workspaceRoot, profile);
|
|
257
|
-
|
|
278
|
+
const files = workspaceFiles(workspaceRoot);
|
|
279
|
+
|
|
280
|
+
if (!(await files.has(layout.profileConfig(profile)))) {
|
|
258
281
|
const available = await listProfiles(workspaceRoot);
|
|
282
|
+
|
|
283
|
+
// **It exists, at the path contract 3 kept it.** Without this the refusal
|
|
284
|
+
// reads "profile personal does not exist. Available: personal" — what a
|
|
285
|
+
// listing that understands both layouts and a lookup that understands one
|
|
286
|
+
// produce together — and names no way forward (ADR-051).
|
|
287
|
+
if (await files.has(legacyProfileConfig(profile))) {
|
|
288
|
+
throw new ConfigError(
|
|
289
|
+
`Profile "${profile}" is still laid out the way contract 3 kept it, and nothing reads ` +
|
|
290
|
+
`that any more.\n Migrate it with: lanes link doctor --fix --profile ${profile} ` +
|
|
291
|
+
'--workspace <name>',
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
259
295
|
throw new ConfigError(
|
|
260
296
|
`Profile "${profile}" does not exist (looked for ${path}).\n` +
|
|
261
297
|
(available.length > 0 ? `Available: ${available.join(', ')}` : 'No profiles exist yet.'),
|
|
@@ -72,7 +72,7 @@ const MAX_ASSET_BYTES = 25 * 1024 * 1024;
|
|
|
72
72
|
const MAX_TEXT_BYTES = 256 * 1024;
|
|
73
73
|
|
|
74
74
|
export const assetsProvider: ProviderDefinition = defineLocalProvider({
|
|
75
|
-
id: '
|
|
75
|
+
id: 'lanes_assets',
|
|
76
76
|
name: 'Assets',
|
|
77
77
|
version: '1.0.0',
|
|
78
78
|
description:
|
|
@@ -104,12 +104,12 @@ export const assetsProvider: ProviderDefinition = defineLocalProvider({
|
|
|
104
104
|
title: 'Stored file',
|
|
105
105
|
description:
|
|
106
106
|
'One stored file, addressed by its name. Text comes back as text; anything else is described rather than encoded.',
|
|
107
|
-
uriTemplate: 'assets://file/{name}',
|
|
107
|
+
uriTemplate: 'lanes-assets://file/{name}',
|
|
108
108
|
redact: keepKeys('uri'),
|
|
109
109
|
|
|
110
110
|
async list(context) {
|
|
111
111
|
return (await allAssets(context.storage)).map((asset) => ({
|
|
112
|
-
uri: `assets://file/${encodeURIComponent(asset.name)}`,
|
|
112
|
+
uri: `lanes-assets://file/${encodeURIComponent(asset.name)}`,
|
|
113
113
|
name: asset.name,
|
|
114
114
|
}));
|
|
115
115
|
},
|
|
@@ -171,7 +171,7 @@ export const assetsProvider: ProviderDefinition = defineLocalProvider({
|
|
|
171
171
|
...shown.flatMap((asset) => [
|
|
172
172
|
{
|
|
173
173
|
type: 'resource_link' as const,
|
|
174
|
-
uri: `assets://file/${encodeURIComponent(asset.name)}`,
|
|
174
|
+
uri: `lanes-assets://file/${encodeURIComponent(asset.name)}`,
|
|
175
175
|
name: asset.name,
|
|
176
176
|
},
|
|
177
177
|
{ type: 'text' as const, text: describeAsset(asset) },
|
|
@@ -194,7 +194,7 @@ export const assetsProvider: ProviderDefinition = defineLocalProvider({
|
|
|
194
194
|
name: 'get',
|
|
195
195
|
title: 'Read a stored file',
|
|
196
196
|
description:
|
|
197
|
-
'Return a text file\'s contents. A binary file is described instead — name, type, size, digest — because encoding it here is the cost this provider exists to avoid. The resource assets://file/{name} is the same content.',
|
|
197
|
+
'Return a text file\'s contents. A binary file is described instead — name, type, size, digest — because encoding it here is the cost this provider exists to avoid. The resource lanes-assets://file/{name} is the same content.',
|
|
198
198
|
inputSchema: z.object({ name: z.string().min(1).describe('The file name') }),
|
|
199
199
|
redact: keepKeys('name'),
|
|
200
200
|
async handler({ name }, context) {
|
|
@@ -271,7 +271,7 @@ export const assetsProvider: ProviderDefinition = defineLocalProvider({
|
|
|
271
271
|
},
|
|
272
272
|
{
|
|
273
273
|
type: 'resource_link',
|
|
274
|
-
uri: `assets://file/${encodeURIComponent(assetName)}`,
|
|
274
|
+
uri: `lanes-assets://file/${encodeURIComponent(assetName)}`,
|
|
275
275
|
name: assetName,
|
|
276
276
|
},
|
|
277
277
|
],
|
|
@@ -58,7 +58,7 @@ const DESCRIPTION =
|
|
|
58
58
|
'so more than one means ask rather than take the first.';
|
|
59
59
|
|
|
60
60
|
export const entitiesProvider: ProviderDefinition = defineLocalProvider({
|
|
61
|
-
id: '
|
|
61
|
+
id: 'lanes_entities',
|
|
62
62
|
name: 'Entities',
|
|
63
63
|
version: '1.0.0',
|
|
64
64
|
description: DESCRIPTION,
|
|
@@ -99,14 +99,14 @@ export const entitiesProvider: ProviderDefinition = defineLocalProvider({
|
|
|
99
99
|
name: 'entity',
|
|
100
100
|
title: 'Entity',
|
|
101
101
|
description: 'One declared entity, addressed by its id.',
|
|
102
|
-
uriTemplate: 'entities://entity/{id}',
|
|
102
|
+
uriTemplate: 'lanes-entities://entity/{id}',
|
|
103
103
|
mimeType: 'text/markdown',
|
|
104
104
|
redact: keepKeys('uri'),
|
|
105
105
|
|
|
106
106
|
async list(context) {
|
|
107
107
|
const catalogue = await openCatalogue(context.storage);
|
|
108
108
|
return catalogue.entities.map((entity) => ({
|
|
109
|
-
uri: `entities://entity/${encodeURIComponent(entity.id)}`,
|
|
109
|
+
uri: `lanes-entities://entity/${encodeURIComponent(entity.id)}`,
|
|
110
110
|
name: entity.name,
|
|
111
111
|
}));
|
|
112
112
|
},
|
|
@@ -230,7 +230,7 @@ export const entitiesProvider: ProviderDefinition = defineLocalProvider({
|
|
|
230
230
|
{ type: 'text', text: renderCandidates(matches, criteria, context.connection.key) },
|
|
231
231
|
...matches.candidates.map((one) => ({
|
|
232
232
|
type: 'resource_link' as const,
|
|
233
|
-
uri: `entities://entity/${encodeURIComponent(one.entity.id)}`,
|
|
233
|
+
uri: `lanes-entities://entity/${encodeURIComponent(one.entity.id)}`,
|
|
234
234
|
name: one.entity.name,
|
|
235
235
|
})),
|
|
236
236
|
],
|
|
@@ -244,7 +244,7 @@ export const entitiesProvider: ProviderDefinition = defineLocalProvider({
|
|
|
244
244
|
content: [
|
|
245
245
|
{
|
|
246
246
|
type: 'resource_link' as const,
|
|
247
|
-
uri: `entities://entity/${encodeURIComponent(found.id)}`,
|
|
247
|
+
uri: `lanes-entities://entity/${encodeURIComponent(found.id)}`,
|
|
248
248
|
name: found.name,
|
|
249
249
|
},
|
|
250
250
|
{ type: 'text', text: renderEntity(found, catalogue, file?.body ?? '') },
|
|
@@ -259,7 +259,7 @@ export const entitiesProvider: ProviderDefinition = defineLocalProvider({
|
|
|
259
259
|
title: 'Read one entity',
|
|
260
260
|
description:
|
|
261
261
|
'Return one entity by id, with its relationships and everything pointing at it. The resource ' +
|
|
262
|
-
'entities://entity/{id} is the same content; this exists for clients that do not read resources.',
|
|
262
|
+
'lanes-entities://entity/{id} is the same content; this exists for clients that do not read resources.',
|
|
263
263
|
inputSchema: z.object({ id: z.string().min(1).describe('Entity id') }),
|
|
264
264
|
redact: keepKeys('id'),
|
|
265
265
|
async handler({ id }, context) {
|
|
@@ -120,7 +120,7 @@ export const writeCapabilities: readonly Capability[] = [
|
|
|
120
120
|
type: 'text',
|
|
121
121
|
text: `${existing === null ? 'Declared' : 'Updated'} "${id}" on ${context.connection.key}.`,
|
|
122
122
|
},
|
|
123
|
-
{ type: 'resource_link', uri: `entities://entity/${id}`, name },
|
|
123
|
+
{ type: 'resource_link', uri: `lanes-entities://entity/${id}`, name },
|
|
124
124
|
],
|
|
125
125
|
};
|
|
126
126
|
},
|
|
@@ -172,7 +172,7 @@ function slugify(title: string): string {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
export const memoryProvider: ProviderDefinition = defineLocalProvider({
|
|
175
|
-
id: '
|
|
175
|
+
id: 'lanes_memory',
|
|
176
176
|
name: 'Memory',
|
|
177
177
|
version: '1.0.0',
|
|
178
178
|
description:
|
|
@@ -212,7 +212,7 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
|
|
|
212
212
|
name: 'entry',
|
|
213
213
|
title: 'Memory entry',
|
|
214
214
|
description: 'One stored memory entry, addressed by its id.',
|
|
215
|
-
uriTemplate: 'memory://entry/{id}',
|
|
215
|
+
uriTemplate: 'lanes-memory://entry/{id}',
|
|
216
216
|
mimeType: 'text/markdown',
|
|
217
217
|
// The address is worth recording and the content is not — the same trade
|
|
218
218
|
// `gmail.get_message` makes with a message id.
|
|
@@ -220,7 +220,7 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
|
|
|
220
220
|
|
|
221
221
|
async list(context) {
|
|
222
222
|
return (await allEntries(context.storage)).map((entry) => ({
|
|
223
|
-
uri: `memory://entry/${encodeURIComponent(entry.id)}`,
|
|
223
|
+
uri: `lanes-memory://entry/${encodeURIComponent(entry.id)}`,
|
|
224
224
|
name: entry.title,
|
|
225
225
|
}));
|
|
226
226
|
},
|
|
@@ -242,7 +242,7 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
|
|
|
242
242
|
name: 'get',
|
|
243
243
|
title: 'Read a memory entry',
|
|
244
244
|
description:
|
|
245
|
-
'Return one entry by id. The resource memory://entry/{id} is the same content; this exists for clients that do not read resources.',
|
|
245
|
+
'Return one entry by id. The resource lanes-memory://entry/{id} is the same content; this exists for clients that do not read resources.',
|
|
246
246
|
inputSchema: z.object({
|
|
247
247
|
id: z.string().min(1).describe('Entry id'),
|
|
248
248
|
}),
|
|
@@ -326,7 +326,7 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
|
|
|
326
326
|
content: matches.flatMap(({ entry, snippet }) => [
|
|
327
327
|
{
|
|
328
328
|
type: 'resource_link' as const,
|
|
329
|
-
uri: `memory://entry/${encodeURIComponent(entry.id)}`,
|
|
329
|
+
uri: `lanes-memory://entry/${encodeURIComponent(entry.id)}`,
|
|
330
330
|
name: entry.title,
|
|
331
331
|
},
|
|
332
332
|
{
|
|
@@ -370,7 +370,7 @@ export const memoryProvider: ProviderDefinition = defineLocalProvider({
|
|
|
370
370
|
return {
|
|
371
371
|
content: [
|
|
372
372
|
{ type: 'text', text: `Stored memory entry "${entryId}" on ${context.connection.key}.` },
|
|
373
|
-
{ type: 'resource_link', uri: `memory://entry/${entryId}`, name: title },
|
|
373
|
+
{ type: 'resource_link', uri: `lanes-memory://entry/${entryId}`, name: title },
|
|
374
374
|
],
|
|
375
375
|
};
|
|
376
376
|
},
|
|
@@ -92,7 +92,7 @@ export function createSetupProvider(options: SetupProviderOptions): ProviderDefi
|
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
return defineLocalProvider({
|
|
95
|
-
id: '
|
|
95
|
+
id: 'lanes_setup',
|
|
96
96
|
name: 'Setup',
|
|
97
97
|
version: '1.0.0',
|
|
98
98
|
description:
|
|
@@ -147,7 +147,7 @@ export function createSetupProvider(options: SetupProviderOptions): ProviderDefi
|
|
|
147
147
|
'The console steps, the values needed, and the exact command that connects it. ' +
|
|
148
148
|
'Use this to tell the owner what to run — do not compose the command yourself.',
|
|
149
149
|
inputSchema: z.object({
|
|
150
|
-
id: z.string().min(1).describe('Provider id, as listed by
|
|
150
|
+
id: z.string().min(1).describe('Provider id, as listed by lanes_setup_overview — e.g. "notion"'),
|
|
151
151
|
connection: z
|
|
152
152
|
.string()
|
|
153
153
|
.optional()
|
|
@@ -167,7 +167,7 @@ export function createSetupProvider(options: SetupProviderOptions): ProviderDefi
|
|
|
167
167
|
{
|
|
168
168
|
type: 'text',
|
|
169
169
|
text:
|
|
170
|
-
`No provider "${id}". Call
|
|
170
|
+
`No provider "${id}". Call lanes_setup_overview for the ids this endpoint knows.`,
|
|
171
171
|
},
|
|
172
172
|
],
|
|
173
173
|
isError: true,
|
|
@@ -110,7 +110,7 @@ export function createSkillsProvider(options: SkillsProviderOptions): ProviderDe
|
|
|
110
110
|
const manage = options.store ? managementCapabilities(options.store, options.onChange) : [];
|
|
111
111
|
|
|
112
112
|
return defineLocalProvider({
|
|
113
|
-
id: '
|
|
113
|
+
id: 'lanes_skills',
|
|
114
114
|
name: 'Skills',
|
|
115
115
|
version: '1.0.0',
|
|
116
116
|
description:
|
|
@@ -246,7 +246,7 @@ function managementCapabilities(
|
|
|
246
246
|
content: [
|
|
247
247
|
{
|
|
248
248
|
type: 'text',
|
|
249
|
-
text: `Stored skill "${skill.name}" on ${context.connection.key}. It is available as the prompt "
|
|
249
|
+
text: `Stored skill "${skill.name}" on ${context.connection.key}. It is available as the prompt "lanes_skills_${skill.name}" where policy allows it.`,
|
|
250
250
|
},
|
|
251
251
|
],
|
|
252
252
|
};
|