@lanes-sh/link 0.7.2 → 0.8.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/README.md +20 -10
- package/instructions/agents/lanes-link-scout.md +2 -2
- package/instructions/skills/lanes-link/SKILL.md +136 -61
- package/package.json +2 -1
- package/src/audit/index.ts +8 -1
- package/src/auth/index.ts +58 -2
- package/src/auth/lanes/assertion.ts +256 -0
- package/src/auth/lanes/callback.ts +135 -0
- package/src/auth/lanes/federation.ts +50 -0
- package/src/auth/lanes/login.ts +294 -0
- package/src/auth/lanes/members.ts +103 -0
- package/src/auth/lanes/session.ts +97 -0
- package/src/auth/oauth/grant.ts +183 -0
- package/src/auth/oauth/result.ts +27 -0
- package/src/auth/oauth/server.ts +176 -203
- package/src/auth/oauth/store.ts +65 -0
- package/src/auth/remote.ts +32 -9
- package/src/cli/accepts.ts +108 -0
- package/src/cli/argv.ts +57 -3
- package/src/cli/audit-change.ts +140 -0
- package/src/cli/callback-page.ts +36 -115
- package/src/cli/commands/auth-dispatch.ts +48 -0
- package/src/cli/commands/auth.ts +229 -0
- package/src/cli/commands/connect/accounts.ts +4 -4
- package/src/cli/commands/connect/authorise.ts +4 -4
- package/src/cli/commands/connect/bind-credential.ts +2 -1
- package/src/cli/commands/connect/custom/index.ts +1 -1
- package/src/cli/commands/connect/custom/write.ts +2 -2
- package/src/cli/commands/connect/grant.ts +29 -14
- package/src/cli/commands/connect/index.ts +88 -87
- package/src/cli/commands/connect/options.ts +83 -0
- package/src/cli/commands/connect/registration.ts +50 -0
- package/src/cli/commands/connect/requirements.ts +1 -1
- package/src/cli/commands/connect/settle.ts +4 -2
- package/src/cli/commands/connect/target-note.ts +7 -2
- package/src/cli/commands/connect/unknown.ts +1 -1
- package/src/cli/commands/connect/variables.ts +3 -2
- package/src/cli/commands/connection-list.ts +116 -0
- package/src/cli/commands/connection.ts +182 -165
- package/src/cli/commands/grant.ts +140 -0
- package/src/cli/commands/identity.ts +21 -9
- package/src/cli/commands/knowledge/index.ts +46 -79
- package/src/cli/commands/knowledge/migrate.ts +74 -13
- package/src/cli/commands/knowledge/show.ts +92 -0
- package/src/cli/commands/knowledge.ts +2 -1
- package/src/cli/commands/mcp/harnesses.ts +30 -8
- package/src/cli/commands/mcp/onboarding.ts +86 -0
- package/src/cli/commands/mcp/register.ts +16 -2
- package/src/cli/commands/mcp.ts +1 -0
- package/src/cli/commands/members.ts +288 -0
- package/src/cli/commands/operate/attach.ts +3 -3
- package/src/cli/commands/operate/audit.ts +11 -7
- package/src/cli/commands/operate/auth.ts +28 -11
- package/src/cli/commands/operate/findings.ts +2 -1
- package/src/cli/commands/operate/inspect.ts +37 -19
- package/src/cli/commands/operate/migrate.ts +29 -12
- package/src/cli/commands/operate/outputs.ts +3 -3
- package/src/cli/commands/operate/pair-certificate.ts +141 -0
- package/src/cli/commands/operate/pair.ts +324 -0
- package/src/cli/commands/operate/policy.ts +73 -22
- package/src/cli/commands/operate/serve.ts +52 -4
- package/src/cli/commands/operate/status.ts +18 -10
- package/src/cli/commands/operate/tools.ts +2 -2
- package/src/cli/commands/operate.ts +2 -0
- package/src/cli/commands/owner/shared.ts +13 -2
- package/src/cli/commands/owner/skills.ts +28 -8
- package/src/cli/commands/profile/removal.ts +79 -76
- package/src/cli/commands/profile/remove.ts +16 -1
- package/src/cli/commands/profile.ts +46 -10
- package/src/cli/commands/relabel.ts +112 -0
- package/src/cli/commands/secrets.ts +34 -12
- package/src/cli/commands/set-workspace.ts +96 -0
- package/src/cli/commands/setup.ts +2 -2
- package/src/cli/commands/sync.ts +8 -8
- package/src/cli/commands/target.ts +9 -7
- package/src/cli/commands/update.ts +58 -17
- package/src/cli/config-edit.ts +75 -140
- package/src/cli/config-migrate.ts +82 -64
- package/src/cli/config-repair.ts +89 -32
- package/src/cli/config-templates.ts +198 -0
- package/src/cli/contract3-data.ts +328 -0
- package/src/cli/contract3-shape.ts +186 -0
- package/src/cli/contract3.ts +282 -0
- package/src/cli/endpoint-url.ts +1 -1
- package/src/cli/lanes.ts +25 -1
- package/src/cli/main.ts +89 -14
- package/src/cli/migrate-plan.ts +12 -6
- package/src/cli/output.ts +34 -1
- package/src/cli/publish.ts +5 -2
- package/src/cli/runtime/open.ts +63 -98
- package/src/cli/runtime/registry.ts +6 -7
- package/src/cli/runtime/stores.ts +53 -0
- package/src/cli/runtime/types.ts +106 -0
- package/src/cli/runtime/vault.ts +19 -4
- package/src/cli/runtime/workspace.ts +60 -0
- package/src/cli/runtime.ts +2 -1
- package/src/cli/selection-require.ts +44 -13
- package/src/cli/selection.ts +127 -145
- package/src/cli/usage.ts +34 -18
- package/src/cli/workspace-migrate.ts +125 -16
- package/src/connectivity/manifest/provider.ts +3 -1
- package/src/connectivity/manifest/requirements.ts +1 -1
- package/src/deployments/bind.ts +1 -1
- package/src/deployments/deploy.ts +36 -27
- package/src/deployments/gcp/bucket.ts +18 -7
- package/src/deployments/gcp/provision.ts +7 -7
- package/src/deployments/prepare.ts +72 -24
- package/src/deployments/record.ts +1 -1
- package/src/deployments/report.ts +2 -2
- package/src/deployments/serving.ts +15 -74
- package/src/deployments/target.ts +15 -15
- package/src/deployments/upload.ts +46 -22
- package/src/dispatch/deps.ts +88 -0
- package/src/dispatch/dispatch.ts +21 -62
- package/src/policy/index.ts +47 -15
- package/src/profile/connections.ts +183 -0
- package/src/profile/deployments.ts +3 -3
- package/src/profile/index.ts +30 -5
- package/src/profile/layout.ts +86 -89
- package/src/profile/load.ts +80 -47
- package/src/profile/pairing.ts +32 -0
- package/src/profile/primitives.ts +35 -1
- package/src/profile/registry.ts +6 -6
- package/src/profile/schema.ts +172 -21
- package/src/profile/targets.ts +21 -9
- package/src/profile/testing.ts +69 -2
- package/src/profile/workspace.ts +58 -3
- package/src/providers/custom/index.ts +1 -1
- package/src/providers/custom/load.ts +2 -3
- package/src/providers/identity/provider.ts +1 -1
- package/src/providers/memory/provider.ts +20 -2
- package/src/providers/setup/plan.ts +1 -1
- package/src/providers/slack/index.ts +2 -2
- package/src/registry/policy-bridge.ts +33 -11
- package/src/registry/reconcile.ts +4 -4
- package/src/server/authorization.ts +94 -0
- package/src/server/edge.ts +14 -1
- package/src/server/endpoint.ts +85 -104
- package/src/server/generation.ts +10 -1
- package/src/server/harness.ts +71 -13
- package/src/server/index.ts +31 -0
- package/src/server/mcp/build.ts +20 -1
- package/src/server/mcp/client-info.ts +54 -0
- package/src/server/mcp/guide.ts +120 -0
- package/src/server/mcp/instructions.ts +1 -1
- package/src/server/mcp/prompts.ts +7 -3
- package/src/server/mcp/resources.ts +16 -8
- package/src/server/mcp/tools.ts +9 -3
- package/src/server/mcp/visibility.ts +18 -3
- package/src/server/oauth.ts +29 -109
- package/src/server/read/credential.ts +134 -0
- package/src/server/read/deployed.ts +56 -0
- package/src/server/read/listener.ts +54 -0
- package/src/server/read/open.ts +101 -0
- package/src/server/read/routes.ts +247 -0
- package/src/server/read/state.ts +171 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { CONNECTIONS_FILE, type IdentityEntry, type ProfileSelection, type Resolution } from '#profile';
|
|
2
2
|
import { ConfigDocument } from '../config-edit.ts';
|
|
3
3
|
import { ensureIdentityConnection, repairLines, repaired } from '../config-repair.ts';
|
|
4
4
|
import { announce, announceProfile, emit, ok, print, style, table, warn } from '../output.ts';
|
|
@@ -51,14 +51,20 @@ function covers(rules: ReadonlyArray<{ capability: string }>): boolean {
|
|
|
51
51
|
* misleading thing this command could print.
|
|
52
52
|
*/
|
|
53
53
|
function readable(config: {
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
grants: ReadonlyArray<{
|
|
55
|
+
connection: string;
|
|
56
|
+
allow: ReadonlyArray<{ capability: string }>;
|
|
57
|
+
deny: ReadonlyArray<{ capability: string }>;
|
|
58
|
+
}>;
|
|
56
59
|
}): boolean {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
60
|
+
// One row now rather than a connection and a rule that had to agree
|
|
61
|
+
// (ADR-058). The old shape could say "declared but not granted" and
|
|
62
|
+
// "granted but not declared", and both served nothing while reading like
|
|
63
|
+
// configuration that worked; neither is expressible here.
|
|
64
|
+
const grant = config.grants.find((row) => row.connection.startsWith('identity.'));
|
|
65
|
+
if (grant === undefined) return false;
|
|
66
|
+
|
|
67
|
+
return covers(grant.allow) && !covers(grant.deny);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
/**
|
|
@@ -96,7 +102,13 @@ export async function addIdentity(
|
|
|
96
102
|
const entry: IdentityEntry = { kind, value, ...(options.note ? { note: options.note } : {}) };
|
|
97
103
|
document.addTo(['identity'], entry, { inline: true });
|
|
98
104
|
|
|
99
|
-
|
|
105
|
+
// The identity *connection* is the workspace's, like every other (ADR-057),
|
|
106
|
+
// so provisioning the surface on first use touches two files. Both saves, or
|
|
107
|
+
// neither: a grant naming a connection that does not exist is refused at load,
|
|
108
|
+
// so writing the profile alone would leave one that no longer opens.
|
|
109
|
+
const connections = await ConfigDocument.openKey(resolution.workspaceRoot, CONNECTIONS_FILE);
|
|
110
|
+
const repair = ensureIdentityConnection(connections, document);
|
|
111
|
+
await connections.save();
|
|
100
112
|
await document.save();
|
|
101
113
|
|
|
102
114
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
soleGrantFor,
|
|
2
3
|
ConfigError,
|
|
3
4
|
KNOWLEDGE_LAYOUT,
|
|
4
5
|
knowledgeTargetSchema,
|
|
@@ -23,7 +24,6 @@ import {
|
|
|
23
24
|
removeLocal,
|
|
24
25
|
summarise,
|
|
25
26
|
} from './migrate.ts';
|
|
26
|
-
|
|
27
27
|
/**
|
|
28
28
|
* `lanes link knowledge` — where this profile's memory and skills are kept.
|
|
29
29
|
*
|
|
@@ -65,78 +65,6 @@ export interface KnowledgeFlags extends GlobalFlags {
|
|
|
65
65
|
readonly fetch?: FetchLike | undefined;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
|
|
69
|
-
const runtime = await openRuntime(flags, { fetch: flags.fetch });
|
|
70
|
-
try {
|
|
71
|
-
if (!flags.json) announce(runtime.resolution);
|
|
72
|
-
|
|
73
|
-
const profile = runtime.config.instance.profile;
|
|
74
|
-
const selection = ` --profile ${runtime.resolution.profile} --target ${runtime.target}`;
|
|
75
|
-
const skills = (await runtime.skills.list()).length;
|
|
76
|
-
const memory = (await runtime.storage.list(`${KNOWLEDGE_LAYOUT.memory}/`)).length;
|
|
77
|
-
const entities = (await runtime.storage.list(`${KNOWLEDGE_LAYOUT.entities}/`)).length;
|
|
78
|
-
const where = runtime.knowledge?.describe;
|
|
79
|
-
|
|
80
|
-
if (flags.json) {
|
|
81
|
-
print(
|
|
82
|
-
JSON.stringify(
|
|
83
|
-
{ target: runtime.target, where: where ?? 'local', memory, skills, entities },
|
|
84
|
-
null,
|
|
85
|
-
2,
|
|
86
|
-
),
|
|
87
|
-
);
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
heading('Knowledge');
|
|
92
|
-
table([
|
|
93
|
-
// The memory *directory*, not the blob root it sits in. `layout.blobs`
|
|
94
|
-
// is `data/<profile>`, which is where every provider's namespace lives —
|
|
95
|
-
// printing it here would name a directory that is mostly not memory.
|
|
96
|
-
[
|
|
97
|
-
' memory',
|
|
98
|
-
where ? `${where}/${KNOWLEDGE_LAYOUT.memory}` : `${layout.blobs(profile)}/${KNOWLEDGE_LAYOUT.memory}`,
|
|
99
|
-
style.dim(`${memory} file${memory === 1 ? '' : 's'}`),
|
|
100
|
-
],
|
|
101
|
-
[
|
|
102
|
-
' skills',
|
|
103
|
-
where ? `${where}/${KNOWLEDGE_LAYOUT.skills}` : layout.skills(profile),
|
|
104
|
-
style.dim(`${skills} file${skills === 1 ? '' : 's'}`),
|
|
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
|
-
],
|
|
116
|
-
]);
|
|
117
|
-
|
|
118
|
-
print('');
|
|
119
|
-
print(
|
|
120
|
-
style.dim(
|
|
121
|
-
` The vault, the credential store, runtime state and the audit log stay in target "${runtime.target}".`,
|
|
122
|
-
),
|
|
123
|
-
);
|
|
124
|
-
// Complete commands, not shapes. Every one of these is pasted, and with
|
|
125
|
-
// nothing left to fall back on (ADR-037) a line missing either flag is a
|
|
126
|
-
// line that refuses — `emitted.test.ts` states the rule for the templates
|
|
127
|
-
// reachable without a runtime, and this is the same rule where there is one.
|
|
128
|
-
print(
|
|
129
|
-
style.dim(
|
|
130
|
-
where
|
|
131
|
-
? ` Bring them back with: lanes link knowledge use local --migrate${selection}`
|
|
132
|
-
: ` Keep them in a repository with: lanes link knowledge use github --repo <owner/name>${selection}`,
|
|
133
|
-
),
|
|
134
|
-
);
|
|
135
|
-
} finally {
|
|
136
|
-
await runtime.close();
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
68
|
export async function knowledgeUse(where: string | undefined, flags: KnowledgeFlags): Promise<void> {
|
|
141
69
|
if (where === 'local') return useLocal(flags);
|
|
142
70
|
if (where === 'github') return useGithub(flags);
|
|
@@ -172,7 +100,7 @@ async function useGithub(flags: KnowledgeFlags): Promise<void> {
|
|
|
172
100
|
const runtime = await openRuntime(flags, { fetch: flags.fetch });
|
|
173
101
|
try {
|
|
174
102
|
announce(runtime.resolution);
|
|
175
|
-
const selection = ` --profile ${runtime.resolution.profile} --
|
|
103
|
+
const selection = ` --profile ${runtime.resolution.profile} --workspace ${runtime.target}`;
|
|
176
104
|
|
|
177
105
|
if (runtime.knowledge) {
|
|
178
106
|
print(style.dim(` This profile already reads ${runtime.knowledge.describe}.`));
|
|
@@ -206,7 +134,16 @@ async function useGithub(flags: KnowledgeFlags): Promise<void> {
|
|
|
206
134
|
[' token', viewer, style.dim('can write')],
|
|
207
135
|
]);
|
|
208
136
|
|
|
209
|
-
|
|
137
|
+
// Which instances this profile grants, so `memory/` and `entities/` reach
|
|
138
|
+
// only its own and not every profile's (ADR-059).
|
|
139
|
+
const instances = grantedInstances(runtime.config);
|
|
140
|
+
|
|
141
|
+
const movable = await localContents(
|
|
142
|
+
runtime.storage,
|
|
143
|
+
runtime.skills ?? null,
|
|
144
|
+
knowledge,
|
|
145
|
+
instances,
|
|
146
|
+
);
|
|
210
147
|
const moving = await decideMigration(movable.length > 0, flags);
|
|
211
148
|
|
|
212
149
|
if (moving) {
|
|
@@ -225,7 +162,7 @@ async function useGithub(flags: KnowledgeFlags): Promise<void> {
|
|
|
225
162
|
if (flags.keep) {
|
|
226
163
|
print(style.dim(' --keep: the local copies are still there, and are no longer read.'));
|
|
227
164
|
} else {
|
|
228
|
-
await removeLocal(runtime.storage, runtime.skills, movable);
|
|
165
|
+
await removeLocal(runtime.storage, runtime.skills ?? null, movable, instances);
|
|
229
166
|
print(ok('removed the local copies'));
|
|
230
167
|
}
|
|
231
168
|
} else if (movable.length > 0) {
|
|
@@ -280,7 +217,13 @@ async function useLocal(flags: KnowledgeFlags): Promise<void> {
|
|
|
280
217
|
flags.migrate ??
|
|
281
218
|
(await agreedTo(flags, `Copy everything in ${knowledge.repo} back onto this target's storage?`))
|
|
282
219
|
) {
|
|
283
|
-
const moved = await moveOut(
|
|
220
|
+
const moved = await moveOut(
|
|
221
|
+
repository,
|
|
222
|
+
knowledge,
|
|
223
|
+
local.storage,
|
|
224
|
+
local.skills,
|
|
225
|
+
grantedInstances(runtime.config),
|
|
226
|
+
);
|
|
284
227
|
print(
|
|
285
228
|
ok(
|
|
286
229
|
`wrote back ${moved.memory} memory entr${moved.memory === 1 ? 'y' : 'ies'}, ` +
|
|
@@ -314,7 +257,7 @@ async function useLocal(flags: KnowledgeFlags): Promise<void> {
|
|
|
314
257
|
*/
|
|
315
258
|
async function openLocalStores(
|
|
316
259
|
runtime: Runtime,
|
|
317
|
-
): Promise<{ storage: BlobStore; skills: BlobStore }> {
|
|
260
|
+
): Promise<{ storage: BlobStore; skills: BlobStore | null }> {
|
|
318
261
|
const { openStorage } = await import('#deployments/target.ts');
|
|
319
262
|
const declared = runtime.declared;
|
|
320
263
|
|
|
@@ -327,7 +270,13 @@ async function openLocalStores(
|
|
|
327
270
|
},
|
|
328
271
|
runtime.credentials,
|
|
329
272
|
);
|
|
330
|
-
|
|
273
|
+
// The *granted* connection, not the profile name. `layout.skills` changed
|
|
274
|
+
// meaning without changing arity at ADR-059, so the compiler was silent.
|
|
275
|
+
const skillsConnection = soleGrantFor(runtime.config, 'skills');
|
|
276
|
+
return {
|
|
277
|
+
storage: factory(),
|
|
278
|
+
skills: skillsConnection === undefined ? null : factory(layout.skills(skillsConnection)),
|
|
279
|
+
};
|
|
331
280
|
}
|
|
332
281
|
|
|
333
282
|
/**
|
|
@@ -386,3 +335,21 @@ async function agreedTo(flags: KnowledgeFlags, question: string): Promise<boolea
|
|
|
386
335
|
if (!yes) print(style.dim(' cancelled'));
|
|
387
336
|
return yes;
|
|
388
337
|
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The memory and entities instances one profile grants.
|
|
341
|
+
*
|
|
342
|
+
* `main` where a profile grants none, which is what a profile that predates the
|
|
343
|
+
* owner layer looks like and is also where a fresh one puts them. The point is
|
|
344
|
+
* that this is never the *surface* — `memory/` alone matches every profile's
|
|
345
|
+
* notes now that the blob root is the workspace's.
|
|
346
|
+
*/
|
|
347
|
+
function grantedInstances(config: Parameters<typeof soleGrantFor>[0]): {
|
|
348
|
+
memory: string;
|
|
349
|
+
entities: string;
|
|
350
|
+
} {
|
|
351
|
+
return {
|
|
352
|
+
memory: soleGrantFor(config, 'memory') ?? 'main',
|
|
353
|
+
entities: soleGrantFor(config, 'entities') ?? 'main',
|
|
354
|
+
};
|
|
355
|
+
}
|
|
@@ -43,16 +43,71 @@ export interface Movable {
|
|
|
43
43
|
* it once means `localContents`, `removeLocal` and `moveOut` each became a loop
|
|
44
44
|
* over this rather than a branch on a literal.
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* One area's local home: where to look, and what of that key is the repository's.
|
|
48
|
+
*
|
|
49
|
+
* Two prefixes rather than one, and the difference is the whole fix. `scope` is
|
|
50
|
+
* what is listed — `memory/<instance>/`, so a profile sees only the instance it
|
|
51
|
+
* grants. `prefix` is what is stripped to get the repository path, which stays
|
|
52
|
+
* `memory/<instance>/<file>` because that is the layout ADR-041 documents and
|
|
53
|
+
* what makes two profiles' notes distinguishable in one repository.
|
|
54
|
+
*
|
|
55
|
+
* Collapsing them listed every profile's memory and then wrote it flat.
|
|
56
|
+
*/
|
|
57
|
+
type LocalArea = {
|
|
58
|
+
readonly store: BlobStore;
|
|
59
|
+
readonly scope: string;
|
|
60
|
+
readonly prefix: string;
|
|
61
|
+
};
|
|
47
62
|
|
|
48
|
-
|
|
63
|
+
/** The connection ids this profile's memory and entities live under. */
|
|
64
|
+
export interface Instances {
|
|
65
|
+
readonly memory: string;
|
|
66
|
+
readonly entities: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function localAreas(
|
|
70
|
+
storage: BlobStore,
|
|
71
|
+
skills: BlobStore | null,
|
|
72
|
+
/** Which instance of each surface this profile grants (ADR-059). */
|
|
73
|
+
instances: Instances,
|
|
74
|
+
): Record<KnowledgeArea, LocalArea> {
|
|
49
75
|
return {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
// Scoped to the instance this profile grants, not to the surface.
|
|
77
|
+
//
|
|
78
|
+
// `layout.blobs()` is the whole workspace since contract 3, so a prefix of
|
|
79
|
+
// `memory/` matched every profile's memory — and `knowledge use github
|
|
80
|
+
// --migrate --profile personal` therefore committed `work`'s notes to
|
|
81
|
+
// personal's repository and then deleted them locally, leaving `work`
|
|
82
|
+
// reading an empty store with no knowledge block of its own. `skills` was
|
|
83
|
+
// already connection-scoped, which is why it alone was correct.
|
|
84
|
+
memory: {
|
|
85
|
+
store: storage,
|
|
86
|
+
scope: `${KNOWLEDGE_LAYOUT.memory}/${instances.memory}/`,
|
|
87
|
+
prefix: `${KNOWLEDGE_LAYOUT.memory}/`,
|
|
88
|
+
},
|
|
89
|
+
// Null becomes an empty area rather than a refusal: a profile granting no
|
|
90
|
+
// skills connection has none to move, and `knowledge use` should still move
|
|
91
|
+
// its memory and entities rather than failing on the one area that is empty
|
|
92
|
+
// by construction (ADR-059).
|
|
93
|
+
skills: { store: skills ?? EMPTY_AREA, scope: '', prefix: '' },
|
|
94
|
+
entities: {
|
|
95
|
+
store: storage,
|
|
96
|
+
scope: `${KNOWLEDGE_LAYOUT.entities}/${instances.entities}/`,
|
|
97
|
+
prefix: `${KNOWLEDGE_LAYOUT.entities}/`,
|
|
98
|
+
},
|
|
53
99
|
};
|
|
54
100
|
}
|
|
55
101
|
|
|
102
|
+
/** Nothing to list, nothing to delete. See `localAreas`. */
|
|
103
|
+
const EMPTY_AREA: BlobStore = {
|
|
104
|
+
get: async () => null,
|
|
105
|
+
put: async () => {},
|
|
106
|
+
has: async () => false,
|
|
107
|
+
delete: async () => {},
|
|
108
|
+
list: async () => [],
|
|
109
|
+
};
|
|
110
|
+
|
|
56
111
|
const AREAS = ['memory', 'skills', 'entities'] as const;
|
|
57
112
|
|
|
58
113
|
/**
|
|
@@ -66,16 +121,18 @@ const AREAS = ['memory', 'skills', 'entities'] as const;
|
|
|
66
121
|
*/
|
|
67
122
|
export async function localContents(
|
|
68
123
|
storage: BlobStore,
|
|
69
|
-
skills
|
|
124
|
+
/** Null when the profile grants no skills connection (ADR-059). */
|
|
125
|
+
skills: BlobStore | null,
|
|
70
126
|
knowledge: KnowledgeConfig,
|
|
127
|
+
instances: Instances,
|
|
71
128
|
): Promise<Movable[]> {
|
|
72
|
-
const areas = localAreas(storage, skills);
|
|
129
|
+
const areas = localAreas(storage, skills, instances);
|
|
73
130
|
const found: Movable[] = [];
|
|
74
131
|
|
|
75
132
|
for (const area of AREAS) {
|
|
76
|
-
const { store, prefix } = areas[area];
|
|
133
|
+
const { store, scope, prefix } = areas[area];
|
|
77
134
|
|
|
78
|
-
for (const entry of await store.list(
|
|
135
|
+
for (const entry of await store.list(scope)) {
|
|
79
136
|
const data = await store.get(entry.key);
|
|
80
137
|
if (data === null) continue; // Listed then deleted; not worth failing over.
|
|
81
138
|
|
|
@@ -128,10 +185,12 @@ export async function moveIn(
|
|
|
128
185
|
/** Remove what has been verified elsewhere. Never called before `moveIn`. */
|
|
129
186
|
export async function removeLocal(
|
|
130
187
|
storage: BlobStore,
|
|
131
|
-
skills
|
|
188
|
+
/** Null when the profile grants no skills connection (ADR-059). */
|
|
189
|
+
skills: BlobStore | null,
|
|
132
190
|
movable: readonly Movable[],
|
|
191
|
+
instances: Instances,
|
|
133
192
|
): Promise<void> {
|
|
134
|
-
const areas = localAreas(storage, skills);
|
|
193
|
+
const areas = localAreas(storage, skills, instances);
|
|
135
194
|
|
|
136
195
|
for (const item of movable) {
|
|
137
196
|
const { store, prefix } = areas[item.area];
|
|
@@ -152,10 +211,12 @@ export async function moveOut(
|
|
|
152
211
|
repository: GithubRepository,
|
|
153
212
|
knowledge: KnowledgeConfig,
|
|
154
213
|
storage: BlobStore,
|
|
155
|
-
skills
|
|
214
|
+
/** Null when the profile grants no skills connection (ADR-059). */
|
|
215
|
+
skills: BlobStore | null,
|
|
216
|
+
instances: Instances,
|
|
156
217
|
): Promise<Record<KnowledgeArea, number>> {
|
|
157
218
|
const { entries } = await repository.entries();
|
|
158
|
-
const areas = localAreas(storage, skills);
|
|
219
|
+
const areas = localAreas(storage, skills, instances);
|
|
159
220
|
const counts: Record<KnowledgeArea, number> = { memory: 0, skills: 0, entities: 0 };
|
|
160
221
|
|
|
161
222
|
for (const entry of entries.values()) {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { KNOWLEDGE_LAYOUT, layout, soleGrantFor } from '#profile';
|
|
2
|
+
import { announce, emit, heading, print, style, table } from '../../output.ts';
|
|
3
|
+
import { openRuntime } from '../../runtime.ts';
|
|
4
|
+
import type { KnowledgeFlags } from './index.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `lanes link knowledge show` — where memory, skills and entities actually are.
|
|
8
|
+
*
|
|
9
|
+
* Split from `knowledge use` so `index.ts` stays inside the size budget, on the
|
|
10
|
+
* seam the two commands already have: this one opens stores and counts, and
|
|
11
|
+
* never writes. That difference is why `show` can answer for a repository `use`
|
|
12
|
+
* would refuse to touch.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export async function knowledgeShow(flags: KnowledgeFlags): Promise<void> {
|
|
16
|
+
const runtime = await openRuntime(flags, { fetch: flags.fetch });
|
|
17
|
+
try {
|
|
18
|
+
if (!flags.json) announce(runtime.resolution);
|
|
19
|
+
|
|
20
|
+
const profile = runtime.config.instance.profile;
|
|
21
|
+
const selection = ` --profile ${runtime.resolution.profile} --target ${runtime.target}`;
|
|
22
|
+
const skills = runtime.skills ? (await runtime.skills.list()).length : 0;
|
|
23
|
+
const memory = (await runtime.storage.list(`${KNOWLEDGE_LAYOUT.memory}/`)).length;
|
|
24
|
+
const entities = (await runtime.storage.list(`${KNOWLEDGE_LAYOUT.entities}/`)).length;
|
|
25
|
+
const where = runtime.knowledge?.describe;
|
|
26
|
+
// Null when no skills connection is granted (ADR-059). The row still prints,
|
|
27
|
+
// saying so: a missing row reads as "there are none", not "not granted".
|
|
28
|
+
const skillsConnection = soleGrantFor(runtime.config, 'skills');
|
|
29
|
+
|
|
30
|
+
if (flags.json) {
|
|
31
|
+
print(
|
|
32
|
+
JSON.stringify(
|
|
33
|
+
{ target: runtime.target, where: where ?? 'local', memory, skills, entities },
|
|
34
|
+
null,
|
|
35
|
+
2,
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
heading('Knowledge');
|
|
42
|
+
table([
|
|
43
|
+
// The memory *directory*, not the blob root it sits in. `layout.blobs`
|
|
44
|
+
// is `data/`, which is where every provider's namespace lives —
|
|
45
|
+
// printing it here would name a directory that is mostly not memory.
|
|
46
|
+
[
|
|
47
|
+
' memory',
|
|
48
|
+
where ? `${where}/${KNOWLEDGE_LAYOUT.memory}` : `${layout.blobs()}/${KNOWLEDGE_LAYOUT.memory}`,
|
|
49
|
+
style.dim(`${memory} file${memory === 1 ? '' : 's'}`),
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
' skills',
|
|
53
|
+
where
|
|
54
|
+
? `${where}/${KNOWLEDGE_LAYOUT.skills}`
|
|
55
|
+
: skillsConnection
|
|
56
|
+
? layout.skills(skillsConnection)
|
|
57
|
+
: style.dim('not granted'),
|
|
58
|
+
style.dim(`${skills} file${skills === 1 ? '' : 's'}`),
|
|
59
|
+
],
|
|
60
|
+
// The count includes the derived `_index.json`, deliberately: it is a
|
|
61
|
+
// file in that directory and it is committed with the rest, so a number
|
|
62
|
+
// that quietly excluded it would not match what a person sees there.
|
|
63
|
+
[
|
|
64
|
+
' entities',
|
|
65
|
+
where
|
|
66
|
+
? `${where}/${KNOWLEDGE_LAYOUT.entities}`
|
|
67
|
+
: `${layout.blobs()}/${KNOWLEDGE_LAYOUT.entities}`,
|
|
68
|
+
style.dim(`${entities} file${entities === 1 ? '' : 's'}`),
|
|
69
|
+
],
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
print('');
|
|
73
|
+
print(
|
|
74
|
+
style.dim(
|
|
75
|
+
` The vault, the credential store, runtime state and the audit log stay in workspace "${runtime.target}".`,
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
// Complete commands, not shapes. Every one of these is pasted, and with
|
|
79
|
+
// nothing left to fall back on (ADR-037) a line missing either flag is a
|
|
80
|
+
// line that refuses — `emitted.test.ts` states the rule for the templates
|
|
81
|
+
// reachable without a runtime, and this is the same rule where there is one.
|
|
82
|
+
print(
|
|
83
|
+
style.dim(
|
|
84
|
+
where
|
|
85
|
+
? ` Bring them back with: lanes link knowledge use local --migrate${selection}`
|
|
86
|
+
: ` Keep them in a repository with: lanes link knowledge use github --repo <owner/name>${selection}`,
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
} finally {
|
|
90
|
+
await runtime.close();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -7,4 +7,5 @@
|
|
|
7
7
|
* the grammar that puts them in order.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
export {
|
|
10
|
+
export { knowledgeUse, type KnowledgeFlags } from './knowledge/index.ts';
|
|
11
|
+
export { knowledgeShow } from './knowledge/show.ts';
|
|
@@ -17,12 +17,31 @@ import { join } from 'node:path';
|
|
|
17
17
|
* command, write the file where it does not. A skill is content in a documented
|
|
18
18
|
* location, not a config format we would be guessing at, and the directory
|
|
19
19
|
* written is named after this project.
|
|
20
|
+
*
|
|
21
|
+
* **No registration carries a token any more** (ADR-062). Every endpoint runs
|
|
22
|
+
* the authorization flow, discovery is served ahead of the auth gate on
|
|
23
|
+
* loopback as well as deployed, and a client pointed at a bare URL finds
|
|
24
|
+
* `/.well-known/oauth-protected-resource`, signs its owner in at lanes.sh, and
|
|
25
|
+
* comes back with a token of its own. What that removes is worth stating: the
|
|
26
|
+
* registration no longer contains a credential, so a config file synced to a
|
|
27
|
+
* dotfiles repository is no longer a leak, and a rotation does not invalidate
|
|
28
|
+
* every harness at once.
|
|
29
|
+
*
|
|
30
|
+
* The exception is `tokenEnv`, which survives for the one caller that has no
|
|
31
|
+
* browser — see `token show`, which is now documented as a CI command.
|
|
20
32
|
*/
|
|
21
33
|
|
|
22
34
|
export interface AddInput {
|
|
23
35
|
readonly name: string;
|
|
24
36
|
readonly url: string;
|
|
25
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The endpoint's static token, for a harness that cannot run a browser.
|
|
39
|
+
*
|
|
40
|
+
* Nothing passes this in the ordinary path. It is here because
|
|
41
|
+
* `--headless` exists for CI, where there is no browser to complete an
|
|
42
|
+
* authorization in and a pasted credential is the only thing that works.
|
|
43
|
+
*/
|
|
44
|
+
readonly token?: string | undefined;
|
|
26
45
|
readonly tokenEnv: string;
|
|
27
46
|
readonly scope: string;
|
|
28
47
|
/**
|
|
@@ -44,11 +63,12 @@ export interface Harness {
|
|
|
44
63
|
/** Whether `--scope` means anything here. Codex config is global. */
|
|
45
64
|
readonly scoped: boolean;
|
|
46
65
|
/**
|
|
47
|
-
* Whether
|
|
66
|
+
* Whether a credential reaches the harness's config at all.
|
|
48
67
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
68
|
+
* Both are `false` in the ordinary path now — the client authorises itself.
|
|
69
|
+
* This still distinguishes what a `--headless` registration would write:
|
|
70
|
+
* Claude Code would hold the header value, and Codex stores only the *name*
|
|
71
|
+
* of an environment variable it reads at launch.
|
|
52
72
|
*/
|
|
53
73
|
readonly storesToken: boolean;
|
|
54
74
|
/**
|
|
@@ -99,8 +119,10 @@ export const HARNESSES: readonly Harness[] = [
|
|
|
99
119
|
'http',
|
|
100
120
|
name,
|
|
101
121
|
url,
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
// Only when there is no browser to sign in with. Registering the bare URL
|
|
123
|
+
// is the ordinary path: Claude Code discovers the protected-resource
|
|
124
|
+
// document and runs the authorization itself.
|
|
125
|
+
...(token ? ['--header', `Authorization: Bearer ${token}`] : []),
|
|
104
126
|
'--scope',
|
|
105
127
|
scope,
|
|
106
128
|
],
|
|
@@ -142,7 +164,7 @@ export const HARNESSES: readonly Harness[] = [
|
|
|
142
164
|
// anywhere else: an unresolvable substitution yields the empty string, the
|
|
143
165
|
// header becomes "Bearer ", and the only symptom is a 401 that reads as a
|
|
144
166
|
// bad token rather than a command that refused.
|
|
145
|
-
` export ${tokenEnv}="$(lanes link token show --raw --profile ${profile} --
|
|
167
|
+
` export ${tokenEnv}="$(lanes link token show --raw --profile ${profile} --workspace ${target})"`,
|
|
146
168
|
'',
|
|
147
169
|
'Add that to your shell profile. This is the better half of the bargain: the token never',
|
|
148
170
|
'reaches ~/.codex/config.toml, and a "lanes link token rotate" is picked up on next launch',
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { print } from '../../output.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `lanes link mcp install-instructions` — the block a client should be told once.
|
|
5
|
+
*
|
|
6
|
+
* A registration points a client at this endpoint. It does not tell the *model*
|
|
7
|
+
* anything, and the model is what decides whether to look here at all. The
|
|
8
|
+
* common failure is not a client that cannot reach the endpoint; it is one that
|
|
9
|
+
* can and answers from nothing anyway, because reaching for a tool is a habit
|
|
10
|
+
* and no habit is installed by adding a server to a config file.
|
|
11
|
+
*
|
|
12
|
+
* So this prints a short block for a person to paste into whatever their client
|
|
13
|
+
* treats as durable instruction — a project file, a custom instruction, a
|
|
14
|
+
* memory. It says where things are and what to read first, and nothing else:
|
|
15
|
+
* the full account lives at `lanes://instructions` and is served by the
|
|
16
|
+
* endpoint, so it can be corrected without anybody pasting anything again.
|
|
17
|
+
*
|
|
18
|
+
* **Printed rather than written.** Every client keeps this somewhere different
|
|
19
|
+
* and several keep it somewhere a person curates by hand. Writing into one of
|
|
20
|
+
* those is the class of thing ADR-016 puts on the other side of the line from
|
|
21
|
+
* `mcp add`: delegate where the harness owns a command, write the file only
|
|
22
|
+
* where it does not, and where neither is true, hand over the text.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** What each client calls the place this belongs, so the hint is actionable. */
|
|
26
|
+
const WHERE: Record<string, string> = {
|
|
27
|
+
claude: 'CLAUDE.md in your project, or ~/.claude/CLAUDE.md for every project',
|
|
28
|
+
chatgpt: "Settings → Personalization → Custom instructions, under 'anything else'",
|
|
29
|
+
codex: 'AGENTS.md in your project, or ~/.codex/AGENTS.md',
|
|
30
|
+
cursor: '.cursor/rules/ in your project',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export interface InstallInstructionsFlags {
|
|
34
|
+
readonly client?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function installInstructions(flags: InstallInstructionsFlags = {}): void {
|
|
38
|
+
const client = flags.client?.toLowerCase();
|
|
39
|
+
|
|
40
|
+
if (client !== undefined && !(client in WHERE)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Unknown client "${client}". Known: ${Object.keys(WHERE).join(', ')}.\n` +
|
|
43
|
+
' Omit --client for the block on its own.',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (client !== undefined) {
|
|
48
|
+
// To stderr, so the block on stdout stays pasteable. Somebody piping this
|
|
49
|
+
// into a file wants the text and not the advice about where to put it.
|
|
50
|
+
process.stderr.write(`Paste this into ${WHERE[client]}:\n\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
print(BLOCK);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The block itself.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately short and deliberately not a copy of the endpoint's own
|
|
60
|
+
* instructions. It exists to establish one habit — look here before answering
|
|
61
|
+
* from nothing — and to name the document that carries the rest. A longer block
|
|
62
|
+
* pasted into a project file is one that goes stale in a place nobody will
|
|
63
|
+
* think to update.
|
|
64
|
+
*
|
|
65
|
+
* No vendor names and no account details, for the same reason nothing under
|
|
66
|
+
* `server/` may carry one: what the owner connected is theirs.
|
|
67
|
+
*/
|
|
68
|
+
const BLOCK = `## Lanes Link
|
|
69
|
+
|
|
70
|
+
Lanes Link is the route to my accounts, my memory, my notes, my saved procedures
|
|
71
|
+
and my contacts. It is an MCP endpoint, and it is already connected.
|
|
72
|
+
|
|
73
|
+
- Before saying you do not know something about me or my work, search memory.
|
|
74
|
+
- Before using anyone's address or handle, look them up in entities. It returns
|
|
75
|
+
every match and never picks one; if there is more than one, ask me.
|
|
76
|
+
- A thing for me to do is a task, not a memory. Tasks have a status.
|
|
77
|
+
- If I have a saved procedure for something, tell me it exists and let me run it
|
|
78
|
+
rather than improvising your own version.
|
|
79
|
+
- Before telling me something cannot be reached or that an account is missing,
|
|
80
|
+
ask the setup surface. It reports what exists and gives the exact command for
|
|
81
|
+
what does not. Run nothing yourself: every change here is mine to make.
|
|
82
|
+
- Read \`lanes://instructions\` before your first call for the full account of
|
|
83
|
+
how routing, profiles and refusals work.
|
|
84
|
+
|
|
85
|
+
Every tool takes a profile. A profile is how I keep things apart, so when it is
|
|
86
|
+
ambiguous which one I mean, ask rather than picking the first.`;
|
|
@@ -32,6 +32,17 @@ export interface McpAddOptions extends GlobalFlags {
|
|
|
32
32
|
readonly force?: boolean | undefined;
|
|
33
33
|
/** Register only; leave the skill and the agent alone. */
|
|
34
34
|
readonly noSkill?: boolean | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Write the endpoint token into the registration, for a machine with no browser.
|
|
37
|
+
*
|
|
38
|
+
* The ordinary path registers a bare URL and the client authorises itself
|
|
39
|
+
* against lanes.sh (ADR-062), which is both safer and less to get wrong. This
|
|
40
|
+
* is the CI escape hatch, and it is a flag rather than a fallback because the
|
|
41
|
+
* difference has to be a decision somebody made: a registration that quietly
|
|
42
|
+
* embedded a credential when a browser was unavailable is one nobody would
|
|
43
|
+
* notice had done so.
|
|
44
|
+
*/
|
|
45
|
+
readonly headless?: boolean | undefined;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
export async function mcpAdd(target: string | undefined, options: McpAddOptions): Promise<void> {
|
|
@@ -71,17 +82,20 @@ export async function mcpAdd(target: string | undefined, options: McpAddOptions)
|
|
|
71
82
|
const runtime = await openRuntime(options);
|
|
72
83
|
|
|
73
84
|
try {
|
|
85
|
+
// Minted either way. The endpoint needs one to serve at all, and `outputs`
|
|
86
|
+
// prints it; what `--headless` decides is whether it is written into
|
|
87
|
+
// somebody's agent config.
|
|
74
88
|
const { token } = await ensureProfileToken(runtime.credentials, runtime.config.auth.token_ref);
|
|
75
89
|
|
|
76
90
|
// The target's own address, not the local one. This built
|
|
77
|
-
// `http://<host>:<port>/mcp` unconditionally, so `mcp add --
|
|
91
|
+
// `http://<host>:<port>/mcp` unconditionally, so `mcp add --workspace cloud`
|
|
78
92
|
// registered loopback with the agent: a registration that reports success,
|
|
79
93
|
// names the right server, and points at a port with nothing behind it.
|
|
80
94
|
const url = await endpointUrl(runtime.config, runtime.declared);
|
|
81
95
|
const input: AddInput = {
|
|
82
96
|
name,
|
|
83
97
|
url,
|
|
84
|
-
token,
|
|
98
|
+
...(options.headless === true ? { token } : {}),
|
|
85
99
|
tokenEnv,
|
|
86
100
|
scope,
|
|
87
101
|
profile: runtime.resolution.profile,
|
package/src/cli/commands/mcp.ts
CHANGED
|
@@ -20,3 +20,4 @@ export { harnessCommands } from './mcp/harnesses.ts';
|
|
|
20
20
|
export { mcpList } from './mcp/list.ts';
|
|
21
21
|
export { mcpAdd, type McpAddOptions } from './mcp/register.ts';
|
|
22
22
|
export { mcpStdio } from './mcp/stdio.ts';
|
|
23
|
+
export { installInstructions, type InstallInstructionsFlags } from './mcp/onboarding.ts';
|